menu.inc 102 KB
Newer Older
Dries's avatar
 
Dries committed
1
<?php
Kjartan's avatar
Kjartan committed
2

3
4
5
6
7
/**
 * @file
 * API for the Drupal menu system.
 */

8
use Drupal\Component\Utility\NestedArray;
9
use Drupal\Component\Utility\String;
10
use Drupal\Core\Cache\Cache;
11
use Drupal\Core\Language\Language;
12
use Drupal\Core\ParamConverter\ParamNotConvertedException;
13
use Drupal\Core\Routing\RequestHelper;
14
use Drupal\Core\Template\Attribute;
15
use Drupal\menu_link\Entity\MenuLink;
16
use Drupal\menu_link\MenuLinkInterface;
17
use Drupal\menu_link\MenuLinkStorageController;
18
use Symfony\Cmf\Component\Routing\RouteObjectInterface;
19
use Symfony\Component\HttpFoundation\Request;
20
use Symfony\Component\Routing\Route;
21

Dries's avatar
   
Dries committed
22
/**
23
 * @defgroup menu Menu and routing system
Dries's avatar
   
Dries committed
24
 * @{
Dries's avatar
   
Dries committed
25
 * Define the navigation menus, and route page requests to code based on URLs.
Dries's avatar
   
Dries committed
26
 *
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
 * The Drupal routing system defines how Drupal responds to URLs passed to the
 * browser. The menu system, which depends on the routing system, is used for
 * navigation. The Menu module allows menus to be created in the user interface
 * as hierarchical lists of links.
 *
 * @section registering_paths Registering router paths
 * To register a path, you need to add lines similar to this in a
 * module.routing.yml file:
 * @code
 * block.admin_display:
 *   path: '/admin/structure/block'
 *   defaults:
 *     _content: '\Drupal\block\Controller\BlockListController::listing'
 *   requirements:
 *     _permission: 'administer blocks'
 * @endcode
 * @todo Add more information here, especially about controllers and what all
 *   the stuff in the routing.yml file means.
 *
 * @section Defining menu links
 * Once you have a route defined, you can use hook_menu() to define links
 * for your module's paths in the main Navigation menu or other menus. See
 * the hook_menu() documentation for more details.
 *
 * @todo The rest of this topic has not been reviewed or updated for Drupal 8.x
 *   and is not correct!
 * @todo It is quite likely that hook_menu() will be replaced with a different
 *   hook, configuration system, or plugin system before the 8.0 release.
Dries's avatar
   
Dries committed
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
 *
 * Drupal's menu system follows a simple hierarchy defined by paths.
 * Implementations of hook_menu() define menu items and assign them to
 * paths (which should be unique). The menu system aggregates these items
 * and determines the menu hierarchy from the paths. For example, if the
 * paths defined were a, a/b, e, a/b/c/d, f/g, and a/b/h, the menu system
 * would form the structure:
 * - a
 *   - a/b
 *     - a/b/c/d
 *     - a/b/h
 * - e
 * - f/g
 * Note that the number of elements in the path does not necessarily
 * determine the depth of the menu item in the tree.
 *
 * When responding to a page request, the menu system looks to see if the
 * path requested by the browser is registered as a menu item with a
 * callback. If not, the system searches up the menu tree for the most
 * complete match with a callback it can find. If the path a/b/i is
 * requested in the tree above, the callback for a/b would be used.
 *
Steven Wittens's avatar
Steven Wittens committed
77
 * The found callback function is called with any arguments specified
78
 * in the "page arguments" attribute of its menu item. The
Steven Wittens's avatar
Steven Wittens committed
79
80
81
82
 * attribute must be an array. After these arguments, any remaining
 * components of the path are appended as further arguments. In this
 * way, the callback for a/b above could respond to a request for
 * a/b/i differently than a request for a/b/j.
Dries's avatar
   
Dries committed
83
84
85
86
 *
 * For an illustration of this process, see page_example.module.
 *
 * Access to the callback functions is also protected by the menu system.
87
88
 * The "access callback" with an optional "access arguments" of each menu
 * item is called before the page callback proceeds. If this returns TRUE,
89
90
91
 * then access is granted; if FALSE, then access is denied. Default local task
 * menu items (see next paragraph) may omit this attribute to use the value
 * provided by the parent item.
Dries's avatar
   
Dries committed
92
93
94
95
96
97
98
99
100
101
102
103
104
 *
 * In the default Drupal interface, you will notice many links rendered as
 * tabs. These are known in the menu system as "local tasks", and they are
 * rendered as tabs by default, though other presentations are possible.
 * Local tasks function just as other menu items in most respects. It is
 * convention that the names of these tasks should be short verbs if
 * possible. In addition, a "default" local task should be provided for
 * each set. When visiting a local task's parent menu item, the default
 * local task will be rendered as if it is selected; this provides for a
 * normal tab user experience. This default task is special in that it
 * links not to its provided path, but to its parent item's path instead.
 * The default task's path is only used to place it appropriately in the
 * menu hierarchy.
105
106
107
 *
 * Everything described so far is stored in the menu_router table. The
 * menu_links table holds the visible menu links. By default these are
108
 * derived from the same hook_menu definitions, however you are free to
109
 * add more with menu_link_save().
Dries's avatar
   
Dries committed
110
111
 */

Dries's avatar
   
Dries committed
112
/**
113
 * @defgroup menu_flags Menu flags
Dries's avatar
   
Dries committed
114
 * @{
Dries's avatar
   
Dries committed
115
116
 * Flags for use in the "type" attribute of menu items.
 */
Dries's avatar
   
Dries committed
117

118
119
120
/**
 * Internal menu flag -- menu item is the root of the menu tree.
 */
121
const MENU_IS_ROOT = 0x0001;
122
123
124
125

/**
 * Internal menu flag -- menu item is visible in the menu tree.
 */
126
const MENU_VISIBLE_IN_TREE = 0x0002;
127
128
129
130

/**
 * Internal menu flag -- menu item is visible in the breadcrumb.
 */
131
const MENU_VISIBLE_IN_BREADCRUMB = 0x0004;
132
133

/**
134
 * Internal menu flag -- menu item links back to its parent.
135
 */
136
const MENU_LINKS_TO_PARENT = 0x0008;
137
138
139
140

/**
 * Internal menu flag -- menu item can be modified by administrator.
 */
141
const MENU_MODIFIED_BY_ADMIN = 0x0020;
142
143
144
145

/**
 * Internal menu flag -- menu item was created by administrator.
 */
146
const MENU_CREATED_BY_ADMIN = 0x0040;
147
148
149
150

/**
 * Internal menu flag -- menu item is a local task.
 */
151
const MENU_IS_LOCAL_TASK = 0x0080;
Dries's avatar
   
Dries committed
152

Dries's avatar
   
Dries committed
153
/**
154
 * @} End of "defgroup menu_flags".
Dries's avatar
   
Dries committed
155
156
157
 */

/**
158
 * @defgroup menu_item_types Menu item types
Dries's avatar
   
Dries committed
159
 * @{
160
 * Definitions for various menu item types.
161
 *
Dries's avatar
   
Dries committed
162
 * Menu item definitions provide one of these constants, which are shortcuts for
163
 * combinations of @link menu_flags Menu flags @endlink.
Dries's avatar
   
Dries committed
164
 */
Dries's avatar
   
Dries committed
165

Dries's avatar
   
Dries committed
166
/**
167
 * Menu type -- A "normal" menu item that's shown in menus.
168
 *
Dries's avatar
   
Dries committed
169
 * Normal menu items show up in the menu tree and can be moved/hidden by
Dries's avatar
   
Dries committed
170
171
 * the administrator. Use this for most menu items. It is the default value if
 * no menu item type is specified.
Dries's avatar
   
Dries committed
172
 */
173
define('MENU_NORMAL_ITEM', MENU_VISIBLE_IN_TREE | MENU_VISIBLE_IN_BREADCRUMB);
174

Dries's avatar
   
Dries committed
175
/**
176
177
 * Menu type -- A hidden, internal callback, typically used for API calls.
 *
Dries's avatar
   
Dries committed
178
 * Callbacks simply register a path so that the correct function is fired
179
 * when the URL is accessed. They do not appear in menus.
Dries's avatar
   
Dries committed
180
 */
181
const MENU_CALLBACK = 0x0000;
Dries's avatar
 
Dries committed
182

Dries's avatar
   
Dries committed
183
/**
184
185
 * Menu type -- A normal menu item, hidden until enabled by an administrator.
 *
Dries's avatar
   
Dries committed
186
187
 * Modules may "suggest" menu items that the administrator may enable. They act
 * just as callbacks do until enabled, at which time they act like normal items.
188
189
 * Note for the value: 0x0010 was a flag which is no longer used, but this way
 * the values of MENU_CALLBACK and MENU_SUGGESTED_ITEM are separate.
Dries's avatar
   
Dries committed
190
 */
191
define('MENU_SUGGESTED_ITEM', MENU_VISIBLE_IN_BREADCRUMB | 0x0010);
Dries's avatar
   
Dries committed
192
193

/**
194
195
196
197
198
 * Menu type -- A task specific to the parent item, usually rendered as a tab.
 *
 * Local tasks are menu items that describe actions to be performed on their
 * parent item. An example is the path "node/52/edit", which performs the
 * "edit" task on "node/52".
Dries's avatar
   
Dries committed
199
 */
200
define('MENU_LOCAL_TASK', MENU_IS_LOCAL_TASK | MENU_VISIBLE_IN_BREADCRUMB);
Dries's avatar
   
Dries committed
201

Dries's avatar
   
Dries committed
202
/**
203
204
 * Menu type -- The "default" local task, which is initially active.
 *
Dries's avatar
   
Dries committed
205
206
207
 * Every set of local tasks should provide one "default" task, that links to the
 * same path as its parent when clicked.
 */
208
define('MENU_DEFAULT_LOCAL_TASK', MENU_IS_LOCAL_TASK | MENU_LINKS_TO_PARENT | MENU_VISIBLE_IN_BREADCRUMB);
Dries's avatar
   
Dries committed
209

210
211
212
/**
 * Menu type -- A task specific to the parent, which is never rendered.
 *
213
 * Sibling local tasks are not rendered themselves, but affect the active
214
215
 * trail and need their sibling tasks rendered as tabs.
 */
216
define('MENU_SIBLING_LOCAL_TASK', MENU_IS_LOCAL_TASK | MENU_VISIBLE_IN_BREADCRUMB);
217

Dries's avatar
   
Dries committed
218
/**
219
 * @} End of "defgroup menu_item_types".
Dries's avatar
   
Dries committed
220
221
 */

222
/**
223
 * @defgroup menu_context_types Menu context types
224
225
226
227
 * @{
 * Flags for use in the "context" attribute of menu router items.
 */

228
229
230
231
232
233
/**
 * Internal menu flag: Invisible local task.
 *
 * This flag may be used for local tasks like "Delete", so custom modules and
 * themes can alter the default context and expose the task by altering menu.
 */
234
const MENU_CONTEXT_NONE = 0x0000;
235

236
237
238
/**
 * Internal menu flag: Local task should be displayed in page context.
 */
239
const MENU_CONTEXT_PAGE = 0x0001;
240
241

/**
242
 * @} End of "defgroup menu_context_types".
243
244
 */

Dries's avatar
   
Dries committed
245
/**
246
 * @defgroup menu_status_codes Menu status codes
Dries's avatar
   
Dries committed
247
 * @{
Dries's avatar
   
Dries committed
248
249
 * Status codes for menu callbacks.
 */
Dries's avatar
   
Dries committed
250

251
252
253
/**
 * Internal menu status code -- Menu item was not found.
 */
254
const MENU_NOT_FOUND = 404;
255
256
257
258

/**
 * Internal menu status code -- Menu item access is denied.
 */
259
const MENU_ACCESS_DENIED = 403;
260
261
262
263

/**
 * Internal menu status code -- Menu item inaccessible because site is offline.
 */
264
const MENU_SITE_OFFLINE = 4;
Dries's avatar
   
Dries committed
265

266
267
268
/**
 * Internal menu status code -- Everything is working fine.
 */
269
const MENU_SITE_ONLINE = 5;
270

Dries's avatar
   
Dries committed
271
/**
272
 * @} End of "defgroup menu_status_codes".
Dries's avatar
   
Dries committed
273
 */
274

275
/**
276
 * @defgroup menu_tree_parameters Menu tree parameters
277
 * @{
278
 * Parameters for a menu tree.
279
280
 */

281
282
 /**
 * The maximum number of path elements for a menu callback
283
 */
284
const MENU_MAX_PARTS = 9;
285

286
287

/**
288
 * The maximum depth of a menu links tree - matches the number of p columns.
289
290
291
 *
 * @todo Move this constant to MenuLinkStorageController along with all the tree
 * functionality.
292
 */
293
const MENU_MAX_DEPTH = 9;
294

295
296

/**
297
 * @} End of "defgroup menu_tree_parameters".
298
299
 */

300
301
302
303
304
305
306
307
308
309
310
311
312
313
/**
 * Reserved key to identify the most specific menu link for a given path.
 *
 * The value of this constant is a hash of the constant name. We use the hash
 * so that the reserved key is over 32 characters in length and will not
 * collide with allowed menu names:
 * @code
 * sha1('MENU_PREFERRED_LINK') = 1cf698d64d1aa4b83907cf6ed55db3a7f8e92c91
 * @endcode
 *
 * @see menu_link_get_preferred()
 */
const MENU_PREFERRED_LINK = '1cf698d64d1aa4b83907cf6ed55db3a7f8e92c91';

314
/**
315
316
317
 * Returns the ancestors (and relevant placeholders) for any given path.
 *
 * For example, the ancestors of node/12345/edit are:
318
319
320
321
322
323
324
 * - node/12345/edit
 * - node/12345/%
 * - node/%/edit
 * - node/%/%
 * - node/12345
 * - node/%
 * - node
325
326
327
328
329
 *
 * To generate these, we will use binary numbers. Each bit represents a
 * part of the path. If the bit is 1, then it represents the original
 * value while 0 means wildcard. If the path is node/12/edit/foo
 * then the 1011 bitstring represents node/%/edit/foo where % means that
330
 * any argument matches that part. We limit ourselves to using binary
331
 * numbers that correspond the patterns of wildcards of router items that
332
 * actually exists. This list of 'masks' is built in menu_router_rebuild().
333
334
335
336
 *
 * @param $parts
 *   An array of path parts, for the above example
 *   array('node', '12345', 'edit').
337
 *
338
339
 * @return
 *   An array which contains the ancestors and placeholders. Placeholders
340
 *   simply contain as many '%s' as the ancestors.
341
342
 */
function menu_get_ancestors($parts) {
343
  $number_parts = count($parts);
344
  $ancestors = array();
345
346
  $length =  $number_parts - 1;
  $end = (1 << $number_parts) - 1;
347
  $masks = \Drupal::state()->get('menu.masks');
348
  // If the optimized menu.masks array is not available use brute force to get
349
  // the correct $ancestors and $placeholders returned. Do not use this as the
350
  // default value of the menu.masks variable to avoid building such a big
351
352
353
354
  // array.
  if (!$masks) {
    $masks = range(511, 1);
  }
355
356
357
358
359
360
361
362
363
364
  // Only examine patterns that actually exist as router items (the masks).
  foreach ($masks as $i) {
    if ($i > $end) {
      // Only look at masks that are not longer than the path of interest.
      continue;
    }
    elseif ($i < (1 << $length)) {
      // We have exhausted the masks of a given length, so decrease the length.
      --$length;
    }
365
366
    $current = '';
    for ($j = $length; $j >= 0; $j--) {
367
      // Check the bit on the $j offset.
368
      if ($i & (1 << $j)) {
369
        // Bit one means the original value.
370
371
372
        $current .= $parts[$length - $j];
      }
      else {
373
        // Bit zero means means wildcard.
374
375
        $current .= '%';
      }
376
      // Unless we are at offset 0, add a slash.
377
378
379
      if ($j) {
        $current .= '/';
      }
Dries's avatar
   
Dries committed
380
    }
381
    $ancestors[] = $current;
382
  }
383
  return $ancestors;
Dries's avatar
   
Dries committed
384
385
386
}

/**
387
 * Unserializes menu data, using a map to replace path elements.
Dries's avatar
   
Dries committed
388
 *
389
390
391
392
393
394
 * The menu system stores various path-related information (such as the 'page
 * arguments' and 'access arguments' components of a menu item) in the database
 * using serialized arrays, where integer values in the arrays represent
 * arguments to be replaced by values from the path. This function first
 * unserializes such menu information arrays, and then does the path
 * replacement.
395
 *
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
 * The path replacement acts on each integer-valued element of the unserialized
 * menu data array ($data) using a map array ($map, which is typically an array
 * of path arguments) as a list of replacements. For instance, if there is an
 * element of $data whose value is the number 2, then it is replaced in $data
 * with $map[2]; non-integer values in $data are left alone.
 *
 * As an example, an unserialized $data array with elements ('node_load', 1)
 * represents instructions for calling the node_load() function. Specifically,
 * this instruction says to use the path component at index 1 as the input
 * parameter to node_load(). If the path is 'node/123', then $map will be the
 * array ('node', 123), and the returned array from this function will have
 * elements ('node_load', 123), since $map[1] is 123. This return value will
 * indicate specifically that node_load(123) is to be called to load the node
 * whose ID is 123 for this menu item.
 *
 * @param $data
 *   A serialized array of menu data, as read from the database.
 * @param $map
 *   A path argument array, used to replace integer values in $data; an integer
 *   value N in $data will be replaced by value $map[N]. Typically, the $map
 *   array is generated from a call to the arg() function.
417
 *
418
 * @return
419
 *   The unserialized $data array, with path arguments replaced.
420
 */
421
422
423
424
425
426
427
428
function menu_unserialize($data, $map) {
  if ($data = unserialize($data)) {
    foreach ($data as $k => $v) {
      if (is_int($v)) {
        $data[$k] = isset($map[$v]) ? $map[$v] : '';
      }
    }
    return $data;
429
  }
430
431
  else {
    return array();
432
433
434
  }
}

435
/**
436
 * Loads objects into the map as defined in the $item['load_functions'].
437
 *
438
 * @param $item
439
 *   A menu router or menu link item
440
 * @param $map
441
 *   An array of path arguments; for example, array('node', '5').
442
 *
443
 * @return
444
445
 *   Returns TRUE for success, FALSE if an object cannot be loaded.
 *   Names of object loading functions are placed in $item['load_functions'].
446
 *   Loaded objects are placed in $map[]; keys are the same as keys in the
447
448
 *   $item['load_functions'] array.
 *   $item['access'] is set to FALSE if an object cannot be loaded.
449
 */
450
451
452
function _menu_load_objects(&$item, &$map) {
  if ($load_functions = $item['load_functions']) {
    // If someone calls this function twice, then unserialize will fail.
453
454
    if (!is_array($load_functions)) {
      $load_functions = unserialize($load_functions);
455
    }
456
457
458
    $path_map = $map;
    foreach ($load_functions as $index => $function) {
      if ($function) {
459
460
461
462
463
464
465
        $value = isset($path_map[$index]) ? $path_map[$index] : '';
        if (is_array($function)) {
          // Set up arguments for the load function. These were pulled from
          // 'load arguments' in the hook_menu() entry, but they need
          // some processing. In this case the $function is the key to the
          // load_function array, and the value is the list of arguments.
          list($function, $args) = each($function);
466
          $load_functions[$index] = $function;
467
468
469

          // Some arguments are placeholders for dynamic items to process.
          foreach ($args as $i => $arg) {
470
            if ($arg === '%index') {
471
              // Pass on argument index to the load function, so multiple
472
              // occurrences of the same placeholder can be identified.
473
474
              $args[$i] = $index;
            }
475
            if ($arg === '%map') {
476
477
478
479
480
              // Pass on menu map by reference. The accepting function must
              // also declare this as a reference if it wants to modify
              // the map.
              $args[$i] = &$map;
            }
481
482
483
            if (is_int($arg)) {
              $args[$i] = isset($path_map[$arg]) ? $path_map[$arg] : '';
            }
484
485
486
487
488
489
490
          }
          array_unshift($args, $value);
          $return = call_user_func_array($function, $args);
        }
        else {
          $return = $function($value);
        }
491
        // If callback returned an error or there is no callback, trigger 404.
492
        if (empty($return)) {
493
          $item['access'] = FALSE;
494
          $map = FALSE;
495
          return FALSE;
496
497
498
499
        }
        $map[$index] = $return;
      }
    }
500
    $item['load_functions'] = $load_functions;
501
  }
502
503
504
505
  return TRUE;
}

/**
506
 * Checks access to a menu item using the access callback.
507
508
 *
 * @param $item
509
 *   A menu router or menu link item
510
 * @param $map
511
 *   An array of path arguments; for example, array('node', '5').
512
 *
513
 * @return
514
 *   $item['access'] becomes TRUE if the item is accessible, FALSE otherwise.
515
516
 */
function _menu_check_access(&$item, $map) {
517
518
  // Determine access callback, which will decide whether or not the current
  // user has access to this path.
519
  $callback = empty($item['access_callback']) ? 0 : trim($item['access_callback']);
520
  // Check for a TRUE or FALSE value.
521
  if (is_numeric($callback)) {
522
    $item['access'] = (bool) $callback;
Dries's avatar
   
Dries committed
523
  }
524
  else {
525
    $arguments = menu_unserialize($item['access_arguments'], $map);
526
527
528
    // As call_user_func_array is quite slow and user_access is a very common
    // callback, it is worth making a special case for it.
    if ($callback == 'user_access') {
529
      $item['access'] = (count($arguments) == 1) ? user_access($arguments[0]) : user_access($arguments[0], $arguments[1]);
530
    }
531
    else {
532
      $item['access'] = call_user_func_array($callback, $arguments);
533
    }
Dries's avatar
   
Dries committed
534
  }
535
}
536

537
/**
538
 * Localizes the router item title using t() or another callback.
539
 *
540
541
542
543
544
545
546
547
548
549
550
551
552
 * Translate the title and description to allow storage of English title
 * strings in the database, yet display of them in the language required
 * by the current user.
 *
 * @param $item
 *   A menu router item or a menu link item.
 * @param $map
 *   The path as an array with objects already replaced. E.g., for path
 *   node/123 $map would be array('node', $node) where $node is the node
 *   object for node 123.
 * @param $link_translate
 *   TRUE if we are translating a menu link item; FALSE if we are
 *   translating a menu router item.
553
 *
554
555
556
 * @return
 *   No return value.
 *   $item['title'] is localized according to $item['title_callback'].
557
 *   If an item's callback is check_plain(), $item['options']['html'] becomes
558
 *   TRUE.
559
560
 *   $item['description'] is computed using $item['description_callback'] if
 *   specified; otherwise it is translated using t().
561
 *   When doing link translation and the $item['options']['attributes']['title']
562
 *   (link title attribute) matches the description, it is translated as well.
563
564
 */
function _menu_item_localize(&$item, $map, $link_translate = FALSE) {
565
566
567
568
  // Allow default menu links to be translated.
  // @todo Figure out a proper way to support translations of menu links, see
  //   https://drupal.org/node/2193777.
  $title_callback = $item instanceof MenuLinkInterface && !$item->customized ? 't' :  $item['title_callback'];
569
  $item['localized_options'] = $item['options'];
570
571
572
573
574
575
576
577
  // All 'class' attributes are assumed to be an array during rendering, but
  // links stored in the database may use an old string value.
  // @todo In order to remove this code we need to implement a database update
  //   including unserializing all existing link options and running this code
  //   on them, as well as adding validation to menu_link_save().
  if (isset($item['options']['attributes']['class']) && is_string($item['options']['attributes']['class'])) {
    $item['localized_options']['attributes']['class'] = explode(' ', $item['options']['attributes']['class']);
  }
578
579
580
581
582
583
  // If we are translating the title of a menu link, and its title is the same
  // as the corresponding router item, then we can use the title information
  // from the router. If it's customized, then we need to use the link title
  // itself; can't localize.
  // If we are translating a router item (tabs, page, breadcrumb), then we
  // can always use the information from the router item.
584
  if (!$link_translate || !isset($item['link_title']) || ($item['title'] == $item['link_title'])) {
585
586
    // t() is a special case. Since it is used very close to all the time,
    // we handle it directly instead of using indirect, slower methods.
587
588
    // @todo Recheck this line once https://drupal.org/node/2084421 is in.
    $item['title'] = isset($item['link_title']) ? $item['link_title'] : $item['title'];
589
    if ($title_callback == 't') {
590
591
592
593
594
595
      if (empty($item['title_arguments'])) {
        $item['title'] = t($item['title']);
      }
      else {
        $item['title'] = t($item['title'], menu_unserialize($item['title_arguments'], $map));
      }
596
    }
597
    elseif ($title_callback) {
598
      if (empty($item['title_arguments'])) {
599
        $item['title'] = $title_callback($item['title']);
600
601
      }
      else {
602
        $item['title'] = call_user_func_array($title_callback, menu_unserialize($item['title_arguments'], $map));
603
      }
604
      // Avoid calling check_plain again on l() function.
605
      if ($title_callback == 'check_plain') {
606
        $item['localized_options']['html'] = TRUE;
607
      }
608
609
    }
  }
610
611
  elseif ($link_translate) {
    $item['title'] = $item['link_title'];
612
613
614
  }

  // Translate description, see the motivation above.
615
  if (!empty($item['description'])) {
616
    $original_description = $item['description'];
617
618
619
620
621
622
623
624
625
626
627
  }
  if (!empty($item['description_arguments']) || !empty($item['description'])) {
    $description_callback = $item['description_callback'];
    // If the description callback is t(), call it directly.
    if ($description_callback == 't') {
      if (empty($item['description_arguments'])) {
        $item['description'] = t($item['description']);
      }
      else {
        $item['description'] = t($item['description'], menu_unserialize($item['description_arguments'], $map));
      }
628
    }
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
    elseif ($description_callback) {
      // If there are no arguments, call the description callback directly.
      if (empty($item['description_arguments'])) {
        $item['description'] = $description_callback($item['description']);
      }
      // Otherwise, use call_user_func_array() to pass the arguments.
      else {
        $item['description'] = call_user_func_array($description_callback, menu_unserialize($item['description_arguments'], $map));
      }
    }
  }
  // If the title and description are the same, use the translated description
  // as a localized title.
  if ($link_translate && isset($original_description) && isset($item['options']['attributes']['title']) && $item['options']['attributes']['title'] == $original_description) {
    $item['localized_options']['attributes']['title'] = $item['description'];
644
  }
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
}

/**
 * Handles dynamic path translation and menu access control.
 *
 * When a user arrives on a page such as node/5, this function determines
 * what "5" corresponds to, by inspecting the page's menu path definition,
 * node/%node. This will call node_load(5) to load the corresponding node
 * object.
 *
 * It also works in reverse, to allow the display of tabs and menu items which
 * contain these dynamic arguments, translating node/%node to node/5.
 *
 * Translation of menu item titles and descriptions are done here to
 * allow for storage of English strings in the database, and translation
660
 * to the language required to generate the current page.
661
 *
662
663
 * @param $router_item
 *   A menu router item
664
 * @param $map
665
 *   An array of path arguments; for example, array('node', '5').
666
 * @param $to_arg
667
 *   Execute $item['to_arg_functions'] or not. Use only if you want to render a
668
 *   path from the menu table, for example tabs.
669
 *
670
671
 * @return
 *   Returns the map with objects loaded as defined in the
672
 *   $item['load_functions']. $item['access'] becomes TRUE if the item is
673
 *   accessible, FALSE otherwise. $item['href'] is set according to the map.
674
 *   If an error occurs during calling the load_functions (like trying to load
675
 *   a non-existent node) then this function returns FALSE.
676
 */
677
function _menu_translate(&$router_item, $map, $to_arg = FALSE) {
678
  if ($to_arg && !empty($router_item['to_arg_functions'])) {
679
680
681
682
683
    // Fill in missing path elements, such as the current uid.
    _menu_link_map_translate($map, $router_item['to_arg_functions']);
  }
  // The $path_map saves the pieces of the path as strings, while elements in
  // $map may be replaced with loaded objects.
684
  $path_map = $map;
685
  if (!empty($router_item['load_functions']) && !_menu_load_objects($router_item, $map)) {
686
    // An error occurred loading an object.
687
    $router_item['access'] = FALSE;
688
689
    return FALSE;
  }
690
691
692
693
  // Avoid notices until we remove this function.
  // @see https://drupal.org/node/2107533
  $tab_root_map = array();
  $tab_parent_map = array();
694
  // Generate the link path for the page request or local tasks.
695
  $link_map = explode('/', $router_item['path']);
696
697
698
699
700
701
  if (isset($router_item['tab_root'])) {
    $tab_root_map = explode('/', $router_item['tab_root']);
  }
  if (isset($router_item['tab_parent'])) {
    $tab_parent_map = explode('/', $router_item['tab_parent']);
  }
702
  for ($i = 0; $i < $router_item['number_parts']; $i++) {
703
704
705
    if ($link_map[$i] == '%') {
      $link_map[$i] = $path_map[$i];
    }
706
707
708
709
710
711
    if (isset($tab_root_map[$i]) && $tab_root_map[$i] == '%') {
      $tab_root_map[$i] = $path_map[$i];
    }
    if (isset($tab_parent_map[$i]) && $tab_parent_map[$i] == '%') {
      $tab_parent_map[$i] = $path_map[$i];
    }
712
  }
713
  $router_item['href'] = implode('/', $link_map);
714
715
  $router_item['tab_root_href'] = implode('/', $tab_root_map);
  $router_item['tab_parent_href'] = implode('/', $tab_parent_map);
716
  $router_item['options'] = array();
717
  if (!empty($router_item['route_name'])) {
718
719
720
721
    // Route-provided menu items do not have menu loaders, so replace the map
    // with the link map.
    $map = $link_map;

722
    $route_provider = \Drupal::getContainer()->get('router.route_provider');
723
    $route = $route_provider->getRouteByName($router_item['route_name']);
724
    $router_item['access'] = menu_item_route_access($route, $router_item['href'], $map);
725
726
727
728
729
  }
  else {
    // @todo: Remove once all routes are converted.
    _menu_check_access($router_item, $map);
  }
730

731
732
733
734
  // For performance, don't localize an item the user can't access.
  if ($router_item['access']) {
    _menu_item_localize($router_item, $map);
  }
735
736
737
738
739

  return $map;
}

/**
740
 * Translates the path elements in the map using any to_arg helper function.
741
 *
742
 * @param $map
743
 *   An array of path arguments; for example, array('node', '5').
744
 * @param $to_arg_functions
745
 *   An array of helper functions; for example, array(2 => 'menu_tail_to_arg').
746
747
 *
 * @see hook_menu()
748
749
 */
function _menu_link_map_translate(&$map, $to_arg_functions) {
750
751
752
753
754
755
756
757
758
  $to_arg_functions = unserialize($to_arg_functions);
  foreach ($to_arg_functions as $index => $function) {
    // Translate place-holders into real values.
    $arg = $function(!empty($map[$index]) ? $map[$index] : '', $map, $index);
    if (!empty($map[$index]) || isset($arg)) {
      $map[$index] = $arg;
    }
    else {
      unset($map[$index]);
759
760
761
762
    }
  }
}

763
/**
764
 * Returns a string containing the path relative to the current index.
765
 */
766
767
768
769
function menu_tail_to_arg($arg, $map, $index) {
  return implode('/', array_slice($map, $index));
}

770
/**
771
 * Loads the path as one string relative to the current index.
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
 *
 * To use this load function, you must specify the load arguments
 * in the router item as:
 * @code
 * $item['load arguments'] = array('%map', '%index');
 * @endcode
 *
 * @see search_menu().
 */
function menu_tail_load($arg, &$map, $index) {
  $arg = implode('/', array_slice($map, $index));
  $map = array_slice($map, 0, $index);
  return $arg;
}

787
/**
788
 * Provides menu link unserializing, access control, and argument handling.
789
790
791
 *
 * This function is similar to _menu_translate(), but it also does
 * link-specific preparation (such as always calling to_arg() functions).
792
 *
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
 * @param array $item
 *   The passed in item has the following keys:
 *   - access: (optional) Becomes TRUE if the item is accessible, FALSE
 *     otherwise. If the key is not set, the access manager is used to
 *     determine the access.
 *   - options: (required) Is unserialized and copied to $item['localized_options'].
 *   - link_title: (required) The title of the menu link.
 *   - route_name: (required) The route name of the menu link.
 *   - route_parameters: (required) The unserialized route parameters of the menu link.
 *   The passed in item is changed by the following keys:
 *   - href: The actual path to the link. This path is generated from the
 *     link_path of the menu link entity.
 *   - title: The title of the link. This title is generated from the
 *     link_title of the menu link entity.
 */
function _menu_link_translate(&$item) {
809
  if (!is_array($item['options'])) {
810
    $item['options'] = (array) unserialize($item['options']);
811
  }
812
813
814
  $item['localized_options'] = $item['options'];
  $item['title'] = $item['link_title'];
  if ($item['external'] || empty($item['route_name'])) {
815
816
    $item['access'] = 1;
    $item['href'] = $item['link_path'];
817
818
819
    $item['route_parameters'] = array();
    // Set to NULL so that drupal_pre_render_link() is certain to skip it.
    $item['route_name'] = NULL;
820
821
  }
  else {
822
823
824
    $item['href'] = NULL;
    if (!is_array($item['route_parameters'])) {
      $item['route_parameters'] = (array) unserialize($item['route_parameters']);
825
    }
826
    // menu_tree_check_access() may set this ahead of time for links to nodes.
827
    if (!isset($item['access'])) {
828
      $item['access'] = \Drupal::getContainer()->get('access_manager')->checkNamedRoute($item['route_name'], $item['route_parameters'], \Drupal::currentUser());
829
    }
830
831
    // For performance, don't localize a link the user can't access.
    if ($item['access']) {
832
      _menu_item_localize($item, array(), TRUE);
833
    }
834
  }
835

836
837
838
839
  // Allow other customizations - e.g. adding a page-specific query string to the
  // options array. For performance reasons we only invoke this hook if the link
  // has the 'alter' flag set in the options array.
  if (!empty($item['options']['alter'])) {
840
    \Drupal::moduleHandler()->alter('translated_menu_link', $item, $map);
841
  }
Dries's avatar
   
Dries committed
842
843
}

844
845
846
847
848
849
/**
 * Checks access to a menu item by mocking a request for a path.
 *
 * @param \Symfony\Component\Routing\Route $route
 *   Router for the given menu item.
 * @param string $href
850
 *   The menu path with '%' replaced by arguments.
851
 * @param array $map
852
 *   An array of path arguments; for example, array('node', '5').
853
854
 * @param \Symfony\Component\HttpFoundation\Request $request
 *   The current request object, used to find the current route.
855
856
857
858
 *
 * @return bool
 *   TRUE if the user has access or FALSE if the user should be presented
 *   with access denied.
859
 *
860
 */
861
862
863
864
865
function menu_item_route_access(Route $route, $href, &$map, Request $request = NULL) {
  if (!isset($request)) {
    $request = RequestHelper::duplicate(\Drupal::request(), '/' . $href);
    $request->attributes->set('_system_path', $href);
  }
866
867
868
  // Attempt to match this path to provide a fully built request to the
  // access checker.
  try {
869
    $request->attributes->add(\Drupal::service('router')->matchRequest($request));
870
  }
871
  catch (ParamNotConvertedException $e) {
872
873
    return FALSE;
  }
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889

  // Populate the map with any matching values from the request.
  $path_bits = explode('/', trim($route->getPath(), '/'));
  foreach ($map as $index => $map_item) {
    $matches = array();
    // Search for placeholders wrapped by curly braces. For example, a path
    // 'foo/{bar}/baz' would return 'bar'.
    if (isset($path_bits[$index]) && preg_match('/{(?<placeholder>.*)}/', $path_bits[$index], $matches)) {
      // If that placeholder is present on the request attributes, replace the
      // placeholder in the map with the value.
      if ($request->attributes->has($matches['placeholder'])) {
        $map[$index] = $request->attributes->get($matches['placeholder']);
      }
    }
  }

890
  return \Drupal::service('access_manager')->check($route, $request, \Drupal::currentUser());
891
892
}

893
/**
894
 * Renders a menu tree based on the current path.
895
896
 *
 * The tree is expanded based on the current path and dynamic paths are also
897
 * changed according to the defined to_arg functions (for example the 'My
898
 * account' link is changed from user/% to a link with the current user's uid).
899
900
901
 *
 * @param $menu_name
 *   The name of the menu.
902
 *
903
 * @return
904
905
 *   A structured array representing the specified menu on the current page, to
 *   be rendered by drupal_render().
906
 */
907
function menu_tree($menu_name) {
908
  $menu_output = &drupal_static(__FUNCTION__, array());
909
910

  if (!isset($menu_output[$menu_name])) {
911
    $tree = menu_tree_page_data($menu_name);
912
913
914
915
916
    $menu_output[$menu_name] = menu_tree_output($tree);
  }
  return $menu_output[$menu_name];
}

Dries's avatar
   
Dries committed
917
/**
918
 * Returns a rendered menu tree.
919
 *
920
921
922
923
924
 * The menu item's LI element is given one of the following classes:
 * - expanded: The menu item is showing its submenu.
 * - collapsed: The menu item has a submenu which is not shown.
 * - leaf: The menu item has no submenu.
 *
925
926
 * @param $tree
 *   A data structure representing the tree as returned from menu_tree_data.
927
 *
928
 * @return
929
 *   A structured array to be rendered by drupal_render().
Dries's avatar
   
Dries committed
930
 */
931
function menu_tree_output($tree) {
932
  $build = array();
933
  $items = array();
934

935
  // Pull out just the menu links we are going to render so that we
936
  // get an accurate count for the first/last classes.
937
  foreach ($tree as $data) {
938
    if ($data['link']['access'] && !$data['link']['hidden']) {
939
940
941
      $items[] = $data;
    }
  }
942

943
  foreach ($items as $data) {
944
    $class = array();
945
946
947
948
    // Set a class for the <li>-tag. Since $data['below'] may contain local
    // tasks, only set 'expanded' class if the link also has children within
    // the current menu.
    if ($data['link']['has_children'] && $data['below']) {
949
950
951
952
      $class[] = 'expanded';
    }
    elseif ($data['link']['has_children']) {
      $class[] = 'collapsed';
953
954
    }
    else {
955
      $class[] = 'leaf';
956
    }
957
958
959
    // Set a class if the link is in the active trail.
    if ($data['link']['in_active_trail']) {
      $class[] = 'active-trail';
960
961
      $data['link']['localized_options']['attributes']['class'][] = 'active-trail';
    }
962

963
    // Allow menu-specific theme overrides.