upgrade to OpenPublish 7.x-1.0-alpha9
[eupraxis:fsrn.git] / openpublish / includes / path.inc
1 <?php
2
3 /**
4  * @file
5  * Functions to handle paths in Drupal, including path aliasing.
6  *
7  * These functions are not loaded for cached pages, but modules that need
8  * to use them in hook_boot() or hook exit() can make them available, by
9  * executing "drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);".
10  */
11
12 /**
13  * Initialize the $_GET['q'] variable to the proper normal path.
14  */
15 function drupal_path_initialize() {
16   // Ensure $_GET['q'] is set before calling drupal_normal_path(), to support
17   // path caching with hook_url_inbound_alter().
18   if (empty($_GET['q'])) {
19     $_GET['q'] = variable_get('site_frontpage', 'node');
20   }
21   $_GET['q'] = drupal_get_normal_path($_GET['q']);
22 }
23
24 /**
25  * Given an alias, return its Drupal system URL if one exists. Given a Drupal
26  * system URL return one of its aliases if such a one exists. Otherwise,
27  * return FALSE.
28  *
29  * @param $action
30  *   One of the following values:
31  *   - wipe: delete the alias cache.
32  *   - alias: return an alias for a given Drupal system path (if one exists).
33  *   - source: return the Drupal system URL for a path alias (if one exists).
34  * @param $path
35  *   The path to investigate for corresponding aliases or system URLs.
36  * @param $path_language
37  *   Optional language code to search the path with. Defaults to the page language.
38  *   If there's no path defined for that language it will search paths without
39  *   language.
40  *
41  * @return
42  *   Either a Drupal system path, an aliased path, or FALSE if no path was
43  *   found.
44  */
45 function drupal_lookup_path($action, $path = '', $path_language = NULL) {
46   global $language_url;
47   // Use the advanced drupal_static() pattern, since this is called very often.
48   static $drupal_static_fast;
49   if (!isset($drupal_static_fast)) {
50     $drupal_static_fast['cache'] = &drupal_static(__FUNCTION__);
51   }
52   $cache = &$drupal_static_fast['cache'];
53
54   if (!isset($cache)) {
55     $cache = array(
56       'map' => array(),
57       'no_source' => array(),
58       'whitelist' => NULL,
59       'system_paths' => array(),
60       'no_aliases' => array(),
61       'first_call' => TRUE,
62     );
63   }
64
65   // Retrieve the path alias whitelist.
66   if (!isset($cache['whitelist'])) {
67     $cache['whitelist'] = variable_get('path_alias_whitelist', NULL);
68     if (!isset($cache['whitelist'])) {
69       $cache['whitelist'] = drupal_path_alias_whitelist_rebuild();
70     }
71   }
72
73   // If no language is explicitly specified we default to the current URL
74   // language. If we used a language different from the one conveyed by the
75   // requested URL, we might end up being unable to check if there is a path
76   // alias matching the URL path.
77   $path_language = $path_language ? $path_language : $language_url->language;
78
79   if ($action == 'wipe') {
80     $cache = array();
81     $cache['whitelist'] = drupal_path_alias_whitelist_rebuild();
82   }
83   elseif ($cache['whitelist'] && $path != '') {
84     if ($action == 'alias') {
85       // During the first call to drupal_lookup_path() per language, load the
86       // expected system paths for the page from cache.
87       if (!empty($cache['first_call'])) {
88         $cache['first_call'] = FALSE;
89
90         $cache['map'][$path_language] = array();
91         // Load system paths from cache.
92         $cid = current_path();
93         if ($cached = cache_get($cid, 'cache_path')) {
94           $cache['system_paths'] = $cached->data;
95           // Now fetch the aliases corresponding to these system paths.
96           $args = array(
97             ':system' => $cache['system_paths'],
98             ':language' => $path_language,
99             ':language_none' => LANGUAGE_NONE,
100           );
101           // Always get the language-specific alias before the language-neutral
102           // one. For example 'de' is less than 'und' so the order needs to be
103           // ASC, while 'xx-lolspeak' is more than 'und' so the order needs to
104           // be DESC. We also order by pid ASC so that fetchAllKeyed() returns
105           // the most recently created alias for each source. Subsequent queries
106           // using fetchField() must use pid DESC to have the same effect.
107           // For performance reasons, the query builder is not used here.
108           if ($path_language == LANGUAGE_NONE) {
109             // Prevent PDO from complaining about a token the query doesn't use.
110             unset($args[':language']);
111             $result = db_query('SELECT source, alias FROM {url_alias} WHERE source IN (:system) AND language = :language_none ORDER BY pid ASC', $args);
112           }
113           elseif ($path_language < LANGUAGE_NONE) {
114             $result = db_query('SELECT source, alias FROM {url_alias} WHERE source IN (:system) AND language IN (:language, :language_none) ORDER BY language ASC, pid ASC', $args);
115           }
116           else {
117             $result = db_query('SELECT source, alias FROM {url_alias} WHERE source IN (:system) AND language IN (:language, :language_none) ORDER BY language DESC, pid ASC', $args);
118           }
119           $cache['map'][$path_language] = $result->fetchAllKeyed();
120           // Keep a record of paths with no alias to avoid querying twice.
121           $cache['no_aliases'][$path_language] = array_flip(array_diff_key($cache['system_paths'], array_keys($cache['map'][$path_language])));
122         }
123       }
124       // If the alias has already been loaded, return it.
125       if (isset($cache['map'][$path_language][$path])) {
126         return $cache['map'][$path_language][$path];
127       }
128       // Check the path whitelist, if the top_level part before the first /
129       // is not in the list, then there is no need to do anything further,
130       // it is not in the database.
131       elseif (!isset($cache['whitelist'][strtok($path, '/')])) {
132         return FALSE;
133       }
134       // For system paths which were not cached, query aliases individually.
135       elseif (!isset($cache['no_aliases'][$path_language][$path])) {
136         $args = array(
137           ':source' => $path,
138           ':language' => $path_language,
139           ':language_none' => LANGUAGE_NONE,
140         );
141         // See the queries above.
142         if ($path_language == LANGUAGE_NONE) {
143           unset($args[':language']);
144           $alias = db_query("SELECT alias FROM {url_alias} WHERE source = :source AND language = :language_none ORDER BY pid DESC", $args)->fetchField();
145         }
146         elseif ($path_language > LANGUAGE_NONE) {
147           $alias = db_query("SELECT alias FROM {url_alias} WHERE source = :source AND language IN (:language, :language_none) ORDER BY language DESC, pid DESC", $args)->fetchField();
148         }
149         else {
150           $alias = db_query("SELECT alias FROM {url_alias} WHERE source = :source AND language IN (:language, :language_none) ORDER BY language ASC, pid DESC", $args)->fetchField();
151         }
152         $cache['map'][$path_language][$path] = $alias;
153         return $alias;
154       }
155     }
156     // Check $no_source for this $path in case we've already determined that there
157     // isn't a path that has this alias
158     elseif ($action == 'source' && !isset($cache['no_source'][$path_language][$path])) {
159       // Look for the value $path within the cached $map
160       $source = FALSE;
161       if (!isset($cache['map'][$path_language]) || !($source = array_search($path, $cache['map'][$path_language]))) {
162         $args = array(
163           ':alias' => $path,
164           ':language' => $path_language,
165           ':language_none' => LANGUAGE_NONE,
166         );
167         // See the queries above.
168         if ($path_language == LANGUAGE_NONE) {
169           unset($args[':language']);
170           $result = db_query("SELECT source FROM {url_alias} WHERE alias = :alias AND language = :language_none ORDER BY pid DESC", $args);
171         }
172         elseif ($path_language > LANGUAGE_NONE) {
173           $result = db_query("SELECT source FROM {url_alias} WHERE alias = :alias AND language IN (:language, :language_none) ORDER BY language DESC, pid DESC", $args);
174         }
175         else {
176           $result = db_query("SELECT source FROM {url_alias} WHERE alias = :alias AND language IN (:language, :language_none) ORDER BY language ASC, pid DESC", $args);
177         }
178         if ($source = $result->fetchField()) {
179           $cache['map'][$path_language][$source] = $path;
180         }
181         else {
182           // We can't record anything into $map because we do not have a valid
183           // index and there is no need because we have not learned anything
184           // about any Drupal path. Thus cache to $no_source.
185           $cache['no_source'][$path_language][$path] = TRUE;
186         }
187       }
188       return $source;
189     }
190   }
191
192   return FALSE;
193 }
194
195 /**
196  * Cache system paths for a page.
197  *
198  * Cache an array of the system paths available on each page. We assume
199  * that aliases will be needed for the majority of these paths during
200  * subsequent requests, and load them in a single query during
201  * drupal_lookup_path().
202  */
203 function drupal_cache_system_paths() {
204   // Check if the system paths for this page were loaded from cache in this
205   // request to avoid writing to cache on every request.
206   $cache = &drupal_static('drupal_lookup_path', array());
207   if (empty($cache['system_paths']) && !empty($cache['map'])) {
208     // Generate a cache ID (cid) specifically for this page.
209     $cid = current_path();
210     // The static $map array used by drupal_lookup_path() includes all
211     // system paths for the page request.
212     if ($paths = current($cache['map'])) {
213       $data = array_keys($paths);
214       $expire = REQUEST_TIME + (60 * 60 * 24);
215       cache_set($cid, $data, 'cache_path', $expire);
216     }
217   }
218 }
219
220 /**
221  * Given an internal Drupal path, return the alias set by the administrator.
222  *
223  * If no path is provided, the function will return the alias of the current
224  * page.
225  *
226  * @param $path
227  *   An internal Drupal path.
228  * @param $path_language
229  *   An optional language code to look up the path in.
230  *
231  * @return
232  *   An aliased path if one was found, or the original path if no alias was
233  *   found.
234  */
235 function drupal_get_path_alias($path = NULL, $path_language = NULL) {
236   // If no path is specified, use the current page's path.
237   if ($path == NULL) {
238     $path = $_GET['q'];
239   }
240   $result = $path;
241   if ($alias = drupal_lookup_path('alias', $path, $path_language)) {
242     $result = $alias;
243   }
244   return $result;
245 }
246
247 /**
248  * Given a path alias, return the internal path it represents.
249  *
250  * @param $path
251  *   A Drupal path alias.
252  * @param $path_language
253  *   An optional language code to look up the path in.
254  *
255  * @return
256  *   The internal path represented by the alias, or the original alias if no
257  *   internal path was found.
258  */
259 function drupal_get_normal_path($path, $path_language = NULL) {
260   $original_path = $path;
261
262   // Lookup the path alias first.
263   if ($source = drupal_lookup_path('source', $path, $path_language)) {
264     $path = $source;
265   }
266
267   // Allow other modules to alter the inbound URL. We cannot use drupal_alter()
268   // here because we need to run hook_url_inbound_alter() in the reverse order
269   // of hook_url_outbound_alter().
270   foreach (array_reverse(module_implements('url_inbound_alter')) as $module) {
271     $function = $module . '_url_inbound_alter';
272     $function($path, $original_path, $path_language);
273   }
274
275   return $path;
276 }
277
278 /**
279  * Check if the current page is the front page.
280  *
281  * @return
282  *   Boolean value: TRUE if the current page is the front page; FALSE if otherwise.
283  */
284 function drupal_is_front_page() {
285   // Use the advanced drupal_static() pattern, since this is called very often.
286   static $drupal_static_fast;
287   if (!isset($drupal_static_fast)) {
288     $drupal_static_fast['is_front_page'] = &drupal_static(__FUNCTION__);
289   }
290   $is_front_page = &$drupal_static_fast['is_front_page'];
291
292   if (!isset($is_front_page)) {
293     // As drupal_path_initialize updates $_GET['q'] with the 'site_frontpage' path,
294     // we can check it against the 'site_frontpage' variable.
295     $is_front_page = ($_GET['q'] == variable_get('site_frontpage', 'node'));
296   }
297
298   return $is_front_page;
299 }
300
301 /**
302  * Check if a path matches any pattern in a set of patterns.
303  *
304  * @param $path
305  *   The path to match.
306  * @param $patterns
307  *   String containing a set of patterns separated by \n, \r or \r\n.
308  *
309  * @return
310  *   Boolean value: TRUE if the path matches a pattern, FALSE otherwise.
311  */
312 function drupal_match_path($path, $patterns) {
313   $regexps = &drupal_static(__FUNCTION__);
314
315   if (!isset($regexps[$patterns])) {
316     // Convert path settings to a regular expression.
317     // Therefore replace newlines with a logical or, /* with asterisks and the <front> with the frontpage.
318     $to_replace = array(
319       '/(\r\n?|\n)/', // newlines
320       '/\\\\\*/',     // asterisks
321       '/(^|\|)\\\\<front\\\\>($|\|)/' // <front>
322     );
323     $replacements = array(
324       '|',
325       '.*',
326       '\1' . preg_quote(variable_get('site_frontpage', 'node'), '/') . '\2'
327     );
328     $patterns_quoted = preg_quote($patterns, '/');
329     $regexps[$patterns] = '/^(' . preg_replace($to_replace, $replacements, $patterns_quoted) . ')$/';
330   }
331   return (bool)preg_match($regexps[$patterns], $path);
332 }
333
334 /**
335  * Return the current URL path of the page being viewed.
336  *
337  * Examples:
338  * - http://example.com/node/306 returns "node/306".
339  * - http://example.com/drupalfolder/node/306 returns "node/306" while
340  *   base_path() returns "/drupalfolder/".
341  * - http://example.com/path/alias (which is a path alias for node/306) returns
342  *   "node/306" as opposed to the path alias.
343  *
344  * This function is not available in hook_boot() so use $_GET['q'] instead.
345  * However, be careful when doing that because in the case of Example #3
346  * $_GET['q'] will contain "path/alias". If "node/306" is needed, calling
347  * drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL) makes this function available.
348  *
349  * @return
350  *   The current Drupal URL path.
351  *
352  * @see request_path()
353  */
354 function current_path() {
355   return $_GET['q'];
356 }
357
358 /**
359  * Rebuild the path alias white list.
360  *
361  * @param $source
362  *   An optional system path for which an alias is being inserted.
363  *
364  * @return
365  *   An array containing a white list of path aliases.
366  */
367 function drupal_path_alias_whitelist_rebuild($source = NULL) {
368   // When paths are inserted, only rebuild the whitelist if the system path
369   // has a top level component which is not already in the whitelist.
370   if (!empty($source)) {
371     $whitelist = variable_get('path_alias_whitelist', NULL);
372     if (isset($whitelist[strtok($source, '/')])) {
373       return $whitelist;
374     }
375   }
376   // For each alias in the database, get the top level component of the system
377   // path it corresponds to. This is the portion of the path before the first
378   // '/', if present, otherwise the whole path itself.
379   $whitelist = array();
380   $result = db_query("SELECT DISTINCT SUBSTRING_INDEX(source, '/', 1) AS path FROM {url_alias}");
381   foreach ($result as $row) {
382     $whitelist[$row->path] = TRUE;
383   }
384   variable_set('path_alias_whitelist', $whitelist);
385   return $whitelist;
386 }
387
388 /**
389  * Fetch a specific URL alias from the database.
390  *
391  * @param $conditions
392  *   A string representing the source, a number representing the pid, or an
393  *   array of query conditions.
394  *
395  * @return
396  *   FALSE if no alias was found or an associative array containing the
397  *   following keys:
398  *   - source: The internal system path.
399  *   - alias: The URL alias.
400  *   - pid: Unique path alias identifier.
401  *   - language: The language of the alias.
402  */
403 function path_load($conditions) {
404   if (is_numeric($conditions)) {
405     $conditions = array('pid' => $conditions);
406   }
407   elseif (is_string($conditions)) {
408     $conditions = array('source' => $conditions);
409   }
410   elseif (!is_array($conditions)) {
411     return FALSE;
412   }
413   $select = db_select('url_alias');
414   foreach ($conditions as $field => $value) {
415     $select->condition($field, $value);
416   }
417   return $select
418     ->fields('url_alias')
419     ->execute()
420     ->fetchAssoc();
421 }
422
423 /**
424  * Save a path alias to the database.
425  *
426  * @param $path
427  *   An associative array containing the following keys:
428  *   - source: The internal system path.
429  *   - alias: The URL alias.
430  *   - pid: (optional) Unique path alias identifier.
431  *   - language: (optional) The language of the alias.
432  */
433 function path_save(&$path) {
434   $path += array('language' => LANGUAGE_NONE);
435
436   // Load the stored alias, if any.
437   if (!empty($path['pid']) && !isset($path['original'])) {
438     $path['original'] = path_load($path['pid']);
439   }
440
441   if (empty($path['pid'])) {
442     drupal_write_record('url_alias', $path);
443     module_invoke_all('path_insert', $path);
444   }
445   else {
446     drupal_write_record('url_alias', $path, array('pid'));
447     module_invoke_all('path_update', $path);
448   }
449
450   // Clear internal properties.
451   unset($path['original']);
452
453   // Clear the static alias cache.
454   drupal_clear_path_cache($path['source']);
455 }
456
457 /**
458  * Delete a URL alias.
459  *
460  * @param $criteria
461  *   A number representing the pid or an array of criteria.
462  */
463 function path_delete($criteria) {
464   if (!is_array($criteria)) {
465     $criteria = array('pid' => $criteria);
466   }
467   $path = path_load($criteria);
468   $query = db_delete('url_alias');
469   foreach ($criteria as $field => $value) {
470     $query->condition($field, $value);
471   }
472   $query->execute();
473   module_invoke_all('path_delete', $path);
474   drupal_clear_path_cache($path['source']);
475 }
476
477 /**
478  * Determine whether a path is in the administrative section of the site.
479  *
480  * By default, paths are considered to be non-administrative. If a path does not
481  * match any of the patterns in path_get_admin_paths(), or if it matches both
482  * administrative and non-administrative patterns, it is considered
483  * non-administrative.
484  *
485  * @param $path
486  *   A Drupal path.
487  *
488  * @return
489  *   TRUE if the path is administrative, FALSE otherwise.
490  *
491  * @see path_get_admin_paths()
492  * @see hook_admin_paths()
493  * @see hook_admin_paths_alter()
494  */
495 function path_is_admin($path) {
496   $path_map = &drupal_static(__FUNCTION__);
497   if (!isset($path_map['admin'][$path])) {
498     $patterns = path_get_admin_paths();
499     $path_map['admin'][$path] = drupal_match_path($path, $patterns['admin']);
500     $path_map['non_admin'][$path] = drupal_match_path($path, $patterns['non_admin']);
501   }
502   return $path_map['admin'][$path] && !$path_map['non_admin'][$path];
503 }
504
505 /**
506  * Get a list of administrative and non-administrative paths.
507  *
508  * @return array
509  *   An associative array containing the following keys:
510  *   'admin': An array of administrative paths and regular expressions
511  *            in a format suitable for drupal_match_path().
512  *   'non_admin': An array of non-administrative paths and regular expressions.
513  *
514  * @see hook_admin_paths()
515  * @see hook_admin_paths_alter()
516  */
517 function path_get_admin_paths() {
518   $patterns = &drupal_static(__FUNCTION__);
519   if (!isset($patterns)) {
520     $paths = module_invoke_all('admin_paths');
521     drupal_alter('admin_paths', $paths);
522     // Combine all admin paths into one array, and likewise for non-admin paths,
523     // for easier handling.
524     $patterns = array();
525     $patterns['admin'] = array();
526     $patterns['non_admin'] = array();
527     foreach ($paths as $path => $enabled) {
528       if ($enabled) {
529         $patterns['admin'][] = $path;
530       }
531       else {
532         $patterns['non_admin'][] = $path;
533       }
534     }
535     $patterns['admin'] = implode("\n", $patterns['admin']);
536     $patterns['non_admin'] = implode("\n", $patterns['non_admin']);
537   }
538   return $patterns;
539 }
540
541 /**
542  * Checks a path exists and the current user has access to it.
543  *
544  * @param $path
545  *   The path to check.
546  * @param $dynamic_allowed
547  *   Whether paths with menu wildcards (like user/%) should be allowed.
548  *
549  * @return
550  *   TRUE if it is a valid path AND the current user has access permission,
551  *   FALSE otherwise.
552  */
553 function drupal_valid_path($path, $dynamic_allowed = FALSE) {
554   global $menu_admin;
555   // We indicate that a menu administrator is running the menu access check.
556   $menu_admin = TRUE;
557   if ($path == '<front>' || url_is_external($path)) {
558     $item = array('access' => TRUE);
559   }
560   elseif ($dynamic_allowed && preg_match('/\/\%/', $path)) {
561     // Path is dynamic (ie 'user/%'), so check directly against menu_router table.
562     if ($item = db_query("SELECT * FROM {menu_router} where path = :path", array(':path' => $path))->fetchAssoc()) {
563       $item['link_path']  = $form_item['link_path'];
564       $item['link_title'] = $form_item['link_title'];
565       $item['external']   = FALSE;
566       $item['options'] = '';
567       _menu_link_translate($item);
568     }
569   }
570   else {
571     $item = menu_get_item($path);
572   }
573   $menu_admin = FALSE;
574   return $item && $item['access'];
575 }
576
577 /**
578  * Clear the path cache.
579  *
580  * @param $source
581  *   An optional system path for which an alias is being changed.
582  */
583 function drupal_clear_path_cache($source = NULL) {
584   // Clear the drupal_lookup_path() static cache.
585   drupal_static_reset('drupal_lookup_path');
586   drupal_path_alias_whitelist_rebuild($source);
587 }