path.inc 20.6 KB
Newer Older
1
<?php
2
3
4
5
6
7

/**
 * @file
 * Functions to handle paths in Drupal, including path aliasing.
 *
 * These functions are not loaded for cached pages, but modules that need
8
9
 * to use them in hook_boot() or hook exit() can make them available, by
 * executing "drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);".
10
11
12
 */

/**
13
 * Initializes the current path to the proper normal path.
14
 */
15
function drupal_path_initialize() {
16
17
18
19
20
21
22
23
24
  // At this point, the current path is either the request path (due to
  // drupal_environment_initialize()) or some modified version of it due to
  // other bootstrap code (e.g., language negotiation), but it has not yet been
  // normalized by drupal_get_normal_path().
  $path = _current_path();

  // If on the front page, resolve to the front page path, including for calls
  // to current_path() while drupal_get_normal_path() is in progress.
  if (empty($path)) {
25
    $path = config('system.site')->get('page.front');
26
    _current_path($path);
27
  }
28
29
30

  // Normalize the path.
  _current_path(drupal_get_normal_path($path));
31
32
33
34
}

/**
 * Given an alias, return its Drupal system URL if one exists. Given a Drupal
35
36
 * system URL return one of its aliases if such a one exists. Otherwise,
 * return FALSE.
37
38
39
40
41
42
43
44
 *
 * @param $action
 *   One of the following values:
 *   - wipe: delete the alias cache.
 *   - alias: return an alias for a given Drupal system path (if one exists).
 *   - source: return the Drupal system URL for a path alias (if one exists).
 * @param $path
 *   The path to investigate for corresponding aliases or system URLs.
45
 * @param $langcode
46
47
48
 *   Optional language code to search the path with. Defaults to the page language.
 *   If there's no path defined for that language it will search paths without
 *   language.
49
50
51
52
53
 *
 * @return
 *   Either a Drupal system path, an aliased path, or FALSE if no path was
 *   found.
 */
54
function drupal_lookup_path($action, $path = '', $langcode = NULL) {
55
  // Use the advanced drupal_static() pattern, since this is called very often.
56
57
58
59
60
  static $drupal_static_fast;
  if (!isset($drupal_static_fast)) {
    $drupal_static_fast['cache'] = &drupal_static(__FUNCTION__);
  }
  $cache = &$drupal_static_fast['cache'];
61
62
63
64
65
66
67
68
69
70
71

  if (!isset($cache)) {
    $cache = array(
      'map' => array(),
      'no_source' => array(),
      'whitelist' => NULL,
      'system_paths' => array(),
      'no_aliases' => array(),
      'first_call' => TRUE,
    );
  }
72

73
  // Retrieve the path alias whitelist.
74
75
76
77
  if (!isset($cache['whitelist'])) {
    $cache['whitelist'] = variable_get('path_alias_whitelist', NULL);
    if (!isset($cache['whitelist'])) {
      $cache['whitelist'] = drupal_path_alias_whitelist_rebuild();
78
    }
79
80
  }

81
82
83
84
  // If no language is explicitly specified we default to the current URL
  // language. If we used a language different from the one conveyed by the
  // requested URL, we might end up being unable to check if there is a path
  // alias matching the URL path.
85
  $langcode = $langcode ? $langcode : language_manager(LANGUAGE_TYPE_URL)->langcode;
86

87
  if ($action == 'wipe') {
88
89
    $cache = array();
    $cache['whitelist'] = drupal_path_alias_whitelist_rebuild();
90
  }
91
  elseif ($cache['whitelist'] && $path != '') {
92
    if ($action == 'alias') {
93
94
      // During the first call to drupal_lookup_path() per language, load the
      // expected system paths for the page from cache.
95
96
      if (!empty($cache['first_call'])) {
        $cache['first_call'] = FALSE;
97

98
        $cache['map'][$langcode] = array();
99
100
        // Load system paths from cache.
        $cid = current_path();
101
        if ($cached = cache('path')->get($cid)) {
102
          $cache['system_paths'] = $cached->data;
103
          // Now fetch the aliases corresponding to these system paths.
104
          $args = array(
105
            ':system' => $cache['system_paths'],
106
            ':langcode' => $langcode,
107
            ':langcode_undetermined' => LANGUAGE_NOT_SPECIFIED,
108
109
110
111
112
113
114
115
          );
          // Always get the language-specific alias before the language-neutral
          // one. For example 'de' is less than 'und' so the order needs to be
          // ASC, while 'xx-lolspeak' is more than 'und' so the order needs to
          // be DESC. We also order by pid ASC so that fetchAllKeyed() returns
          // the most recently created alias for each source. Subsequent queries
          // using fetchField() must use pid DESC to have the same effect.
          // For performance reasons, the query builder is not used here.
116
          if ($langcode == LANGUAGE_NOT_SPECIFIED) {
117
            // Prevent PDO from complaining about a token the query doesn't use.
118
            unset($args[':langcode']);
119
            $result = db_query('SELECT source, alias FROM {url_alias} WHERE source IN (:system) AND langcode = :langcode_undetermined ORDER BY pid ASC', $args);
120
          }
121
122
          elseif ($langcode < LANGUAGE_NOT_SPECIFIED) {
            $result = db_query('SELECT source, alias FROM {url_alias} WHERE source IN (:system) AND langcode IN (:langcode, :langcode_undetermined) ORDER BY langcode ASC, pid ASC', $args);
123
124
          }
          else {
125
            $result = db_query('SELECT source, alias FROM {url_alias} WHERE source IN (:system) AND langcode IN (:langcode, :langcode_undetermined) ORDER BY langcode DESC, pid ASC', $args);
126
          }
127
          $cache['map'][$langcode] = $result->fetchAllKeyed();
128
          // Keep a record of paths with no alias to avoid querying twice.
129
          $cache['no_aliases'][$langcode] = array_flip(array_diff_key($cache['system_paths'], array_keys($cache['map'][$langcode])));
130
131
132
        }
      }
      // If the alias has already been loaded, return it.
133
134
      if (isset($cache['map'][$langcode][$path])) {
        return $cache['map'][$langcode][$path];
135
      }
136
137
138
      // Check the path whitelist, if the top_level part before the first /
      // is not in the list, then there is no need to do anything further,
      // it is not in the database.
139
      elseif (!isset($cache['whitelist'][strtok($path, '/')])) {
140
141
        return FALSE;
      }
142
      // For system paths which were not cached, query aliases individually.
143
      elseif (!isset($cache['no_aliases'][$langcode][$path])) {
144
        $args = array(
145
          ':source' => $path,
146
          ':langcode' => $langcode,
147
          ':langcode_undetermined' => LANGUAGE_NOT_SPECIFIED,
148
149
        );
        // See the queries above.
150
        if ($langcode == LANGUAGE_NOT_SPECIFIED) {
151
          unset($args[':langcode']);
152
          $alias = db_query("SELECT alias FROM {url_alias} WHERE source = :source AND langcode = :langcode_undetermined ORDER BY pid DESC", $args)->fetchField();
153
        }
154
155
        elseif ($langcode > LANGUAGE_NOT_SPECIFIED) {
          $alias = db_query("SELECT alias FROM {url_alias} WHERE source = :source AND langcode IN (:langcode, :langcode_undetermined) ORDER BY langcode DESC, pid DESC", $args)->fetchField();
156
157
        }
        else {
158
          $alias = db_query("SELECT alias FROM {url_alias} WHERE source = :source AND langcode IN (:langcode, :langcode_undetermined) ORDER BY langcode ASC, pid DESC", $args)->fetchField();
159
        }
160
        $cache['map'][$langcode][$path] = $alias;
161
162
        return $alias;
      }
163
    }
164
    // Check $no_source for this $path in case we've already determined that there
165
    // isn't a path that has this alias
166
    elseif ($action == 'source' && !isset($cache['no_source'][$langcode][$path])) {
167
      // Look for the value $path within the cached $map
168
      $source = FALSE;
169
      if (!isset($cache['map'][$langcode]) || !($source = array_search($path, $cache['map'][$langcode]))) {
170
171
        $args = array(
          ':alias' => $path,
172
          ':langcode' => $langcode,
173
          ':langcode_undetermined' => LANGUAGE_NOT_SPECIFIED,
174
175
        );
        // See the queries above.
176
        if ($langcode == LANGUAGE_NOT_SPECIFIED) {
177
          unset($args[':langcode']);
178
          $result = db_query("SELECT source FROM {url_alias} WHERE alias = :alias AND langcode = :langcode_undetermined ORDER BY pid DESC", $args);
179
        }
180
181
        elseif ($langcode > LANGUAGE_NOT_SPECIFIED) {
          $result = db_query("SELECT source FROM {url_alias} WHERE alias = :alias AND langcode IN (:langcode, :langcode_undetermined) ORDER BY langcode DESC, pid DESC", $args);
182
183
        }
        else {
184
          $result = db_query("SELECT source FROM {url_alias} WHERE alias = :alias AND langcode IN (:langcode, :langcode_undetermined) ORDER BY langcode ASC, pid DESC", $args);
185
186
        }
        if ($source = $result->fetchField()) {
187
          $cache['map'][$langcode][$source] = $path;
188
189
190
191
        }
        else {
          // We can't record anything into $map because we do not have a valid
          // index and there is no need because we have not learned anything
192
          // about any Drupal path. Thus cache to $no_source.
193
          $cache['no_source'][$langcode][$path] = TRUE;
194
195
        }
      }
196
      return $source;
197
198
199
200
201
202
    }
  }

  return FALSE;
}

203
204
205
206
/**
 * Cache system paths for a page.
 *
 * Cache an array of the system paths available on each page. We assume
207
 * that aliases will be needed for the majority of these paths during
208
209
210
211
212
213
 * subsequent requests, and load them in a single query during
 * drupal_lookup_path().
 */
function drupal_cache_system_paths() {
  // Check if the system paths for this page were loaded from cache in this
  // request to avoid writing to cache on every request.
214
  $cache = &drupal_static('drupal_lookup_path', array());
215
  if (empty($cache['system_paths']) && !empty($cache['map'])) {
216
217
    // Generate a cache ID (cid) specifically for this page.
    $cid = current_path();
218
219
220
    // The static $map array used by drupal_lookup_path() includes all
    // system paths for the page request.
    if ($paths = current($cache['map'])) {
221
222
      $data = array_keys($paths);
      $expire = REQUEST_TIME + (60 * 60 * 24);
223
      cache('path')->set($cid, $data, $expire);
224
225
226
227
    }
  }
}

228
229
230
/**
 * Given an internal Drupal path, return the alias set by the administrator.
 *
231
232
233
 * If no path is provided, the function will return the alias of the current
 * page.
 *
234
235
 * @param $path
 *   An internal Drupal path.
236
 * @param $langcode
237
 *   An optional language code to look up the path in.
238
239
240
241
242
 *
 * @return
 *   An aliased path if one was found, or the original path if no alias was
 *   found.
 */
243
function drupal_get_path_alias($path = NULL, $langcode = NULL) {
244
245
  // If no path is specified, use the current page's path.
  if ($path == NULL) {
246
    $path = current_path();
247
  }
248
  $result = $path;
249
  if ($alias = drupal_lookup_path('alias', $path, $langcode)) {
250
251
252
253
254
255
256
257
258
259
    $result = $alias;
  }
  return $result;
}

/**
 * Given a path alias, return the internal path it represents.
 *
 * @param $path
 *   A Drupal path alias.
260
 * @param $langcode
261
 *   An optional language code to look up the path in.
262
263
264
265
266
 *
 * @return
 *   The internal path represented by the alias, or the original alias if no
 *   internal path was found.
 */
267
function drupal_get_normal_path($path, $langcode = NULL) {
268
269
270
  $original_path = $path;

  // Lookup the path alias first.
271
  if ($source = drupal_lookup_path('source', $path, $langcode)) {
272
    $path = $source;
273
  }
274
275
276
277
278
279

  // Allow other modules to alter the inbound URL. We cannot use drupal_alter()
  // here because we need to run hook_url_inbound_alter() in the reverse order
  // of hook_url_outbound_alter().
  foreach (array_reverse(module_implements('url_inbound_alter')) as $module) {
    $function = $module . '_url_inbound_alter';
280
    $function($path, $original_path, $langcode);
281
  }
282
283

  return $path;
284
285
}

286
287
288
289
290
291
292
/**
 * Check if the current page is the front page.
 *
 * @return
 *   Boolean value: TRUE if the current page is the front page; FALSE if otherwise.
 */
function drupal_is_front_page() {
293
  // Use the advanced drupal_static() pattern, since this is called very often.
294
295
296
297
298
  static $drupal_static_fast;
  if (!isset($drupal_static_fast)) {
    $drupal_static_fast['is_front_page'] = &drupal_static(__FUNCTION__);
  }
  $is_front_page = &$drupal_static_fast['is_front_page'];
299
300

  if (!isset($is_front_page)) {
301
    $is_front_page = (current_path() == config('system.site')->get('page.front'));
302
303
304
  }

  return $is_front_page;
305
}
306
307
308
309
310
311
312
313
314
315
316
317
318

/**
 * Check if a path matches any pattern in a set of patterns.
 *
 * @param $path
 *   The path to match.
 * @param $patterns
 *   String containing a set of patterns separated by \n, \r or \r\n.
 *
 * @return
 *   Boolean value: TRUE if the path matches a pattern, FALSE otherwise.
 */
function drupal_match_path($path, $patterns) {
319
  $regexps = &drupal_static(__FUNCTION__);
320

321
  if (!isset($regexps[$patterns])) {
322
323
324
325
326
327
328
329
330
331
    // Convert path settings to a regular expression.
    // Therefore replace newlines with a logical or, /* with asterisks and the <front> with the frontpage.
    $to_replace = array(
      '/(\r\n?|\n)/', // newlines
      '/\\\\\*/',     // asterisks
      '/(^|\|)\\\\<front\\\\>($|\|)/' // <front>
    );
    $replacements = array(
      '|',
      '.*',
332
      '\1' . preg_quote(config('system.site')->get('page.front'), '/') . '\2'
333
334
335
    );
    $patterns_quoted = preg_quote($patterns, '/');
    $regexps[$patterns] = '/^(' . preg_replace($to_replace, $replacements, $patterns_quoted) . ')$/';
336
  }
337
  return (bool)preg_match($regexps[$patterns], $path);
338
}
339
340
341
342
343
344
345
346
347
348
349

/**
 * Return the current URL path of the page being viewed.
 *
 * Examples:
 * - http://example.com/node/306 returns "node/306".
 * - http://example.com/drupalfolder/node/306 returns "node/306" while
 *   base_path() returns "/drupalfolder/".
 * - http://example.com/path/alias (which is a path alias for node/306) returns
 *   "node/306" as opposed to the path alias.
 *
350
 * This function is not available in hook_boot() so use request_path() instead.
351
 * However, be careful when doing that because in the case of Example #3
352
 * request_path() will contain "path/alias". If "node/306" is needed, calling
353
 * drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL) makes this function available.
354
355
356
 *
 * @return
 *   The current Drupal URL path.
357
358
 *
 * @see request_path()
359
360
 */
function current_path() {
361
362
363
  if (drupal_container()->has('request')) {
    return drupal_container()->get('request')->attributes->get('system_path');
  }
364
  return _current_path();
365
}
366
367
368
369

/**
 * Rebuild the path alias white list.
 *
370
371
372
 * @param $source
 *   An optional system path for which an alias is being inserted.
 *
373
374
375
 * @return
 *   An array containing a white list of path aliases.
 */
376
377
378
379
380
381
382
383
384
function drupal_path_alias_whitelist_rebuild($source = NULL) {
  // When paths are inserted, only rebuild the whitelist if the system path
  // has a top level component which is not already in the whitelist.
  if (!empty($source)) {
    $whitelist = variable_get('path_alias_whitelist', NULL);
    if (isset($whitelist[strtok($source, '/')])) {
      return $whitelist;
    }
  }
385
  // For each alias in the database, get the top level component of the system
386
387
  // path it corresponds to. This is the portion of the path before the first
  // '/', if present, otherwise the whole path itself.
388
  $whitelist = array();
389
  $result = db_query("SELECT DISTINCT SUBSTRING_INDEX(source, '/', 1) AS path FROM {url_alias}");
390
391
392
393
394
395
  foreach ($result as $row) {
    $whitelist[$row->path] = TRUE;
  }
  variable_set('path_alias_whitelist', $whitelist);
  return $whitelist;
}
396
397
398
399
400
401
402
403
404
405
406
407
408
409

/**
 * Fetch a specific URL alias from the database.
 *
 * @param $conditions
 *   A string representing the source, a number representing the pid, or an
 *   array of query conditions.
 *
 * @return
 *   FALSE if no alias was found or an associative array containing the
 *   following keys:
 *   - source: The internal system path.
 *   - alias: The URL alias.
 *   - pid: Unique path alias identifier.
410
 *   - langcode: The language code of the alias.
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
 */
function path_load($conditions) {
  if (is_numeric($conditions)) {
    $conditions = array('pid' => $conditions);
  }
  elseif (is_string($conditions)) {
    $conditions = array('source' => $conditions);
  }
  elseif (!is_array($conditions)) {
    return FALSE;
  }
  $select = db_select('url_alias');
  foreach ($conditions as $field => $value) {
    $select->condition($field, $value);
  }
  return $select
    ->fields('url_alias')
    ->execute()
    ->fetchAssoc();
}

/**
 * Save a path alias to the database.
 *
 * @param $path
 *   An associative array containing the following keys:
 *   - source: The internal system path.
 *   - alias: The URL alias.
 *   - pid: (optional) Unique path alias identifier.
440
 *   - langcode: (optional) The language code of the alias.
441
442
 */
function path_save(&$path) {
443
  $path += array('langcode' => LANGUAGE_NOT_SPECIFIED);
444

445
446
447
448
  // Load the stored alias, if any.
  if (!empty($path['pid']) && !isset($path['original'])) {
    $path['original'] = path_load($path['pid']);
  }
449

450
451
452
453
454
455
456
  if (empty($path['pid'])) {
    drupal_write_record('url_alias', $path);
    module_invoke_all('path_insert', $path);
  }
  else {
    drupal_write_record('url_alias', $path, array('pid'));
    module_invoke_all('path_update', $path);
457
  }
458
459
460
461
462
463

  // Clear internal properties.
  unset($path['original']);

  // Clear the static alias cache.
  drupal_clear_path_cache($path['source']);
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
}

/**
 * Delete a URL alias.
 *
 * @param $criteria
 *   A number representing the pid or an array of criteria.
 */
function path_delete($criteria) {
  if (!is_array($criteria)) {
    $criteria = array('pid' => $criteria);
  }
  $path = path_load($criteria);
  $query = db_delete('url_alias');
  foreach ($criteria as $field => $value) {
    $query->condition($field, $value);
  }
  $query->execute();
  module_invoke_all('path_delete', $path);
483
  drupal_clear_path_cache($path['source']);
484
485
}

486
487
488
489
490
491
492
493
494
495
/**
 * Determine whether a path is in the administrative section of the site.
 *
 * By default, paths are considered to be non-administrative. If a path does not
 * match any of the patterns in path_get_admin_paths(), or if it matches both
 * administrative and non-administrative patterns, it is considered
 * non-administrative.
 *
 * @param $path
 *   A Drupal path.
496
 *
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
 * @return
 *   TRUE if the path is administrative, FALSE otherwise.
 *
 * @see path_get_admin_paths()
 * @see hook_admin_paths()
 * @see hook_admin_paths_alter()
 */
function path_is_admin($path) {
  $path_map = &drupal_static(__FUNCTION__);
  if (!isset($path_map['admin'][$path])) {
    $patterns = path_get_admin_paths();
    $path_map['admin'][$path] = drupal_match_path($path, $patterns['admin']);
    $path_map['non_admin'][$path] = drupal_match_path($path, $patterns['non_admin']);
  }
  return $path_map['admin'][$path] && !$path_map['non_admin'][$path];
}

/**
 * Get a list of administrative and non-administrative paths.
 *
 * @return array
 *   An associative array containing the following keys:
 *   'admin': An array of administrative paths and regular expressions
 *            in a format suitable for drupal_match_path().
 *   'non_admin': An array of non-administrative paths and regular expressions.
 *
 * @see hook_admin_paths()
 * @see hook_admin_paths_alter()
 */
function path_get_admin_paths() {
  $patterns = &drupal_static(__FUNCTION__);
  if (!isset($patterns)) {
    $paths = module_invoke_all('admin_paths');
    drupal_alter('admin_paths', $paths);
    // Combine all admin paths into one array, and likewise for non-admin paths,
    // for easier handling.
    $patterns = array();
    $patterns['admin'] = array();
    $patterns['non_admin'] = array();
    foreach ($paths as $path => $enabled) {
      if ($enabled) {
        $patterns['admin'][] = $path;
      }
      else {
        $patterns['non_admin'][] = $path;
      }
    }
    $patterns['admin'] = implode("\n", $patterns['admin']);
    $patterns['non_admin'] = implode("\n", $patterns['non_admin']);
  }
  return $patterns;
}
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584

/**
 * Checks a path exists and the current user has access to it.
 *
 * @param $path
 *   The path to check.
 * @param $dynamic_allowed
 *   Whether paths with menu wildcards (like user/%) should be allowed.
 *
 * @return
 *   TRUE if it is a valid path AND the current user has access permission,
 *   FALSE otherwise.
 */
function drupal_valid_path($path, $dynamic_allowed = FALSE) {
  global $menu_admin;
  // We indicate that a menu administrator is running the menu access check.
  $menu_admin = TRUE;
  if ($path == '<front>' || url_is_external($path)) {
    $item = array('access' => TRUE);
  }
  elseif ($dynamic_allowed && preg_match('/\/\%/', $path)) {
    // Path is dynamic (ie 'user/%'), so check directly against menu_router table.
    if ($item = db_query("SELECT * FROM {menu_router} where path = :path", array(':path' => $path))->fetchAssoc()) {
      $item['link_path']  = $form_item['link_path'];
      $item['link_title'] = $form_item['link_title'];
      $item['external']   = FALSE;
      $item['options'] = '';
      _menu_link_translate($item);
    }
  }
  else {
    $item = menu_get_item($path);
  }
  $menu_admin = FALSE;
  return $item && $item['access'];
}
585
586
587
588
589
590
591
592
593
594
595
596

/**
 * Clear the path cache.
 *
 * @param $source
 *   An optional system path for which an alias is being changed.
 */
function drupal_clear_path_cache($source = NULL) {
  // Clear the drupal_lookup_path() static cache.
  drupal_static_reset('drupal_lookup_path');
  drupal_path_alias_whitelist_rebuild($source);
}