:source AND alias = :alias AND language IN (:language, :language_none) ORDER BY language DESC, pid DESC", 0, 1, array( ':source' => $source, ':alias' => $alias, ':language' => $language, ':language_none' => LANGUAGE_NONE, ))->fetchField(); return !empty($pid); } /** * Fetches an existing URL alias given a path and optional language. * * @param $source * An internal Drupal path. * @param $language * An optional language code to look up the path in. * @return * FALSE if no alias was found or an associative array containing the * following keys: * - pid: Unique path alias identifier. * - alias: The URL alias. */ function _pathauto_existing_alias_data($source, $language = LANGUAGE_NONE) { $pid = db_query_range("SELECT pid FROM {url_alias} WHERE source = :source AND language IN (:language, :language_none) ORDER BY language DESC, pid DESC", 0, 1, array(':source' => $source, ':language' => $language, ':language_none' => LANGUAGE_NONE))->fetchField(); return path_load(array('pid' => $pid)); } /** * Clean up a string segment to be used in an URL alias. * * Performs the following possible alterations: * - Process the string through the transliteration module. * - Replace or remove punctuation with the separator character. * - Remove back-slashes. * - Replace non-ascii and non-numeric characters with the separator. * - Remove common words. * - Replace whitespace with the separator character. * - Trim duplicate, leading, and trailing separators. * - Convert to lower-case. * - Shorten to a desired length and logical position based on word boundaries. * * This function should *not* be called on URL alias or path strings because it * is assumed that they are already clean. * * @param $string * A string to clean. * @return * The cleaned string. */ function pathauto_cleanstring($string) { $output = $string; // Optionally transliterate (by running through the Transliteration module) if (variable_get('pathauto_transliterate', FALSE)) { if (module_exists('transliteration')) { $output = transliteration_get($output); } else { drupal_set_message(t('Pathauto could not transliterate the path, as the Transliteration module has been disabled.'), 'error'); } } // Replace or drop punctuation based on user settings $separator = variable_get('pathauto_separator', '-'); $punctuation = pathauto_punctuation_chars(); foreach ($punctuation as $name => $details) { $action = variable_get('pathauto_punctuation_' . $name, PATHAUTO_PUNCTUATION_REMOVE); if ($action != PATHAUTO_PUNCTUATION_DO_NOTHING) { // Slightly tricky inline if which either replaces with the separator or nothing $output = str_replace($details['value'], ($action ? $separator : ''), $output); } } // Reduce strings to letters and numbers if (variable_get('pathauto_reduce_ascii', FALSE)) { $pattern = '/[^a-zA-Z0-9\/]+/'; $output = preg_replace($pattern, $separator, $output); } // Calculate and statically cache the ignored words regex expression. $ignore_words_regex = &drupal_static('pathauto_cleanstring:ignore_words_regex'); if (!isset($ignore_words_regex)) { $ignore_words = variable_get('pathauto_ignore_words', PATHAUTO_IGNORE_WORDS); $ignore_words_regex = preg_replace(array('/^[,\s]+|[,\s]+$/', '/[,\s]+/'), array('', '\b|\b'), $ignore_words); if ($ignore_words_regex) { $ignore_words_regex = '\b' . $ignore_words_regex . '\b'; } } // Get rid of words that are on the ignore list if ($ignore_words_regex) { if (function_exists('mb_eregi_replace')) { $words_removed = mb_eregi_replace($ignore_words_regex, '', $output); } else { $words_removed = preg_replace("/$ignore_words_regex/i", '', $output); } if (drupal_strlen(trim($words_removed)) > 0) { $output = $words_removed; } } // Always replace whitespace with the separator. $output = preg_replace('/\s+/', $separator, $output); // Trim duplicates and remove trailing and leading separators. $output = _pathauto_clean_separators($output); // Optionally convert to lower case. if (variable_get('pathauto_case', PATHAUTO_CASE_LOWER)) { $output = drupal_strtolower($output); } // Shorten to a logical place based on word boundaries. $maxlength = min(variable_get('pathauto_max_component_length', 100), _pathauto_get_schema_alias_maxlength()); $output = truncate_utf8($output, $maxlength, TRUE); return $output; } /** * Trims duplicate, leading, and trailing separators from a string. * * @param $string * The string to clean path separators from. * @param $separator * The path separator to use when cleaning. * @return * The cleaned version of the string. * * @see pathauto_cleanstring() * @see pathauto_clean_alias() */ function _pathauto_clean_separators($string, $separator = NULL) { $output = $string; if (!isset($separator)) { $separator = variable_get('pathauto_separator', '-'); } // Clean duplicate or trailing separators. if (strlen($separator)) { // Escape the separator. $seppattern = preg_quote($separator, '/'); // Trim any leading or trailing separators. $output = preg_replace("/^$seppattern+|$seppattern+$/", '', $output); // Replace trailing separators around slashes. if ($separator !== '/') { $output = preg_replace("/$seppattern+\/|\/$seppattern+/", "/", $output); } // Replace multiple separators with a single one. $output = preg_replace("/$seppattern+/", $separator, $output); } return $output; } /** * Clean up an URL alias. * * Performs the following alterations: * - Trim duplicate, leading, and trailing back-slashes. * - Trim duplicate, leading, and trailing separators. * - Shorten to a desired length and logical position based on word boundaries. * * @param $alias * A string with the URL alias to clean up. * @return * The cleaned URL alias. */ function pathauto_clean_alias($alias) { $output = $alias; // Trim duplicate, leading, and trailing back-slashes. $output = _pathauto_clean_separators($output, '/'); // Trim duplicate, leading, and trailing separators. $output = _pathauto_clean_separators($output); // Shorten to a logical place based on word boundaries. $maxlength = min(variable_get('pathauto_max_length', 100), _pathauto_get_schema_alias_maxlength()); $output = truncate_utf8($output, $maxlength, TRUE); return $output; } /** * Apply patterns to create an alias. * * @param $module * The name of your module (e.g., 'node'). * @param $op * Operation being performed on the content being aliased * ('insert', 'update', 'return', or 'bulkupdate'). * @param $source * An internal Drupal path to be aliased. * @param $data * An array of keyed objects to pass to token_replace(). For simple * replacement scenarios 'node', 'user', and others are common keys, with an * accompanying node or user object being the value. Some token types, like * 'site', do not require any explicit information from $data and can be * replaced even if it is empty. * @param $type * For modules which provided pattern items in hook_pathauto(), * the relevant identifier for the specific item to be aliased * (e.g., $node->type). * @param $language * A string specify the path's language. * @return * The alias that was created. * * @see _pathauto_set_alias() * @see token_replace() */ function pathauto_create_alias($module, $op, $source, $data, $type = NULL, $language = LANGUAGE_NONE) { // Retrieve and apply the pattern for this content type. $pattern = pathauto_pattern_load_by_entity($module, $type, $language); if (empty($pattern)) { // No pattern? Do nothing (otherwise we may blow away existing aliases...) return ''; } // Special handling when updating an item which is already aliased. $existing_alias = NULL; if ($op == 'update' || $op == 'bulkupdate') { if ($existing_alias = _pathauto_existing_alias_data($source, $language)) { switch (variable_get('pathauto_update_action', PATHAUTO_UPDATE_ACTION_DELETE)) { case PATHAUTO_UPDATE_ACTION_NO_NEW: // If an alias already exists, and the update action is set to do nothing, // then gosh-darn it, do nothing. return ''; } } } // Replace any tokens in the pattern. Uses callback option to clean replacements. No sanitization. $alias = token_replace($pattern, $data, array( 'sanitize' => FALSE, 'clear' => TRUE, 'callback' => 'pathauto_clean_token_values', 'language' => (object) array('language' => $language), 'pathauto' => TRUE, )); // Check if the token replacement has not actually replaced any values. If // that is the case, then stop because we should not generate an alias. // @see token_scan() $pattern_tokens_removed = preg_replace('/\[[^\s\]:]*:[^\s\]]*\]/', '', $pattern); if ($alias === $pattern_tokens_removed) { return ''; } $alias = pathauto_clean_alias($alias); // Allow other modules to alter the alias. $context = array( 'module' => $module, 'op' => $op, 'source' => $source, 'data' => $data, 'type' => $type, 'language' => $language, 'pattern' => $pattern, ); drupal_alter('pathauto_alias', $alias, $context); // If we have arrived at an empty string, discontinue. if (!drupal_strlen($alias)) { return ''; } // If the alias already exists, generate a new, hopefully unique, variant if (_pathauto_alias_exists($alias, $source, $language)) { $maxlength = min(variable_get('pathauto_max_length', 100), _pathauto_get_schema_alias_maxlength()); $separator = variable_get('pathauto_separator', '-'); $original_alias = $alias; $i = 0; do { // Append an incrementing numeric suffix until we find a unique alias. $unique_suffix = $separator . $i; $alias = truncate_utf8($original_alias, $maxlength - drupal_strlen($unique_suffix, TRUE)) . $unique_suffix; $i++; } while (_pathauto_alias_exists($alias, $source, $language)); // Alert the user why this happened. _pathauto_verbose(t('The automatically generated alias %original_alias conflicted with an existing alias. Alias changed to %alias.', array( '%original_alias' => $original_alias, '%alias' => $alias, )), $op); } // Return the generated alias if requested. if ($op == 'return') { return $alias; } // Build the new path alias array and send it off to be created. $path = array( 'source' => $source, 'alias' => $alias, 'language' => $language, ); $path = _pathauto_set_alias($path, $existing_alias, $op); return $path; } /** * Verify if the given path is a valid menu callback. * * Taken from menu_execute_active_handler(). * * @param $path * A string containing a relative path. * @return * TRUE if the path already exists. */ function _pathauto_path_is_callback($path) { $menu = menu_get_item($path); if (isset($menu['path']) && $menu['path'] == $path) { return TRUE; } return FALSE; } /** * Private function for Pathauto to create an alias. * * @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. * @param $existing_alias * (optional) An associative array of the existing path alias. * @param $op * An optional string with the operation being performed. * * @return * The saved path from path_save() or NULL if the path was not saved. * * @see path_save() */ function _pathauto_set_alias(array $path, $existing_alias = NULL, $op = NULL) { $verbose = _pathauto_verbose(NULL, $op); // Alert users that an existing callback cannot be overridden automatically if (_pathauto_path_is_callback($path['alias'])) { if ($verbose) { _pathauto_verbose(t('Ignoring alias %alias due to existing path conflict.', array('%alias' => $path['alias']))); } return; } // Alert users if they are trying to create an alias that is the same as the internal path if ($path['source'] == $path['alias']) { if ($verbose) { _pathauto_verbose(t('Ignoring alias %alias because it is the same as the internal path.', array('%alias' => $path['alias']))); } return; } // Skip replacing the current alias with an identical alias if (empty($existing_alias) || $existing_alias['alias'] != $path['alias']) { $path += array('pathauto' => TRUE, 'original' => $existing_alias); // If there is already an alias, respect some update actions. if (!empty($existing_alias)) { switch (variable_get('pathauto_update_action', PATHAUTO_UPDATE_ACTION_DELETE)) { case PATHAUTO_UPDATE_ACTION_NO_NEW: // Do not create the alias. return; case PATHAUTO_UPDATE_ACTION_LEAVE: // Create a new alias instead of overwriting the existing by leaving // $path['pid'] empty. break; case PATHAUTO_UPDATE_ACTION_DELETE: // The delete actions should overwrite the existing alias. $path['pid'] = $existing_alias['pid']; break; } } // Save the path array. path_save($path); if ($verbose) { if (!empty($existing_alias['pid'])) { _pathauto_verbose(t('Created new alias %alias for %source, replacing %old_alias.', array('%alias' => $path['alias'], '%source' => $path['source'], '%old_alias' => $existing_alias['alias']))); } else { _pathauto_verbose(t('Created new alias %alias for %source.', array('%alias' => $path['alias'], '%source' => $path['source']))); } } return $path; } } /** * Output a helpful message if verbose output is enabled. * * Verbose output is only enabled when: * - The 'pathauto_verbose' setting is enabled. * - The current user has the 'notify of path changes' permission. * - The $op parameter is anything but 'bulkupdate' or 'return'. * * @param $message * An optional string of the verbose message to display. This string should * already be run through t(). * @param $op * An optional string with the operation being performed. * @return * TRUE if verbose output is enabled, or FALSE otherwise. */ function _pathauto_verbose($message = NULL, $op = NULL) { static $verbose; if (!isset($verbose)) { $verbose = variable_get('pathauto_verbose', FALSE) && user_access('notify of path changes'); } if (!$verbose || (isset($op) && in_array($op, array('bulkupdate', 'return')))) { return FALSE; } if ($message) { drupal_set_message($message); } return $verbose; } /** * Clean tokens so they are URL friendly. * * @param $replacements * An array of token replacements that need to be "cleaned" for use in the URL. * @param $data * An array of objects used to generate the replacements. * @param $options * An array of options used to generate the replacements. */ function pathauto_clean_token_values(&$replacements, $data = array(), $options = array()) { foreach ($replacements as $token => $value) { // Only clean non-path tokens. if (!preg_match('/(path|alias|url|url-brief)\]$/', $token)) { $replacements[$token] = pathauto_cleanstring($value); } // @todo Clean up 'tree-style' tokens that are not actually URLs. } } /** * Return an array of arrays for punctuation values. * * Returns an array of arrays for punctuation values keyed by a name, including * the value and a textual description. * Can and should be expanded to include "all" non text punctuation values. * * @return * An array of arrays for punctuation values keyed by a name, including the * value and a textual description. */ function pathauto_punctuation_chars() { static $punctuation; if (!isset($punctuation)) { $punctuation = array(); $punctuation['double_quotes'] = array('value' => '"', 'name' => t('Double quotes "')); $punctuation['quotes'] = array('value' => "'", 'name' => t("Single quotes (apostrophe) '")); $punctuation['backtick'] = array('value' => '`', 'name' => t('Back tick `')); $punctuation['comma'] = array('value' => ',', 'name' => t('Comma ,')); $punctuation['period'] = array('value' => '.', 'name' => t('Period .')); $punctuation['hyphen'] = array('value' => '-', 'name' => t('Hyphen -')); $punctuation['underscore'] = array('value' => '_', 'name' => t('Underscore _')); $punctuation['colon'] = array('value' => ':', 'name' => t('Colon :')); $punctuation['semicolon'] = array('value' => ';', 'name' => t('Semicolon ;')); $punctuation['pipe'] = array('value' => '|', 'name' => t('Pipe |')); $punctuation['left_curly'] = array('value' => '{', 'name' => t('Left curly bracket {')); $punctuation['left_square'] = array('value' => '[', 'name' => t('Left square bracket [')); $punctuation['right_curly'] = array('value' => '}', 'name' => t('Right curly bracket }')); $punctuation['right_square'] = array('value' => ']', 'name' => t('Right square bracket ]')); $punctuation['plus'] = array('value' => '+', 'name' => t('Plus +')); $punctuation['equal'] = array('value' => '=', 'name' => t('Equal =')); $punctuation['asterisk'] = array('value' => '*', 'name' => t('Asterisk *')); $punctuation['ampersand'] = array('value' => '&', 'name' => t('Ampersand &')); $punctuation['percent'] = array('value' => '%', 'name' => t('Percent %')); $punctuation['caret'] = array('value' => '^', 'name' => t('Caret ^')); $punctuation['dollar'] = array('value' => '$', 'name' => t('Dollar $')); $punctuation['hash'] = array('value' => '#', 'name' => t('Hash #')); $punctuation['at'] = array('value' => '@', 'name' => t('At @')); $punctuation['exclamation'] = array('value' => '!', 'name' => t('Exclamation !')); $punctuation['tilde'] = array('value' => '~', 'name' => t('Tilde ~')); $punctuation['left_parenthesis'] = array('value' => '(', 'name' => t('Left parenthesis (')); $punctuation['right_parenthesis'] = array('value' => ')', 'name' => t('Right parenthesis )')); $punctuation['question_mark'] = array('value' => '?', 'name' => t('Question mark ?')); $punctuation['less_than'] = array('value' => '<', 'name' => t('Less than <')); $punctuation['greater_than'] = array('value' => '>', 'name' => t('Greater than >')); $punctuation['slash'] = array('value' => '/', 'name' => t('Slash /')); $punctuation['back_slash'] = array('value' => '\\', 'name' => t('Backslash \\')); } return $punctuation; } /** * Fetch the maximum length of the {url_alias}.alias field from the schema. * * @return * An integer of the maximum URL alias length allowed by the database. */ function _pathauto_get_schema_alias_maxlength() { $maxlength = &drupal_static(__FUNCTION__); if (!isset($maxlength)) { $schema = drupal_get_schema('url_alias'); $maxlength = $schema['fields']['alias']['length']; } return $maxlength; }