path.inc 18.4 KB
Newer Older
1 2
<?php
// $Id$
3 4 5 6 7 8

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

/**
 * Initialize the $_GET['q'] variable to the proper normal path.
 */
16
function drupal_path_initialize() {
17 18 19 20 21 22 23 24 25 26
  if (!empty($_GET['q'])) {
    $_GET['q'] = drupal_get_normal_path(trim($_GET['q'], '/'));
  }
  else {
    $_GET['q'] = drupal_get_normal_path(variable_get('site_frontpage', 'node'));
  }
}

/**
 * Given an alias, return its Drupal system URL if one exists. Given a Drupal
27 28
 * system URL return one of its aliases if such a one exists. Otherwise,
 * return FALSE.
29 30 31 32 33 34 35 36
 *
 * @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.
37 38 39 40
 * @param $path_language
 *   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.
41 42 43 44 45
 *
 * @return
 *   Either a Drupal system path, an aliased path, or FALSE if no path was
 *   found.
 */
46 47
function drupal_lookup_path($action, $path = '', $path_language = '') {
  global $language;
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
  // Use the advanced drupal_static() pattern, since this is called very often.
  static $drupal_static = array();
  isset($drupal_static[__FUNCTION__]) || ($drupal_static[__FUNCTION__] = &drupal_static(__FUNCTION__));
  $cache = &$drupal_static[__FUNCTION__];

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

64
  // Retrieve the path alias whitelist.
65 66 67 68
  if (!isset($cache['whitelist'])) {
    $cache['whitelist'] = variable_get('path_alias_whitelist', NULL);
    if (!isset($cache['whitelist'])) {
      $cache['whitelist'] = drupal_path_alias_whitelist_rebuild();
69
    }
70 71
  }

72 73
  $path_language = $path_language ? $path_language : $language->language;

74
  if ($action == 'wipe') {
75 76
    $cache = array();
    $cache['whitelist'] = drupal_path_alias_whitelist_rebuild();
77
  }
78
  elseif ($cache['whitelist'] && $path != '') {
79
    if ($action == 'alias') {
80 81
      // During the first call to drupal_lookup_path() per language, load the
      // expected system paths for the page from cache.
82 83
      if (!empty($cache['first_call'])) {
        $cache['first_call'] = FALSE;
84

85
        $cache['map'][$path_language] = array();
86 87
        // Load system paths from cache.
        $cid = current_path();
88 89
        if ($cached = cache_get($cid, 'cache_path')) {
          $cache['system_paths'] = $cached->data;
90 91 92
          // Now fetch the aliases corresponding to these system paths.
          // We order by ASC and overwrite array keys to ensure the correct
          // alias is used when there are multiple aliases per path.
93
          $cache['map'][$path_language] = db_query("SELECT source, alias FROM {url_alias} WHERE source IN (:system) AND language IN (:language, '') ORDER BY language ASC, pid ASC", array(
94
            ':system' => $cache['system_paths'],
95 96 97
            ':language' => $path_language
          ))->fetchAllKeyed();
          // Keep a record of paths with no alias to avoid querying twice.
98
          $cache['no_aliases'][$path_language] = array_flip(array_diff_key($cache['system_paths'], array_keys($cache['map'][$path_language])));
99 100 101
        }
      }
      // If the alias has already been loaded, return it.
102 103
      if (isset($cache['map'][$path_language][$path])) {
        return $cache['map'][$path_language][$path];
104
      }
105 106 107
      // 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.
108
      elseif (!isset($cache['whitelist'][strtok($path, '/')])) {
109 110
        return FALSE;
      }
111
      // For system paths which were not cached, query aliases individually.
112
      else if (!isset($cache['no_aliases'][$path_language][$path])) {
113
        // Get the most fitting result falling back with alias without language
114 115
        $alias = db_query("SELECT alias FROM {url_alias} WHERE source = :source AND language IN (:language, '') ORDER BY language DESC, pid DESC", array(
          ':source' => $path,
116 117
          ':language' => $path_language
        ))->fetchField();
118
        $cache['map'][$path_language][$path] = $alias;
119 120
        return $alias;
      }
121
    }
122
    // Check $no_source for this $path in case we've already determined that there
123
    // isn't a path that has this alias
124
    elseif ($action == 'source' && !isset($cache['no_source'][$path_language][$path])) {
125
      // Look for the value $path within the cached $map
126 127
      $source = '';
      if (!isset($cache['map'][$path_language]) || !($source = array_search($path, $cache['map'][$path_language]))) {
128
        // Get the most fitting result falling back with alias without language
129 130
        if ($source = db_query("SELECT source FROM {url_alias} WHERE alias = :alias AND language IN (:language, '') ORDER BY language DESC, pid DESC", array(
                     ':alias' => $path,
131
                     ':language' => $path_language))
132
            ->fetchField()) {
133
          $cache['map'][$path_language][$source] = $path;
134 135 136 137
        }
        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
138 139
          // about any Drupal path. Thus cache to $no_source.
          $cache['no_source'][$path_language][$path] = TRUE;
140 141
        }
      }
142
      return $source;
143 144 145 146 147 148
    }
  }

  return FALSE;
}

149 150 151 152 153 154 155 156 157 158 159
/**
 * Cache system paths for a page.
 *
 * Cache an array of the system paths available on each page. We assume
 * that aiases will be needed for the majority of these paths during
 * 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.
160 161
  $cache = &drupal_static('drupal_lookup_path', array());
  if (!$cache['system_paths']) {
162 163
    // Generate a cache ID (cid) specifically for this page.
    $cid = current_path();
164 165 166
    // The static $map array used by drupal_lookup_path() includes all
    // system paths for the page request.
    if ($paths = current($cache['map'])) {
167 168 169 170 171 172 173
      $data = array_keys($paths);
      $expire = REQUEST_TIME + (60 * 60 * 24);
      cache_set($cid, $data, 'cache_path', $expire);
    }
  }
}

174 175 176
/**
 * Given an internal Drupal path, return the alias set by the administrator.
 *
177 178 179
 * If no path is provided, the function will return the alias of the current
 * page.
 *
180 181
 * @param $path
 *   An internal Drupal path.
182 183
 * @param $path_language
 *   An optional language code to look up the path in.
184 185 186 187 188
 *
 * @return
 *   An aliased path if one was found, or the original path if no alias was
 *   found.
 */
189 190 191 192 193
function drupal_get_path_alias($path = NULL, $path_language = '') {
  // If no path is specified, use the current page's path.
  if ($path == NULL) {
    $path = $_GET['q'];
  }
194
  $result = $path;
195
  if ($alias = drupal_lookup_path('alias', $path, $path_language)) {
196 197 198 199 200 201 202 203 204 205
    $result = $alias;
  }
  return $result;
}

/**
 * Given a path alias, return the internal path it represents.
 *
 * @param $path
 *   A Drupal path alias.
206 207
 * @param $path_language
 *   An optional language code to look up the path in.
208 209 210 211 212
 *
 * @return
 *   The internal path represented by the alias, or the original alias if no
 *   internal path was found.
 */
213
function drupal_get_normal_path($path, $path_language = '') {
214 215 216
  $original_path = $path;

  // Lookup the path alias first.
217
  if ($source = drupal_lookup_path('source', $path, $path_language)) {
218
    $path = $source;
219
  }
220 221 222 223 224 225 226

  // 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';
    $function($path, $original_path, $path_language);
227
  }
228 229

  return $path;
230 231 232 233 234
}

/**
 * Return a component of the current Drupal path.
 *
235
 * When viewing a page at the path "admin/structure/types", for example, arg(0)
236
 * returns "admin", arg(1) returns "content", and arg(2) returns "types".
237 238
 *
 * Avoid use of this function where possible, as resulting code is hard to read.
239 240 241 242
 * In menu callback functions, attempt to use named arguments. See the explanation
 * in menu.inc for how to construct callbacks that take arguments. When attempting
 * to use this function to load an element from the current path, e.g. loading the
 * node on a node page, please use menu_get_object() instead.
243 244 245 246
 *
 * @param $index
 *   The index of the component, where each component is separated by a '/'
 *   (forward-slash), and where the first component has an index of 0 (zero).
247 248
 * @param $path
 *   A path to break into components. Defaults to the path of the current page.
249 250
 *
 * @return
251
 *   The component specified by $index, or NULL if the specified component was
252 253
 *   not found.
 */
254
function arg($index = NULL, $path = NULL) {
255 256 257 258 259 260 261 262
  // Even though $arguments doesn't need to be resettable for any functional
  // reasons (the result of explode() does not depend on any run-time
  // information), it should be resettable anyway in case a module needs to
  // free up the memory used by it.
  // Use the advanced drupal_static() pattern, since this is called very often.
  static $drupal_static = array();
  isset($drupal_static[__FUNCTION__]) || ($drupal_static[__FUNCTION__] = &drupal_static(__FUNCTION__));
  $arguments = &$drupal_static[__FUNCTION__];
263

264 265
  if (!isset($path)) {
    $path = $_GET['q'];
266
  }
267 268 269 270 271 272 273 274
  if (!isset($arguments[$path])) {
    $arguments[$path] = explode('/', $path);
  }
  if (!isset($index)) {
    return $arguments[$path];
  }
  if (isset($arguments[$path][$index])) {
    return $arguments[$path][$index];
275 276 277 278 279 280 281 282 283 284 285 286
  }
}

/**
 * Get the title of the current page, for display on the page and in the title bar.
 *
 * @return
 *   The current page's title.
 */
function drupal_get_title() {
  $title = drupal_set_title();

287
  // During a bootstrap, menu.inc is not included and thus we cannot provide a title.
288 289 290 291 292 293 294 295 296 297 298 299 300
  if (!isset($title) && function_exists('menu_get_active_title')) {
    $title = check_plain(menu_get_active_title());
  }

  return $title;
}

/**
 * Set the title of the current page, for display on the page and in the title bar.
 *
 * @param $title
 *   Optional string value to assign to the page title; or if set to NULL
 *   (default), leaves the current title unchanged.
301 302 303 304 305
 * @param $output
 *   Optional flag - normally should be left as CHECK_PLAIN. Only set to
 *   PASS_THROUGH if you have already removed any possibly dangerous code
 *   from $title using a function like check_plain() or filter_xss(). With this
 *   flag the string will be passed through unchanged.
306 307 308 309
 *
 * @return
 *   The updated title of the current page.
 */
310
function drupal_set_title($title = NULL, $output = CHECK_PLAIN) {
311
  $stored_title = &drupal_static(__FUNCTION__);
312 313

  if (isset($title)) {
314
    $stored_title = ($output == PASS_THROUGH) ? $title : check_plain($title);
315
  }
316

317 318
  return $stored_title;
}
319 320 321 322 323 324 325 326

/**
 * 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() {
327 328 329 330
  // Use the advanced drupal_static() pattern, since this is called very often.
  static $drupal_static = array();
  isset($drupal_static[__FUNCTION__]) || ($drupal_static[__FUNCTION__] = &drupal_static(__FUNCTION__));
  $is_front_page = &$drupal_static[__FUNCTION__];
331 332

  if (!isset($is_front_page)) {
333
    // As drupal_path_initialize updates $_GET['q'] with the 'site_frontpage' path,
334
    // we can check it against the 'site_frontpage' variable.
335
    $is_front_page = ($_GET['q'] == variable_get('site_frontpage', 'node'));
336 337 338
  }

  return $is_front_page;
339
}
340 341 342 343 344 345 346 347 348 349 350 351 352

/**
 * 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) {
353
  $regexps = &drupal_static(__FUNCTION__);
354

355
  if (!isset($regexps[$patterns])) {
356
    $regexps[$patterns] = '/^(' . preg_replace(array('/(\r\n?|\n)/', '/\\\\\*/', '/(^|\|)\\\\<front\\\\>($|\|)/'), array('|', '.*', '\1' . preg_quote(variable_get('site_frontpage', 'node'), '/') . '\2'), preg_quote($patterns, '/')) . ')$/';
357
  }
358
  return (bool)preg_match($regexps[$patterns], $path);
359
}
360 361 362 363 364 365 366 367 368 369 370 371 372 373

/**
 * 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.
 *
 * This function is not available in hook_boot() so use $_GET['q'] instead.
 * However, be careful when doing that because in the case of Example #3
 * $_GET['q'] will contain "path/alias". If "node/306" is needed, calling
374
 * drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL) makes this function available.
375 376 377 378 379 380 381
 *
 * @return
 *   The current Drupal URL path.
 */
function current_path() {
  return $_GET['q'];
}
382 383 384 385 386 387 388 389 390 391 392 393

/**
 * Rebuild the path alias white list.
 *
 * @return
 *   An array containing a white list of path aliases.
 */
function drupal_path_alias_whitelist_rebuild() {
  // For each alias in the database, get the top level component of the system
  // path it corresponds to. This is the portion of the path before the first /
  // if present, otherwise the whole path itself.
  $whitelist = array();
394
  $result = db_query("SELECT SUBSTRING_INDEX(source, '/', 1) AS path FROM {url_alias} GROUP BY path");
395 396 397 398 399 400
  foreach ($result as $row) {
    $whitelist[$row->path] = TRUE;
  }
  variable_set('path_alias_whitelist', $whitelist);
  return $whitelist;
}
401 402 403 404 405 406 407 408 409 410 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 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484

/**
 * 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.
 *   - language: The language of the alias.
 */
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.
 *   - language: (optional) The language of the alias.
 */
function path_save(&$path) {
  $path += array('pid' => NULL, 'language' => '');

  // Insert or update the alias.
  $status = drupal_write_record('url_alias', $path, (!empty($path['pid']) ? 'pid' : NULL));

  // Verify that a record was written.
  if (isset($status) && $status) {
    if ($status === SAVED_NEW) {
      module_invoke_all('path_insert', $path);
    }
    else {
      module_invoke_all('path_update', $path);
    }
    drupal_clear_path_cache();
  }
}

/**
 * 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);
  drupal_clear_path_cache();
}

485 486 487 488 489 490 491 492 493 494 495 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
/**
 * 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.
 * @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;
}