Issue #1329196: Fixed token_scan() broke the token rules integration.
[project/token.git] / token.module
1 <?php
2
3 /**
4 * @file
5 * The Token API module.
6 *
7 * The Token module provides an API for providing tokens to other modules.
8 * Tokens are small bits of text that can be placed into larger documents
9 * via simple placeholders, like %site-name or [user].
10 *
11 * @ingroup token
12 */
13
14 /**
15 * The default token prefix string.
16 */
17 define('TOKEN_PREFIX', '[');
18
19 /**
20 * The default token suffix string.
21 */
22 define('TOKEN_SUFFIX', ']');
23
24 /**
25 * Implements hook_help().
26 */
27 function token_help($path, $arg) {
28 if ($path == 'admin/help#token') {
29 $output = '<dl>';
30 $output .= '<dt>' . t('List of the currently available tokens on this site') . '</dt>';
31 $output .= '<dd>' . theme('token_tree', 'all', TRUE, FALSE) . '</dd>';
32 $output .= '</dl>';
33 return $output;
34 }
35 }
36
37 /**
38 * Return an array of the core modules supported by token.module.
39 */
40 function _token_core_supported_modules() {
41 return array('node', 'user', 'taxonomy', 'comment', 'menu', 'book');
42 }
43
44 /**
45 * Implements hook_menu().
46 */
47 function token_menu() {
48 $items = array();
49
50 // Devel token pages.
51 if (module_exists('devel')) {
52 $items['node/%node/devel/token'] = array(
53 'title' => 'Tokens',
54 'page callback' => 'token_devel_token_object',
55 'page arguments' => array('node', 1),
56 'access arguments' => array('access devel information'),
57 'type' => MENU_LOCAL_TASK,
58 'file' => 'token.pages.inc',
59 'weight' => 5,
60 );
61 $items['user/%user/devel/token'] = array(
62 'title' => 'Tokens',
63 'page callback' => 'token_devel_token_object',
64 'page arguments' => array('user', 1),
65 'access arguments' => array('access devel information'),
66 'type' => MENU_LOCAL_TASK,
67 'file' => 'token.pages.inc',
68 'weight' => 5,
69 );
70 }
71
72 return $items;
73 }
74
75 /**
76 * Implements hook_theme().
77 */
78 function token_theme() {
79 return array(
80 'token_help' => array(
81 'arguments' => array('type' => 'all', 'prefix' => TOKEN_PREFIX, 'suffix' => TOKEN_SUFFIX),
82 'file' => 'token.pages.inc',
83 ),
84 'token_tree' => array(
85 'arguments' => array('token_types' => array(), 'global_types' => TRUE , 'click_insert' => TRUE),
86 'file' => 'token.pages.inc',
87 ),
88 );
89 }
90
91 /**
92 * Implements hook_token_values().
93 */
94 function token_token_values($type, $object = NULL) {
95 global $user;
96 $values = array();
97
98 switch ($type) {
99 case 'global':
100 // Current user tokens.
101 $values['user-name'] = $user->uid ? $user->name : variable_get('anonymous', t('Anonymous'));
102 $values['user-id'] = $user->uid ? $user->uid : 0;
103 $values['user-mail'] = $user->uid ? $user->mail : '';
104
105 // Site information tokens.
106 $values['site-url'] = url('<front>', array('absolute' => TRUE));
107 $values['site-name'] = check_plain(variable_get('site_name', t('Drupal')));
108 $values['site-slogan'] = check_plain(variable_get('site_slogan', ''));
109 $values['site-mission'] = filter_xss_admin(variable_get('site_mission', ''));
110 $values['site-mail'] = variable_get('site_mail', '');
111 $values += token_get_date_token_values(NULL, 'site-date-');
112
113 // Current page tokens.
114 $values['current-page-title'] = drupal_get_title();
115 $alias = drupal_get_path_alias($_GET['q']);
116 $values['current-page-path-raw'] = $alias;
117 $values['current-page-path'] = check_plain($alias);
118 $values['current-page-url'] = url($_GET['q'], array('absolute' => TRUE));
119
120 $page = isset($_GET['page']) ? $_GET['page'] : '';
121 $pager_page_array = explode(',', $page);
122 $page = $pager_page_array[0];
123 $values['current-page-number'] = (int) $page + 1;
124
125 // Backwards compatability for renamed tokens.
126 $values['site-date'] = $values['site-date-small'];
127 $values['page-number'] = $values['current-page-number'];
128
129 break;
130 }
131 return $values;
132 }
133
134 /**
135 * Implements hook_token_list().
136 */
137 function token_token_list($type = 'all') {
138 $tokens = array();
139
140 if ($type == 'global' || $type == 'all') {
141 // Current user tokens.
142 $tokens['global']['user-name'] = t('The name of the currently logged in user.');
143 $tokens['global']['user-id'] = t('The user ID of the currently logged in user.');
144 $tokens['global']['user-mail'] = t('The email address of the currently logged in user.');
145
146 // Site information tokens.
147 $tokens['global']['site-url'] = t("The URL of the site's front page.");
148 $tokens['global']['site-name'] = t('The name of the site.');
149 $tokens['global']['site-slogan'] = t('The slogan of the site.');
150 $tokens['global']['site-mission'] = t("The optional 'mission' of the site.");
151 $tokens['global']['site-mail'] = t('The administrative email address for the site.');
152 $tokens['global'] += token_get_date_token_info(t('The current'), 'site-date-');
153
154 // Current page tokens.
155 $tokens['global']['current-page-title'] = t('The title of the current page.');
156 $tokens['global']['current-page-path'] = t('The URL alias of the current page.');
157 $tokens['global']['current-page-path-raw'] = t('The URL alias of the current page.');
158 $tokens['global']['current-page-url'] = t('The URL of the current page.');
159 $tokens['global']['current-page-number'] = t('The page number of the current page when viewing paged lists.');
160 }
161
162 return $tokens;
163 }
164
165 /**
166 * General function to include the files that token relies on for the real work.
167 */
168 function token_include() {
169 static $run = FALSE;
170
171 if (!$run) {
172 $run = TRUE;
173 $modules_enabled = array_keys(module_list());
174 $modules = array_intersect(_token_core_supported_modules(), $modules_enabled);
175 foreach ($modules as $module) {
176 module_load_include('inc', 'token', "token_$module");
177 }
178 }
179 }
180
181 /**
182 * Replace all tokens in a given string with appropriate values.
183 *
184 * @param $text
185 * A string potentially containing replaceable tokens.
186 * @param $type
187 * (optional) A flag indicating the class of substitution tokens to use. If
188 * an object is passed in the second param, 'type' should contain the
189 * object's type. For example, 'node', 'comment', or 'user'. If no type is
190 * specified, only 'global' site-wide substitution tokens are built.
191 * @param $object
192 * (optional) An object to use for building substitution values (e.g. a node
193 * comment, or user object).
194 * @param $leading
195 * (optional) Character(s) to prepend to the token key before searching for
196 * matches. Defaults to TOKEN_PREFIX.
197 * @param $trailing
198 * (optional) Character(s) to append to the token key before searching for
199 * matches. Defaults to TOKEN_SUFFIX.
200 * @param $options
201 * (optional) A keyed array of settings and flags to control the token
202 * generation and replacement process. Supported options are:
203 * - clear: A boolean flag indicating that tokens should be removed from the
204 * final text if no replacement value can be generated.
205 * @param $flush
206 * (optional) A flag indicating whether or not to flush the token cache.
207 * Useful for processes that need to slog through huge numbers of tokens
208 * in a single execution cycle. Flushing it will keep them from burning
209 * through memory. Defaults to FALSE.
210 *
211 * @return
212 * Text with tokens replaced.
213 */
214 function token_replace($text, $type = 'global', $object = NULL, $leading = TOKEN_PREFIX, $trailing = TOKEN_SUFFIX, $options = array(), $flush = FALSE) {
215 return token_replace_multiple($text, array($type => $object), $leading, $trailing, $options, $flush);
216 }
217
218 /**
219 * Replace all tokens in a given string with appropriate values.
220 *
221 * Contrary to token_replace() this function supports replacing multiple types.
222 *
223 * @param $text
224 * A string potentially containing replaceable tokens.
225 * @param $types
226 * (optional) An array of substitution classes and optional objects. The key
227 * is a flag indicating the class of substitution tokens to use. If an object
228 * is passed as value, the key should contain the object's type. For example,
229 * 'node', 'comment', or 'user'. The object will be used for building
230 * substitution values. If no type is specified, only 'global' site-wide
231 * substitution tokens are built.
232 * @param $leading
233 * (optional) Character(s) to prepend to the token key before searching for
234 * matches. Defaults to TOKEN_PREFIX.
235 * @param $trailing
236 * (optional) Character(s) to append to the token key before searching for
237 * matches. Defaults to TOKEN_SUFFIX.
238 * @param $options
239 * (optional) A keyed array of settings and flags to control the token
240 * generation and replacement process. Supported options are:
241 * - clear: A boolean flag indicating that tokens should be removed from the
242 * final text if no replacement value can be generated.
243 * @param $flush
244 * (optional) A flag indicating whether or not to flush the token cache.
245 * Useful for processes that need to slog through huge numbers of tokens
246 * in a single execution cycle. Flushing it will keep them from burning
247 * through memory. Defaults to FALSE.
248 *
249 * @return
250 * Text with tokens replaced.
251 */
252 function token_replace_multiple($text, $types = array('global' => NULL), $leading = TOKEN_PREFIX, $trailing = TOKEN_SUFFIX, $options = array(), $flush = FALSE) {
253 // Ensure that the $text parameter is a string and not an array which is an
254 // invalid input.
255 if (is_array($text)) {
256 $backtrace = debug_backtrace();
257 foreach ($backtrace as $caller) {
258 switch ($caller['function']) {
259 case 'token_replace':
260 case 'token_replace_multiple':
261 continue;
262 default:
263 trigger_error(t('The @function() function called token replacement with an array rather than a string for $text', array('@function' => $caller['function'])), E_USER_NOTICE);
264 break 2;
265 }
266 }
267
268 foreach ($text as $key => $value) {
269 $text[$key] = token_replace_multiple($value, $types, $leading, $trailing, $options, $flush);
270 }
271 return $text;
272 }
273
274 // If there are no tokens to replace, just return the text.
275 $text_tokens = token_scan($text, $leading, $trailing);
276 if (empty($text_tokens)) {
277 return $text;
278 }
279
280 $full = new stdClass();
281 $full->tokens = $full->values = array();
282
283 // Allow global token replacement by default.
284 if (empty($types) || !is_array($types)) {
285 $types = array('global' => NULL);
286 }
287
288 foreach ($types as $type => $object) {
289 $temp = token_get_values($type, $object, $flush, $options);
290 $full->tokens = array_merge($full->tokens, $temp->tokens);
291 $full->values = array_merge($full->values, $temp->values);
292 }
293
294 // Support clearing out tokens that would not be replaced.
295 if (!empty($options['clear'])) {
296 foreach ($text_tokens as $token) {
297 if (!in_array($token, $full->tokens)) {
298 $full->tokens[] = $token;
299 $full->values[] = '';
300 }
301 }
302 }
303
304 $tokens = token_prepare_tokens($full->tokens, $leading, $trailing);
305 return str_replace($tokens, $full->values, $text);
306 }
307
308 /**
309 * Return a list of valid substitution tokens and their values for
310 * the specified type.
311 *
312 * @param $type
313 * (optional) A flag indicating the class of substitution tokens to use. If an
314 * object is passed in the second param, 'type' should contain the
315 * object's type. For example, 'node', 'comment', or 'user'. If no
316 * type is specified, only 'global' site-wide substitution tokens are
317 * built.
318 * @param $object
319 * (optional) An object to use for building substitution values (e.g. a node
320 * comment, or user object).
321 * @param $flush
322 * (optional) A flag indicating whether or not to flush the token cache.
323 * Useful for processes that need to slog through huge numbers of tokens
324 * in a single execution cycle. Flushing it will keep them from burning
325 * through memory. Defaults to FALSE.
326 * @param $options
327 * (optional) A keyed array of settings and flags to control the token
328 * generation process.
329 *
330 * @return
331 * An object with two properties:
332 * - tokens: All the possible tokens names generated.
333 * - values: The corresponding values for the tokens.
334 *
335 * Note that before performing actual token replacement that the token names
336 * should be run through token_prepare_tokens().
337 */
338 function token_get_values($type = 'global', $object = NULL, $flush = FALSE, $options = array()) {
339 static $tokens = array();
340 static $running = FALSE;
341
342 // Simple recursion check. This is to avoid content_view()'s potential
343 // for endless looping when a filter uses tokens, which load the content
344 // view, which calls the filter, which uses tokens, which...
345 if ($running) {
346 // We'll allow things to get two levels deep, but bail out after that
347 // without performing any substitutions.
348 $result = new stdClass();
349 $result->tokens = array();
350 $result->values = array();
351 return $result;
352 }
353 else {
354 $running = TRUE;
355 }
356
357 // Flush the static token cache. Useful for processes that need to slog
358 // through huge numbers of tokens in a single execution cycle. Flushing it
359 // will keep them from burning through memory.
360 if ($flush || !empty($options['reset'])) {
361 $tokens = array();
362 }
363
364 // Allow simple resets of the static values.
365 if ($type === 'reset') {
366 $tokens = array();
367 $running = FALSE;
368 return;
369 }
370
371 // Neutralize options that do not affect token replacement.
372 $serialized_options = $options;
373 unset($serialized_options['clear']);
374
375 // Store the token cache by object ID and serialized options.
376 $cid = _token_get_id($type, $object) . ':' . md5(serialize($serialized_options));
377 if ($type != 'global' && !isset($tokens[$type][$cid])) {
378 token_include();
379 $tokens[$type][$cid] = module_invoke_all('token_values', $type, $object, $options);
380 }
381
382 // Special-case global tokens, as we always want to be able to process
383 // those substitutions.
384 if (!isset($tokens['global'][$cid])) {
385 token_include();
386 $tokens['global'][$cid] = module_invoke_all('token_values', 'global', NULL, $options);
387 }
388
389 $all = $tokens['global'][$cid];
390 if ($type != 'global') {
391 // Avoid using array_merge() if only global tokens were requested.
392 $all = array_merge($all, $tokens[$type][$cid]);
393 }
394
395 // Allow other modules to alter the replacements.
396 $context = array(
397 'type' => $type,
398 'object' => $object,
399 'options' => $options,
400 );
401 drupal_alter('token_values', $all, $context);
402
403 $result = new stdClass();
404 $result->tokens = array_keys($all);
405 $result->values = array_values($all);
406
407 $running = FALSE;
408
409 return $result;
410 }
411
412 /**
413 * A helper function that retrieves all currently exposed tokens,
414 * and merges them recursively. This is only necessary when building
415 * the token listing -- during actual value replacement, only tokens
416 * in a particular domain are requested and a normal array_marge() is
417 * sufficient.
418 *
419 * @param $types
420 * A flag indicating the class of substitution tokens to use. If an
421 * object is passed in the second param, 'types' should contain the
422 * object's type. For example, 'node', 'comment', or 'user'. 'types'
423 * may also be an array of types of the form array('node','user'). If no
424 * type is specified, only 'global' site-wide substitution tokens are
425 * built.
426 *
427 * @return
428 * The array of usable tokens and their descriptions, organized by
429 * token type.
430 */
431 function token_get_list($types = 'all') {
432 token_include();
433 $return = array();
434 settype($types, 'array');
435 foreach (module_implements('token_list') as $module) {
436 foreach ($types as $type) {
437 $module_token_list = module_invoke($module, 'token_list', $type);
438 if (isset($module_token_list) && is_array($module_token_list)) {
439 foreach ($module_token_list as $category => $tokens) {
440 foreach ($tokens as $token => $title) {
441 // Automatically append a raw token warning.
442 if (substr($token, -4) === '-raw' && strpos($title, t('raw user input')) === FALSE && strpos($title, t('UNIX timestamp format')) === FALSE) {
443 $title .= ' <em>' . t('Warning: Token value contains raw user input.') . '</em>';
444 }
445 $return[$category][$token] = $title;
446 }
447 }
448 }
449 }
450 }
451 // Sort the tokens by name.
452 foreach (array_keys($return) as $category) {
453 ksort($return[$category]);
454 }
455 return $return;
456 }
457
458 /**
459 * A helper function to prepare raw tokens for replacement.
460 *
461 * @param $tokens
462 * The array of tokens names with no delimiting characters.
463 * @param $leading
464 * String to prepend to the token. Default is TOKEN_PREFIX.
465 * @param $trailing
466 * String to append to the token. Default is TOKEN_SUFFIX.
467 *
468 * @return
469 * An array of the formatted tokens.
470 */
471 function token_prepare_tokens($tokens = array(), $leading = TOKEN_PREFIX, $trailing = TOKEN_SUFFIX) {
472 foreach ($tokens as $key => $value) {
473 $tokens[$key] = $leading . $value . $trailing;
474 }
475 return $tokens;
476 }
477
478 /**
479 * A helper function to return an object's ID for use in static caching.
480 */
481 function _token_get_id($type = 'global', $object = NULL) {
482 if (!isset($object)) {
483 return "default";
484 }
485 switch ($type) {
486 case 'node':
487 return isset($object->vid) ? $object->vid : (isset($object->nid) ? $object->nid : 0);
488 case 'comment':
489 return isset($object->cid) ? $object->cid : 0;
490 case 'user':
491 return isset($object->uid) ? $object->uid : 0;
492 case 'taxonomy':
493 return isset($object->tid) ? $object->tid : 0;
494 default:
495 return crc32(serialize($object));
496 }
497 }
498
499 /**
500 * Build a list of common date tokens for use in hook_token_list().
501 *
502 * @param $description
503 */
504 function token_get_date_token_info($description, $token_prefix = '') {
505 $time = time();
506 $tokens[$token_prefix . 'small'] = t("!description date in 'small' format. (%date)", array('!description' => $description, '%date' => format_date($time, 'small')));
507 $tokens[$token_prefix . 'yyyy'] = t("!description year (four digit)", array('!description' => $description));
508 $tokens[$token_prefix . 'yy'] = t("!description year (two digit)", array('!description' => $description));
509 $tokens[$token_prefix . 'month'] = t("!description month (full word)", array('!description' => $description));
510 $tokens[$token_prefix . 'mon'] = t("!description month (abbreviated)", array('!description' => $description));
511 $tokens[$token_prefix . 'mm'] = t("!description month (two digits with leading zeros)", array('!description' => $description));
512 $tokens[$token_prefix . 'm'] = t("!description month (one or two digits without leading zeros)", array('!description' => $description));
513 $tokens[$token_prefix . 'ww'] = t("!description week (two digits with leading zeros)", array('!description' => $description));
514 if (version_compare(PHP_VERSION, '5.1.0', '>=')) {
515 $tokens[$token_prefix . 'date'] = t("!description date (numeric representation of the day of the week)", array('!description' => $description));
516 }
517 $tokens[$token_prefix . 'day'] = t("!description day (full word)", array('!description' => $description));
518 $tokens[$token_prefix . 'ddd'] = t("!description day (abbreviation)", array('!description' => $description));
519 $tokens[$token_prefix . 'dd'] = t("!description day (two digits with leading zeros)", array('!description' => $description));
520 $tokens[$token_prefix . 'd'] = t("!description day (one or two digits without leading zeros)", array('!description' => $description));
521 $tokens[$token_prefix . 'raw'] = t("!description in UNIX timestamp format (%date)", array('!description' => $description, '%date' => $time));
522 $tokens[$token_prefix . 'since'] = t("!description in 'time-since' format. (%date)", array('!description' => $description, '%date' => format_interval($time - 360, 2)));
523 return $tokens;
524 }
525
526 /**
527 * Build a list of common date tokens for use in hook_token_values().
528 */
529 function token_get_date_token_values($timestamp = NULL, $token_prefix = '', $langcode = NULL) {
530 static $formats;
531
532 if (!isset($formats)) {
533 $formats = array();
534 $formats['small'] = variable_get('date_format_short', 'm/d/Y - H:i');
535 $formats['yyyy'] = 'Y';
536 $formats['yy'] = 'y';
537 $formats['month'] = 'F';
538 $formats['mon'] = 'M';
539 $formats['mm'] = 'm';
540 $formats['m'] = 'n';
541 $formats['ww'] = 'W';
542 if (version_compare(PHP_VERSION, '5.1.0', '>=')) {
543 $formats['date'] = 'N';
544 }
545 $formats['day'] = 'l';
546 $formats['ddd'] = 'D';
547 $formats['dd'] = 'd';
548 $formats['d'] = 'j';
549 }
550
551 $time = time();
552 if (!isset($timestamp)) {
553 $timestamp = $time;
554 }
555
556 $tokens = array();
557 foreach ($formats as $token => $format) {
558 $tokens[$token_prefix . $token] = token_format_date($timestamp, 'custom', $format, NULL, $langcode);
559 }
560 $tokens[$token_prefix . 'raw'] = $timestamp;
561 $tokens[$token_prefix . 'since'] = format_interval($time - $timestamp, 2, $langcode);
562
563 return $tokens;
564 }
565
566 /**
567 * A copy of format_date() that supports the 'N' date format character.
568 *
569 * @see format_date()
570 */
571 function token_format_date($timestamp, $type = 'medium', $format = '', $timezone = NULL, $langcode = NULL) {
572 global $user;
573 static $timezones = array();
574
575 // Statically cache each user's timezone so it doesn't need to be re-fetched
576 // ever call.
577 if (!isset($timezones[$user->uid])) {
578 if (!empty($user->uid) && variable_get('configurable_timezones', 1) && strlen($user->timezone)) {
579 $timezones[$user->uid] = $user->timezone;
580 }
581 else {
582 $timezones[$user->uid] = variable_get('date_default_timezone', 0);
583 }
584 }
585
586 $timestamp += $timezones[$user->uid];
587
588 switch ($type) {
589 case 'custom':
590 // No change to format.
591 break;
592 case 'small':
593 $format = variable_get('date_format_short', 'm/d/Y - H:i');
594 break;
595 case 'large':
596 $format = variable_get('date_format_long', 'l, F j, Y - H:i');
597 break;
598 case 'medium':
599 default:
600 $format = variable_get('date_format_medium', 'D, m/d/Y - H:i');
601 }
602
603 $max = strlen($format);
604 $date = '';
605 for ($i = 0; $i < $max; $i++) {
606 $c = $format[$i];
607 if (strpos('AaDlM', $c) !== FALSE) {
608 $date .= t(gmdate($c, $timestamp), array(), $langcode);
609 }
610 elseif ($c == 'F') {
611 // Special treatment for long month names: May is both an abbreviation
612 // and a full month name in English, but other languages have
613 // different abbreviations.
614 $date .= trim(t('!long-month-name ' . gmdate($c, $timestamp), array('!long-month-name' => ''), $langcode));
615 }
616 elseif (strpos('BdgGhHiIjLmnNsStTUwWYyz', $c) !== FALSE) {
617 // This condition was modified to allow the 'N' date format character.
618 $date .= gmdate($c, $timestamp);
619 }
620 elseif ($c == 'r') {
621 $date .= token_format_date($timestamp - $timezone, 'custom', 'D, d M Y H:i:s O', $timezone, $langcode);
622 }
623 elseif ($c == 'O') {
624 $date .= sprintf('%s%02d%02d', ($timezone < 0 ? '-' : '+'), abs($timezone / 3600), abs($timezone % 3600) / 60);
625 }
626 elseif ($c == 'Z') {
627 $date .= $timezone;
628 }
629 elseif ($c == '\\') {
630 $date .= $format[++$i];
631 }
632 else {
633 $date .= $c;
634 }
635 }
636
637 return $date;
638 }
639
640 /**
641 * Validate an tokens in raw text based on possible contexts.
642 *
643 * @param $value
644 * A string with the raw text containing the raw tokens, or an array of
645 * tokens from token_scan().
646 * @param $valid_types
647 * An array of token types to validage against.
648 * @param $leading
649 * Character(s) to prepend to the token key before searching for
650 * matches. Defaults to TOKEN_PREFIX.
651 * @param $trailing
652 * Character(s) to append to the token key before searching for
653 * matches. Defaults to TOKEN_SUFFIX.
654 *
655 * @return
656 * An array with the invalid tokens in their original raw forms.
657 */
658 function token_get_invalid_tokens_by_context($value, $valid_types = array(), $leading = TOKEN_PREFIX, $trailing = TOKEN_SUFFIX) {
659 if (in_array('all', $valid_types)) {
660 $valid_types = array('all');
661 }
662 else {
663 // Add the token types that are always valid in global context.
664 $valid_types[] = 'global';
665 }
666
667 $invalid_tokens = array();
668 $valid_tokens = array();
669 $value_tokens = is_string($value) ? token_scan($value, $leading, $trailing) : $value;
670
671 foreach (token_get_list($valid_types) as $category => $tokens) {
672 $valid_tokens += $tokens;
673 }
674
675 foreach ($value_tokens as $token) {
676 if (isset($valid_tokens[$token])) {
677 continue;
678 }
679 elseif (preg_match('/^(.*[_-])([^-_])+$/', $token, $matches)) {
680 // Allow tokens that do not have a direct match to tokens listed in
681 // hook_token_info() to be matched against a 'wildcard' token name.
682 if (isset($valid_tokens[$matches[1] . '?'])) {
683 // [token-name-?] wildcards.
684 continue;
685 }
686 elseif (isset($valid_tokens[$matches[1] . '????'])) {
687 // [token-name-????] wildcards.
688 continue;
689 }
690 elseif (is_numeric($matches[2]) && isset($valid_tokens[$matches[1] . 'N'])) {
691 // [token-name-N] wildcards if N is a numeric value.
692 continue;
693 }
694 }
695 $invalid_tokens[] = $token;
696 }
697
698 array_unique($invalid_tokens);
699 $invalid_tokens = token_prepare_tokens($invalid_tokens, $leading, $trailing);
700 return $invalid_tokens;
701 }
702
703 /**
704 * Build a list of all token-like patterns that appear in the text.
705 *
706 * @param $text
707 * The text to be scanned for possible tokens.
708 * @param $leading
709 * Character(s) to prepend to the token key before searching for
710 * matches. Defaults to TOKEN_PREFIX.
711 * @param $trailing
712 * Character(s) to append to the token key before searching for
713 * matches. Defaults to TOKEN_SUFFIX.
714 *
715 * @return
716 * An array of discovered tokens.
717 */
718 function token_scan($text, $leading = TOKEN_PREFIX, $trailing = TOKEN_SUFFIX) {
719 $leadingregex = preg_quote($leading, '/');
720 $trailingregex = preg_quote($trailing, '/');
721
722 $regex = '/' . $leadingregex;
723 $regex .= '([^\s';
724 if (drupal_strlen($leading) == 1) {
725 // Only add the leading string as a non-match if it is a single character.
726 $regex .= $leadingregex;
727 }
728 if (drupal_strlen($trailing) == 1) {
729 // Only add the trailing string as a non-match if it is a single character.
730 $regex .= $trailingregex;
731 }
732 $regex .= ']+)' . $trailingregex . '/x';
733
734 preg_match_all($regex, $text, $matches);
735 return $matches[1];
736 }
737
738 /**
739 * Validate a form element that should have tokens in it.
740 *
741 * Form elements that want to add this validation should have the #token_types
742 * parameter defined.
743 *
744 * For example:
745 * @code
746 * $form['my_node_text_element'] = array(
747 * '#type' => 'textfield',
748 * '#title' => t('Some text to token-ize that has a node context.'),
749 * '#default_value' => 'The title of this node is [title].',
750 * '#element_validate' => array('token_element_validate'),
751 * '#token_types' => array('node'),
752 * '#min_tokens' => 1,
753 * '#max_tokens' => 10,
754 * );
755 * @endcode
756 */
757 function token_element_validate(&$element, &$form_state) {
758 $value = isset($element['#value']) ? $element['#value'] : $element['#default_value'];
759
760 if (!drupal_strlen($value)) {
761 // Empty value needs no further validation since the element should depend
762 // on using the '#required' FAPI property.
763 return $element;
764 }
765
766 $tokens = token_scan($value);
767 $title = empty($element['#title']) ? $element['#parents'][0] : $element['#title'];
768
769 // Validate if an element must have a minimum number of tokens.
770 if (isset($element['#min_tokens']) && count($tokens) < $element['#min_tokens']) {
771 // @todo Change this error message to include the minimum number.
772 $error = format_plural($element['#min_tokens'], 'The %element-title cannot contain fewer than one token.', 'The %element-title must contain at least @count tokens.', array('%element-title' => $title));
773 form_error($element, $error);
774 }
775
776 // Validate if an element must have a maximum number of tokens.
777 if (isset($element['#max_tokens']) && count($tokens) > $element['#max_tokens']) {
778 // @todo Change this error message to include the maximum number.
779 $error = format_plural($element['#max_tokens'], 'The %element-title must contain as most one token.', 'The %element-title must contain at most @count tokens.', array('%element-title' => $title));
780 form_error($element, $error);
781 }
782
783 // Check if the field defines specific token types.
784 if (!empty($element['#token_types'])) {
785 $invalid_tokens = token_get_invalid_tokens_by_context($tokens, $element['#token_types']);
786 if ($invalid_tokens) {
787 form_error($element, t('The %element-title is using the following invalid tokens: @invalid-tokens.', array('%element-title' => $title, '@invalid-tokens' => implode(', ', $invalid_tokens))));
788 }
789 }
790
791 return $element;
792 }
793
794 /**
795 * Deprecated. Use token_element_validate() instead.
796 */
797 function token_element_validate_token_context(&$element, &$form_state) {
798 return token_element_validate($element, $form_state);
799 }
800
801 /**
802 * Find tokens that have been declared twice by different modules.
803 */
804 function token_find_duplicate_tokens() {
805 token_include();
806 $all_tokens = array();
807
808 foreach (module_implements('token_list') as $module) {
809 $module_token_list = module_invoke($module, 'token_list', 'all');
810 if (!isset($module_token_list) || !is_array($module_token_list)) {
811 // Skip modules that do not return an array as that is a valid return
812 // value.
813 continue;
814 }
815 if (in_array($module, _token_core_supported_modules())) {
816 $module = 'token';
817 }
818 foreach ($module_token_list as $type => $tokens) {
819 foreach (array_keys($tokens) as $token) {
820 $all_tokens[$type . ':' . $token][] = $module;
821 }
822 }
823 }
824
825 foreach ($all_tokens as $token => $modules) {
826 if (count($modules) < 2) {
827 unset($all_tokens[$token]);
828 }
829 }
830
831 return $all_tokens;
832 }
833
834 /**
835 * Get a translated menu link by its mlid, without access checking.
836 *
837 * This function is a copy of menu_link_load() but with its own cache and a
838 * simpler query to load the link. This also skips normal menu link access
839 * checking by using _token_menu_link_translate().
840 *
841 * @param $mlid
842 * The mlid of the menu item.
843 *
844 * @return
845 * A menu link translated for rendering.
846 *
847 * @see menu_link_load()
848 * @see _token_menu_link_translate()
849 */
850 function token_menu_link_load($mlid) {
851 static $cache = array();
852
853 if (!is_numeric($mlid)) {
854 return FALSE;
855 }
856
857 if (!isset($cache[$mlid])) {
858 $item = db_fetch_array(db_query("SELECT * FROM {menu_links} ml LEFT JOIN {menu_router} m ON m.path = ml.router_path WHERE ml.mlid = %d", $mlid));
859 if (!empty($item)) {
860 _token_menu_link_translate($item);
861 }
862 $cache[$mlid] = $item;
863 }
864
865 return $cache[$mlid];
866 }
867
868 /**
869 * Get a translated book menu link by its mlid, without access checking.
870 *
871 * This function is a copy of book_link_load() but with its own cache and a
872 * simpler query to load the link. This also skips normal menu link access
873 * checking by using _token_menu_link_translate().
874 *
875 * @param $mlid
876 * The mlid of the book menu item.
877 *
878 * @return
879 * A book menu link translated for rendering.
880 *
881 * @see book_link_load()
882 * @see _token_menu_link_translate()
883 */
884 function token_book_link_load($mlid) {
885 static $cache = array();
886
887 if (!is_numeric($mlid)) {
888 return FALSE;
889 }
890
891 if (!isset($cache[$mlid])) {
892 $item = db_fetch_array(db_query("SELECT * FROM {menu_links} ml INNER JOIN {book} b ON b.mlid = ml.mlid LEFT JOIN {menu_router} m ON m.path = ml.router_path WHERE ml.mlid = %d", $mlid));
893 if (!empty($item)) {
894 _token_menu_link_translate($item);
895 }
896 $cache[$mlid] = $item;
897 }
898
899 return $cache[$mlid];
900 }
901
902 function _token_menu_link_translate(&$item) {
903 $map = array();
904
905 if (!is_array($item['options'])) {
906 $item['options'] = unserialize($item['options']);
907 }
908
909 if ($item['external']) {
910 $item['access'] = 1;
911 $item['href'] = $item['link_path'];
912 $item['title'] = $item['link_title'];
913 $item['localized_options'] = $item['options'];
914 }
915 else {
916 $map = explode('/', $item['link_path']);
917 _menu_link_map_translate($map, $item['to_arg_functions']);
918 $item['href'] = implode('/', $map);
919
920 // Note - skip callbacks without real values for their arguments.
921 if (strpos($item['href'], '%') !== FALSE) {
922 $item['access'] = FALSE;
923 return FALSE;
924 }
925
926 $item['access'] = TRUE;
927 _menu_item_localize($item, $map, TRUE);
928 }
929
930 // Allow other customizations - e.g. adding a page-specific query string to the
931 // options array. For performance reasons we only invoke this hook if the link
932 // has the 'alter' flag set in the options array.
933 if (!empty($item['options']['alter'])) {
934 drupal_alter('translated_menu_link', $item, $map);
935 }
936
937 return $map;
938 }
939
940 /**
941 * Find all ancestors of a given menu link ID.
942 *
943 * @param $mlid
944 * A menu link ID.
945 *
946 * @return
947 * An array of menu links from token_menu_link_load() with the root link
948 * first, and the menu link with ID $mlid last.
949 */
950 function token_menu_link_get_parents_all($mlid) {
951 $parents = array();
952
953 while (!empty($mlid)) {
954 $link = token_menu_link_load($mlid);
955 array_unshift($parents, $link);
956 $mlid = $link['plid'];
957 }
958
959 return $parents;
960 }
961
962 /**
963 * Deprecated. Use the raw return value of token_menu_link_get_parents_all() instead.
964 */
965 function _menu_titles($menu_link, $nid) {
966 $titles = array();
967 $parents = token_menu_link_get_parents_all($menu_link['mlid']);
968 foreach ($parents as $mlid => $parent) {
969 $titles[] = $parent['title'];
970 }
971 return $titles;
972 }