diff --git a/API.txt b/API.txt new file mode 100644 index 0000000..0e845ba --- /dev/null +++ b/API.txt @@ -0,0 +1,149 @@ +This document explains how to provide "Pathauto integration" in a +module. You need this if you would like to provide additional tokens +or if your module has paths and you wish to have them automatically +aliased. The simplest integration is just to provide tokens so we +cover that first. More advanced integration requires an +implementation of hook_pathauto to provide a settings form. + +It may be helpful to review some examples of integration from the +pathauto_node.inc, pathauto_taxonomy.inc, and pathauto_user.inc files. + + +================== +1 - Providing additional tokens +================== + +If all you want is to enable tokens for your module you will simply +need to implement two functions: + + hook_token_values + hook_token_list + +See the token.module and it's API.txt for more information about this +process. + +If the token is intended to generate a path expected to contain slashes, +the token name must end in 'path', 'path-raw' or 'alias'. This indicates to +Pathauto that the slashes should not be removed from the replacement value. + +When an object is created (whether it is a node or a user or a +taxonomy term) the data that Pathauto hands to the token_values in the +$object is in a specific format. This is the format that most people +write code to handle. However, during edits and bulk updates the data +may be in a totally different format. So, if you are writing a +hook_token_values implementation to add special tokens, be sure to +test creation, edit, and bulk update cases to make sure your code will +handle it. + +================== +2 - Settings hook - To create aliases for your module +================== +You must implement hook_pathauto($op), where $op is always (at this +time) 'settings'. Return an object (NOT an array) containing the +following members, which will be used by pathauto to build a group +of settings for your module and define the variables for saving your +settings: + +module - The name of your module (e.g., 'node') +groupheader - The translated label for the settings group (e.g., + t('Content path settings') +patterndescr - The translated label for the default pattern (e.g., + t('Default path pattern (applies to all content types with blank patterns below)') +patterndefault - A translated default pattern (e.g., t('[cat]/[title].html')) +token_type - The token type (e.g. 'node', 'user') that can be used. +patternitems - For modules which need to express multiple patterns + (for example, the node module supports a separate pattern for each + content type), an array whose keys consist of identifiers for each + pattern (e.g., the content type name) and values consist of the + translated label for the pattern +bulkname - For modules which support a bulk update operation, the + translated label for the action (e.g., t('Bulk update content paths')) +bulkdescr - For modules which support a bulk update operation, a + translated, more thorough description of what the operation will do + (e.g., t('Generate aliases for all existing content items which do not already have aliases.')) + + +================== +2 - $alias = \Drupal::service('pathauto.manager')->createAlias($module, $op, $placeholders, $src, $type=NULL) +================== + +At the appropriate time (usually when a new item is being created for +which a generated alias is desired), call \Drupal::service('pathauto.manager')->createAlias() to +generate and create the alias. See the user, taxonomy, and nodeapi hook +implementations in pathauto.module for examples. + +$module - The name of your module (e.g., 'node') +$op - Operation being performed on the item ('insert', 'update', or + 'bulkupdate') +$placeholders - An array whose keys consist of the translated placeholders + which appear in patterns and values are the "clean" values to be + substituted into the pattern. Call \Drupal::service('pathauto.manager')->cleanString() on any + values which you do not know to be purely alphanumeric, to substitute + any non-alphanumerics with the user's designated separator. Note that + if the pattern has multiple slash-separated components (e.g., [term:path]), + \Drupal::service('pathauto.manager')->cleanString() should be called for each component, not the + complete string. + Example: $placeholders[t('[title]')] = \Drupal::service('pathauto.manager')->cleanString($node->title); +$src - The "real" URI of the content to be aliased (e.g., "node/$node->nid") +$type - For modules which provided patternitems in hook_autopath(), + the relevant identifier for the specific item to be aliased (e.g., + $node->type) + +\Drupal::service('pathauto.manager')->createAlias() returns the alias that was created. + + +================== +3 - Bulk update function +================== + +If a module supports bulk updating of aliases, it must provide a +function of this form, to be called by pathauto when the corresponding +checkbox is selected and the settings page submitted: + +function _pathauto_bulkupdate() + +The function should iterate over the content items controlled by the +module, calling \Drupal::service('pathauto.manager')->createAlias() for each one. It is +recommended that the function report on its success (e.g., with a +count of created aliases) via drupal_set_message(). + + +================== +4 - Bulk delete hook_path_alias_types() +================== + +For modules that create new types of pages that can be aliased with pathauto, a +hook implementation is needed to allow the user to delete them all at once. + +function hook_path_alias_types() + +This hook returns an array whose keys match the beginning of the source paths +(e.g.: "node/", "user/", etc.) and whose values describe the type of page (e.g.: +"content", "users"). Like all displayed strings, these descriptionsshould be +localized with t(). Use % to match interior pieces of a path; "user/%/track". This +is a database wildcard, so be careful. + + +================== +Modules that extend node and/or taxonomy +================== + +NOTE: this is basically not true any more. If you feel you need this file an issue. + +Many contributed Drupal modules extend the core node and taxonomy +modules. To extend pathauto patterns to support their extensions, they +may implement the pathauto_node and pathauto_taxonomy hooks. + +To do so, implement the function _pathauto_node (or _taxonomy), +accepting the arguments $op and $node (or $term). Two operations are +supported: + +$op = 'placeholders' - return an array keyed on placeholder strings +(e.g., t('[eventyyyy]')) valued with descriptions (e.g. t('The year the +event starts.')). +$op = 'values' - return an array keyed on placeholder strings, valued +with the "clean" actual value for the passed node or category (e.g., +\Drupal::service('pathauto.manager')->cleanString(date('M', $eventstart))); + +See contrib/pathauto_node_event.inc for an example of extending node +patterns. diff --git a/README.md b/README.md new file mode 100644 index 0000000..800e177 --- /dev/null +++ b/README.md @@ -0,0 +1,102 @@ +#Pathauto [![Build Status](https://travis-ci.org/md-systems/pathauto.svg?branch=8.x-1.x)](https://travis-ci.org/md-systems/pathauto) + +Please read this file and also the INSTALL.txt. +They contain answers to many common questions. +If you are developing for this module, the API.txt may be interesting. +If you are upgrading, check the CHANGELOG.txt for major changes. + +##Description + +The Pathauto module provides support functions for other modules to +automatically generate aliases based on appropriate criteria, with a +central settings path for site administrators. + +Implementations are provided for core entity types: content, taxonomy terms, +and users (including blogs and tracker pages). + +Pathauto also provides a way to delete large numbers of aliases. This feature +is available at Administer > Site building > URL aliases > Delete aliases + +##Benefits + +Besides making the page address more reflective of its content than +"node/138", it's important to know that modern search engines give +heavy weight to search terms which appear in a page's URL. By +automatically using keywords based directly on the page content in the URL, +relevant search engine hits for your page can be significantly +enhanced. + +##Installation AND Upgrades + +See the INSTALL.txt file. + +##Notices + +Pathauto just adds URL aliases to content, users, and taxonomy terms. +Because it's an alias, the standard Drupal URL (for example node/123 or +taxonomy/term/1) will still function as normal. If you have external links +to your site pointing to standard Drupal URLs, or hardcoded links in a module, +template, content or menu which point to standard Drupal URLs it will bypass +the alias set by Pathauto. + +There are reasons you might not want two URLs for the same content on your +site. If this applies to you, please note that you will need to update any +hard coded links in your content or blocks. + +If you use the "system path" (i.e. node/10) for menu items and settings like +that, Drupal will replace it with the url_alias. + +For external links, you might want to consider the Path Redirect or +Global Redirect modules, which allow you to set forwarding either per item or +across the site to your aliased URLs. + +URLs (not) Getting Replaced With Aliases: +Please bear in mind that only URLs passed through Drupal's l() or url() +functions will be replaced with their aliases during page output. If a module +or your template contains hardcoded links, such as 'href="node/$node->nid"' +those won't get replaced with their corresponding aliases. Use the +Drupal API instead: + +* 'href="'. url("node/$node->nid") .'"' or +* l("Your link title", "node/$node->nid") + +See http://api.drupal.org/api/HEAD/function/url and +http://api.drupal.org/api/HEAD/function/l for more information. + +## Disabling Pathauto for a specific content type (or taxonomy) + +When the pattern for a content type is left blank, the default pattern will be +used. But if the default pattern is also blank, Pathauto will be disabled +for that content type. + +## Bulk Updates Must be Run Multiple Times + +As of 5.x-2.x Pathauto now performs bulk updates in a manner which is more +likely to succeed on large sites. The drawback is that it needs to be run +multiple times. If you want to reduce the number of times that you need to +run Pathauto you can increase the "Maximum number of objects to alias in a +bulk update:" setting under General Settings. + +##WYSIWYG Conflicts - FCKEditor, TinyMCE, etc. + +If you use a WYSIWYG editor, please disable it for the Pathauto admin page. +Failure to do so may cause errors about "preg_replace" problems due to the

+tag being added to the "strings to replace". See http://drupal.org/node/175772 + +##Credits: + +The original module combined the functionality of Mike Ryan's autopath with +Tommy Sundstrom's path_automatic. + +Significant enhancements were contributed by jdmquin @ www.bcdems.net. + +Matt England added the tracker support. + +Other suggestions and patches contributed by the Drupal community. + +Current maintainers: + +- Dave Reid - http://www.davereid.net +- Greg Knaddison - http://www.knaddison.com +- Mike Ryan - http://mikeryan.name +- Frederik 'Freso' S. Olesen - http://freso.dk diff --git a/README.txt b/README.txt deleted file mode 100644 index fc56959..0000000 --- a/README.txt +++ /dev/null @@ -1,81 +0,0 @@ -Please read this file and also the INSTALL.txt. -They contain answers to many common questions. -If you are developing for this module, the API.txt may be interesting. -If you are upgrading, check the CHANGELOG.txt for major changes. - -** Description: -The Pathauto module provides support functions for other modules to -automatically generate aliases based on appropriate criteria, with a -central settings path for site administrators. - -Implementations are provided for core entity types: content, taxonomy terms, -and users (including blogs and forum pages). - -Pathauto also provides a way to delete large numbers of aliases. This feature -is available at Administer > Configuration > Search and metadata > URL aliases -> Delete aliases. - -** Benefits: -Besides making the page address more reflective of its content than -"node/138", it's important to know that modern search engines give -heavy weight to search terms which appear in a page's URL. By -automatically using keywords based directly on the page content in the URL, -relevant search engine hits for your page can be significantly -enhanced. - -** Installation AND Upgrades: -See the INSTALL.txt file. - -** Notices: -Pathauto just adds URL aliases to content, users, and taxonomy terms. -Because it's an alias, the standard Drupal URL (for example node/123 or -taxonomy/term/1) will still function as normal. If you have external links -to your site pointing to standard Drupal URLs, or hardcoded links in a module, -template, content or menu which point to standard Drupal URLs it will bypass -the alias set by Pathauto. - -There are reasons you might not want two URLs for the same content on your -site. If this applies to you, please note that you will need to update any -hard coded links in your content or blocks. - -If you use the "system path" (i.e. node/10) for menu items and settings like -that, Drupal will replace it with the url_alias. - -For external links, you might want to consider the Path Redirect or -Global Redirect modules, which allow you to set forwarding either per item or -across the site to your aliased URLs. - -URLs (not) Getting Replaced With Aliases: -Please bear in mind that only URLs passed through Drupal's l() or url() -functions will be replaced with their aliases during page output. If a module -or your template contains hardcoded links, such as 'href="node/$node->nid"' -those won't get replaced with their corresponding aliases. Use the -Drupal API instead: - -* 'href="'. url("node/$node->nid") .'"' or -* l("Your link title", "node/$node->nid") - -See http://api.drupal.org/api/HEAD/function/url and -http://api.drupal.org/api/HEAD/function/l for more information. - -** Disabling Pathauto for a specific content type (or taxonomy) -When the pattern for a content type is left blank, the default pattern will be -used. But if the default pattern is also blank, Pathauto will be disabled -for that content type. - -** Credits: -The original module combined the functionality of Mike Ryan's autopath with -Tommy Sundstrom's path_automatic. - -Significant enhancements were contributed by jdmquin @ www.bcdems.net. - -Matt England added the tracker support (tracker support has been removed in -recent changes). - -Other suggestions and patches contributed by the Drupal community. - -Current maintainers: - Dave Reid - http://www.davereid.net - Greg Knaddison - http://www.knaddison.com - Mike Ryan - http://mikeryan.name - Frederik 'Freso' S. Olesen - http://freso.dk diff --git a/TODO.txt b/TODO.txt new file mode 100644 index 0000000..710a830 --- /dev/null +++ b/TODO.txt @@ -0,0 +1,11 @@ +- Creating a new node with a manually set alias and not unchecking + the checkbox results in two aliases, one saved by pathauto and one by + PathItem. A possible clean way to do this might be to replace the PathItem + class with a ustom one and completely replace the logic there. Or fix core to + make sure that it sets ->pid after saving a new alias. + +- pathauto_path_alias_types() => Plugins + +- INSTALL.txt: Update + +- Update hook_help(): https://drupal.org/node/2250345 diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..0b1952a --- /dev/null +++ b/composer.json @@ -0,0 +1,6 @@ +{ + "name": "drupal/pathauto", + "description": "Provides a generic set of views plugins a mechanism for modules to automatically generate aliases for the content they manage.", + "type": "drupal-module", + "license": "GPL-2.0+" +} diff --git a/config/install/pathauto.pattern.yml b/config/install/pathauto.pattern.yml new file mode 100644 index 0000000..008417f --- /dev/null +++ b/config/install/pathauto.pattern.yml @@ -0,0 +1,12 @@ +patterns: + node: + default: 'content/[node:title]' + + taxonomy_term: + default: '[term:vocabulary]/[term:name]' + + forum: + default: '[term:vocabulary]/[term:name]' + + user: + default: 'users/[user:name]' diff --git a/config/install/pathauto.settings.yml b/config/install/pathauto.settings.yml new file mode 100644 index 0000000..bdae42e --- /dev/null +++ b/config/install/pathauto.settings.yml @@ -0,0 +1,12 @@ +punctuation: + hyphen: 1 +verbose : FALSE +separator : '-' +max_length : 100 +max_component_length: 100 +transliterate : FALSE +reduce_ascii : FALSE +ignore_words : ', in, is,that, the , this, with, ' +case : 1 +ignore_words : 'a, an, as, at, before, but, by, for, from, is, in, into, like, of, off, on, onto, per, since, than, the, this, that, to, up, via, with' +update_action : 2 diff --git a/config/install/system.action.pathauto_update_alias_node.yml b/config/install/system.action.pathauto_update_alias_node.yml new file mode 100644 index 0000000..5abe03b --- /dev/null +++ b/config/install/system.action.pathauto_update_alias_node.yml @@ -0,0 +1,9 @@ +id: pathauto_update_alias_node +label: 'Update URL-Alias' +status: true +langcode: en +type: node +plugin: pathauto_update_alias +dependencies: + module: + - node diff --git a/config/install/system.action.pathauto_update_alias_user.yml b/config/install/system.action.pathauto_update_alias_user.yml new file mode 100644 index 0000000..0e96d63 --- /dev/null +++ b/config/install/system.action.pathauto_update_alias_user.yml @@ -0,0 +1,8 @@ +id: pathauto_update_alias_user +label: 'Update URL-Alias' +status: true +type: user +plugin: pathauto_update_alias +dependencies: + module: + - user diff --git a/config/schema/pathauto.schema.yml b/config/schema/pathauto.schema.yml new file mode 100644 index 0000000..da53340 --- /dev/null +++ b/config/schema/pathauto.schema.yml @@ -0,0 +1,54 @@ +pathauto.pattern: + type: mapping + mapping: + patterns: + type: sequence + sequence: + - type: mapping + mapping: + default: + type: string + bundles: + type: sequence + sequence: + - type: mapping + mapping: + default: + type: string + languages: + type: sequence + sequence: + - type: string + + +pathauto.settings: + type: mapping + mapping: + punctuation: + type: sequence + sequence: + - type: integer + verbose: + type: boolean + separator: + type: string + max_length: + type: integer + max_component_length: + type: integer + transliterate: + type: boolean + reduce_ascii: + type: boolean + ignore_words: + type: string + case: + type: boolean + ignore_words: + type: string + update_action: + type: integer + +action.configuration.pathauto_update_alias: + type: action_configuration_default + label: 'Update URL-Alias' diff --git a/pathauto.admin.inc b/pathauto.admin.inc deleted file mode 100644 index b8679dd..0000000 --- a/pathauto.admin.inc +++ /dev/null @@ -1,437 +0,0 @@ -module; - $patterndescr = $settings->patterndescr; - $patterndefault = $settings->patterndefault; - $groupheader = $settings->groupheader; - - $form[$module] = array( - '#type' => 'fieldset', - '#title' => $groupheader, - '#collapsible' => TRUE, - '#collapsed' => FALSE, - ); - - // Prompt for the default pattern for this module - $variable = 'pathauto_' . $module . '_pattern'; - $form[$module][$variable] = array( - '#type' => 'textfield', - '#title' => $patterndescr, - '#default_value' => variable_get($variable, $patterndefault), - '#size' => 65, - '#maxlength' => 1280, - '#element_validate' => array('token_element_validate'), - '#after_build' => array('token_element_validate'), - '#token_types' => array($settings->token_type), - '#min_tokens' => 1, - '#parents' => array($variable), - ); - - // If the module supports a set of specialized patterns, set - // them up here - if (isset($settings->patternitems)) { - foreach ($settings->patternitems as $itemname => $itemlabel) { - $variable = 'pathauto_' . $module . '_' . $itemname . '_pattern'; - $form[$module][$variable] = array( - '#type' => 'textfield', - '#title' => $itemlabel, - '#default_value' => variable_get($variable, ''), - '#size' => 65, - '#maxlength' => 1280, - '#element_validate' => array('token_element_validate'), - '#after_build' => array('token_element_validate'), - '#token_types' => array($settings->token_type), - '#min_tokens' => 1, - '#parents' => array($variable), - ); - } - } - - // Display the user documentation of placeholders supported by - // this module, as a description on the last pattern - $form[$module]['token_help'] = array( - '#title' => t('Replacement patterns'), - '#type' => 'fieldset', - '#collapsible' => TRUE, - '#collapsed' => TRUE, - ); - $form[$module]['token_help']['help'] = array( - '#theme' => 'token_tree', - '#token_types' => array($settings->token_type), - ); - } - - return system_settings_form($form); -} - -/** - * Form builder; Configure the Pathauto settings. - * - * @ingroup forms - * @see system_settings_form() - */ -function pathauto_settings_form($form) { - module_load_include('inc', 'pathauto'); - - $form['pathauto_verbose'] = array( - '#type' => 'checkbox', - '#title' => t('Verbose'), - '#default_value' => variable_get('pathauto_verbose', FALSE), - '#description' => t('Display alias changes (except during bulk updates).'), - ); - - $form['pathauto_separator'] = array( - '#type' => 'textfield', - '#title' => t('Separator'), - '#size' => 1, - '#maxlength' => 1, - '#default_value' => variable_get('pathauto_separator', '-'), - '#description' => t('Character used to separate words in titles. This will replace any spaces and punctuation characters. Using a space or + character can cause unexpected results.'), - ); - - $form['pathauto_case'] = array( - '#type' => 'radios', - '#title' => t('Character case'), - '#default_value' => variable_get('pathauto_case', PATHAUTO_CASE_LOWER), - '#options' => array( - PATHAUTO_CASE_LEAVE_ASIS => t('Leave case the same as source token values.'), - PATHAUTO_CASE_LOWER => t('Change to lower case'), - ), - ); - - $max_length = _pathauto_get_schema_alias_maxlength(); - - $form['pathauto_max_length'] = array( - '#type' => 'textfield', - '#title' => t('Maximum alias length'), - '#size' => 3, - '#maxlength' => 3, - '#default_value' => variable_get('pathauto_max_length', 100), - '#min_value' => 1, - '#max_value' => $max_length, - '#description' => t('Maximum length of aliases to generate. 100 is the recommended length. @max is the maximum possible length. See Pathauto help for details.', array('@pathauto-help' => url('admin/help/pathauto'), '@max' => $max_length)), - '#element_validate' => array('_pathauto_validate_numeric_element'), - ); - $form['pathauto_max_component_length'] = array( - '#type' => 'textfield', - '#title' => t('Maximum component length'), - '#size' => 3, - '#maxlength' => 3, - '#default_value' => variable_get('pathauto_max_component_length', 100), - '#min_value' => 1, - '#max_value' => $max_length, - '#description' => t('Maximum text length of any component in the alias (e.g., [title]). 100 is the recommended length. @max is the maximum possible length. See Pathauto help for details.', array('@pathauto-help' => url('admin/help/pathauto'), '@max' => $max_length)), - '#element_validate' => array('_pathauto_validate_numeric_element'), - ); - - - $description = t('What should Pathauto do when updating an existing content item which already has an alias?'); - if (module_exists('redirect')) { - $description .= ' ' . t('The Redirect module settings affect whether a redirect is created when an alias is deleted.', array('!url' => url('admin/config/search/redirect/settings'))); - } - else { - $description .= ' ' . t('Considering installing the Redirect module to get redirects when your aliases change.', array('!url' => 'http://drupal.org/project/redirect')); - } - $form['pathauto_update_action'] = array( - '#type' => 'radios', - '#title' => t('Update action'), - '#default_value' => variable_get('pathauto_update_action', PATHAUTO_UPDATE_ACTION_DELETE), - '#options' => array( - PATHAUTO_UPDATE_ACTION_NO_NEW => t('Do nothing. Leave the old alias intact.'), - PATHAUTO_UPDATE_ACTION_LEAVE => t('Create a new alias. Leave the existing alias functioning.'), - PATHAUTO_UPDATE_ACTION_DELETE => t('Create a new alias. Delete the old alias.'), - ), - '#description' => $description, - ); - - $form['pathauto_transliterate'] = array( - '#type' => 'checkbox', - '#title' => t('Transliterate prior to creating alias'), - '#default_value' => variable_get('pathauto_transliterate', FALSE) && module_exists('transliteration'), - '#description' => t('When a pattern includes certain characters (such as those with accents) should Pathauto attempt to transliterate them into the US-ASCII alphabet? Transliteration is handled by the Transliteration module.'), - '#access' => module_exists('transliteration'), - ); - - $form['pathauto_reduce_ascii'] = array( - '#type' => 'checkbox', - '#title' => t('Reduce strings to letters and numbers'), - '#default_value' => variable_get('pathauto_reduce_ascii', FALSE), - '#description' => t('Filters the new alias to only letters and numbers found in the ASCII-96 set.'), - ); - - $form['pathauto_ignore_words'] = array( - '#type' => 'textarea', - '#title' => t('Strings to Remove'), - '#default_value' => variable_get('pathauto_ignore_words', PATHAUTO_IGNORE_WORDS), - '#description' => t('Words to strip out of the URL alias, separated by commas. Do not use this to remove punctuation.'), - '#wysiwyg' => FALSE, - ); - - $form['punctuation'] = array( - '#type' => 'fieldset', - '#title' => t('Punctuation'), - '#collapsible' => TRUE, - '#collapsed' => TRUE, - ); - - $punctuation = pathauto_punctuation_chars(); - foreach ($punctuation as $name => $details) { - $details['default'] = PATHAUTO_PUNCTUATION_REMOVE; - if ($details['value'] == variable_get('pathauto_separator', '-')) { - $details['default'] = PATHAUTO_PUNCTUATION_REPLACE; - } - $form['punctuation']['pathauto_punctuation_' . $name] = array( - '#type' => 'select', - '#title' => $details['name'] . ' (' . check_plain($details['value']) . ')', - '#default_value' => variable_get('pathauto_punctuation_' . $name, $details['default']), - '#options' => array( - PATHAUTO_PUNCTUATION_REMOVE => t('Remove'), - PATHAUTO_PUNCTUATION_REPLACE => t('Replace by separator'), - PATHAUTO_PUNCTUATION_DO_NOTHING => t('No action (do not replace)'), - ), - ); - } - - return system_settings_form($form); -} - -/** - * Validate a form element that should have an numeric value. - */ -function _pathauto_validate_numeric_element($element, &$form_state) { - $value = $element['#value']; - - if (!is_numeric($value)) { - form_error($element, t('The field %name is not a valid number.', array('%name' => $element['#title']))); - } - elseif (isset($element['#max_value']) && $value > $element['#max_value']) { - form_error($element, t('The field %name cannot be greater than @max.', array('%name' => $element['#title'], '@max' => $element['#max_value']))); - } - elseif (isset($element['#min_value']) && $value < $element['#min_value']) { - form_error($element, t('The field %name cannot be less than @min.', array('%name' => $element['#title'], '@min' => $element['#min_value']))); - } -} - -/** - * Validate pathauto_settings_form form submissions. - */ -function pathauto_settings_form_validate($form, &$form_state) { - module_load_include('inc', 'pathauto'); - - // Perform a basic check for HTML characters in the strings to remove field. - if (strip_tags($form_state['values']['pathauto_ignore_words']) != $form_state['values']['pathauto_ignore_words']) { - form_set_error('pathauto_ignore_words', t('The Strings to remove field must not contain HTML. Make sure to disable any WYSIWYG editors for this field.')); - } - - // Validate that the separator is not set to be removed per http://drupal.org/node/184119 - // This isn't really all that bad so warn, but still allow them to save the value. - $separator = $form_state['values']['pathauto_separator']; - $punctuation = pathauto_punctuation_chars(); - foreach ($punctuation as $name => $details) { - if ($details['value'] == $separator) { - $action = $form_state['values']['pathauto_punctuation_' . $name]; - if ($action == PATHAUTO_PUNCTUATION_REMOVE) { - drupal_set_message(t('You have configured the @name to be the separator and to be removed when encountered in strings. This can cause problems with your patterns and especially with the term:path token. You should probably set the action for @name to be "replace by separator".', array('@name' => $details['name'])), 'error'); - } - } - } -} - -/** - * Form contructor for path alias bulk update form. - * - * @see pathauto_bulk_update_form_submit() - * @ingroup forms - */ -function pathauto_bulk_update_form() { - $form['#update_callbacks'] = array(); - - $form['update'] = array( - '#type' => 'checkboxes', - '#title' => t('Select the types of un-aliased paths for which to generate URL aliases'), - '#options' => array(), - '#default_value' => array(), - ); - - $pathauto_settings = module_invoke_all('pathauto', 'settings'); - foreach ($pathauto_settings as $settings) { - if (!empty($settings->batch_update_callback)) { - $form['#update_callbacks'][$settings->batch_update_callback] = $settings; - $form['update']['#options'][$settings->batch_update_callback] = $settings->groupheader; - } - } - - $form['actions']['#type'] = 'actions'; - $form['actions']['submit'] = array( - '#type' => 'submit', - '#value' => t('Update'), - ); - - return $form; -} - -/** - * Form submit handler for path alias bulk update form. - * - * @see pathauto_batch_update_form() - * @see pathauto_bulk_update_batch_finished() - */ -function pathauto_bulk_update_form_submit($form, &$form_state) { - $batch = array( - 'title' => t('Bulk updating URL aliases'), - 'operations' => array( - array('pathauto_bulk_update_batch_start', array()), - ), - 'finished' => 'pathauto_bulk_update_batch_finished', - 'file' => drupal_get_path('module', 'pathauto') . '/pathauto.admin.inc', - ); - - foreach ($form_state['values']['update'] as $callback) { - if (!empty($callback)) { - $settings = $form['#update_callbacks'][$callback]; - if (!empty($settings->batch_file)) { - $batch['operations'][] = array('pathauto_bulk_update_batch_process', array($callback, $settings)); - } - else { - $batch['operations'][] = array($callback, array()); - } - } - } - - batch_set($batch); -} - -/** - * Batch callback; count the current number of URL aliases for comparison later. - */ -function pathauto_bulk_update_batch_start(&$context) { - $context['results']['count_before'] = db_select('url_alias')->countQuery()->execute()->fetchField(); -} - -/** - * Common batch processing callback for all operations. - * - * Required to load our include the proper batch file. - */ -function pathauto_bulk_update_batch_process($callback, $settings, &$context) { - if (!empty($settings->batch_file)) { - require_once DRUPAL_ROOT . '/' . $settings->batch_file; - } - return $callback($context); -} - -/** - * Batch finished callback. - */ -function pathauto_bulk_update_batch_finished($success, $results, $operations) { - if ($success) { - // Count the current number of URL aliases after the batch is completed - // and compare to the count before the batch started. - $results['count_after'] = db_select('url_alias')->countQuery()->execute()->fetchField(); - $results['count_changed'] = max($results['count_after'] - $results['count_before'], 0); - if ($results['count_changed']) { - drupal_set_message(format_plural($results['count_changed'], 'Generated 1 URL alias.', 'Generated @count URL aliases.')); - } - else { - drupal_set_message(t('No new URL aliases to generate.')); - } - } - else { - $error_operation = reset($operations); - drupal_set_message(t('An error occurred while processing @operation with arguments : @args', array('@operation' => $error_operation[0], '@args' => print_r($error_operation[0], TRUE)))); - } -} - -/** - * Menu callback; select certain alias types to delete. - */ -function pathauto_admin_delete() { - /* TODO: - 1) all - DONE - 2) all node aliases - DONE - 4) all user aliases - DONE - 5) all taxonomy aliases - DONE - 6) by node type - 7) by taxonomy vocabulary - 8) no longer existing aliases (see http://drupal.org/node/128366 ) - 9) where src like 'pattern' - DON'T DO - 10) where dst like 'pattern' - DON'T DO - */ - - $form['delete'] = array( - '#type' => 'fieldset', - '#title' => t('Choose aliases to delete'), - '#collapsible' => FALSE, - '#collapsed' => FALSE, - ); - - // First we do the "all" case - $total_count = db_query('SELECT count(1) FROM {url_alias}')->fetchField(); - $form['delete']['all_aliases'] = array( - '#type' => 'checkbox', - '#title' => t('All aliases'), - '#default_value' => FALSE, - '#description' => t('Delete all aliases. Number of aliases which will be deleted: %count.', array('%count' => $total_count)), - ); - - // Next, iterate over an array of objects/alias types which can be deleted and provide checkboxes - $objects = module_invoke_all('path_alias_types'); - foreach ($objects as $internal_name => $label) { - $count = db_query("SELECT count(1) FROM {url_alias} WHERE source LIKE :src", array(':src' => "$internal_name%"))->fetchField(); - $form['delete'][$internal_name] = array( - '#type' => 'checkbox', - '#title' => $label, // This label is sent through t() in the hard coded function where it is defined - '#default_value' => FALSE, - '#description' => t('Delete aliases for all @label. Number of aliases which will be deleted: %count.', array('@label' => $label, '%count' => $count)), - ); - } - - // Warn them and give a button that shows we mean business - $form['warning'] = array('#value' => '

' . t('Note: there is no confirmation. Be sure of your action before clicking the "Delete aliases now!" button.
You may want to make a backup of the database and/or the url_alias table prior to using this feature.') . '

'); - $form['buttons']['submit'] = array( - '#type' => 'submit', - '#value' => t('Delete aliases now!'), - ); - - return $form; -} - -/** - * Process pathauto_admin_delete form submissions. - */ -function pathauto_admin_delete_submit($form, &$form_state) { - foreach ($form_state['values'] as $key => $value) { - if ($value) { - if ($key === 'all_aliases') { - db_delete('url_alias') - ->execute(); - drupal_set_message(t('All of your path aliases have been deleted.')); - } - $objects = module_invoke_all('path_alias_types'); - if (array_key_exists($key, $objects)) { - db_delete('url_alias') - ->condition('source', db_like($key) . '%', 'LIKE') - ->execute(); - drupal_set_message(t('All of your %type path aliases have been deleted.', array('%type' => $objects[$key]))); - } - } - } - $form_state['redirect'] = 'admin/config/search/path/delete_bulk'; -} diff --git a/pathauto.api.php b/pathauto.api.php index 7d42a83..26ec374 100644 --- a/pathauto.api.php +++ b/pathauto.api.php @@ -1,119 +1,18 @@ $type) { - $settings['patternitems'][$file_type] = t('Pattern for all @file_type paths.', array('@file_type' => $type->label)); - } - return (object) $settings; - - default: - break; - } } /** @@ -129,7 +28,7 @@ function hook_pathauto($op) { * @param string $langcode * The language code for the alias (e.g. 'en'). * - * @return bool + * @return * TRUE if $alias conflicts with an existing, reserved path, or FALSE/NULL if * it does not match any reserved paths. * @@ -158,9 +57,9 @@ function hook_pathauto_is_alias_reserved($alias, $source, $langcode) { * - 'language': A string of the language code for the alias (e.g. 'en'). * This can be altered by reference. */ -function hook_pathauto_pattern_alter(&$pattern, array $context) { +function hook_pathauto_pattern_alter(&$pattern, array &$context) { // Switch out any [node:created:*] tokens with [node:updated:*] on update. - if ($context['module'] == 'node' && ($context['op'] == 'update')) { + if ($module == 'node' && ($context['op'] == 'update')) { $pattern = preg_replace('/\[node:created(\:[^]]*)?\]/', '[node:updated$1]', $pattern); } } @@ -188,7 +87,7 @@ function hook_pathauto_alias_alter(&$alias, array &$context) { $alias .= '.html'; // Force all aliases to be saved as language neutral. - $context['language'] = LANGUAGE_NONE; + $context['language'] = Language::LANGCODE_NOT_SPECIFIED; } /** diff --git a/pathauto.inc b/pathauto.inc deleted file mode 100644 index 63b9244..0000000 --- a/pathauto.inc +++ /dev/null @@ -1,702 +0,0 @@ - $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: - * - Remove all HTML tags. - * - 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. - * @param array $options - * (optional) A keyed array of settings and flags to control the Pathauto - * clean string replacement process. Supported options are: - * - langcode: A language code to be used when translating strings. - * - * @return - * The cleaned string. - */ -function pathauto_cleanstring($string, array $options = array()) { - // Use the advanced drupal_static() pattern, since this is called very often. - static $drupal_static_fast; - if (!isset($drupal_static_fast)) { - $drupal_static_fast['cache'] = &drupal_static(__FUNCTION__); - } - $cache = &$drupal_static_fast['cache']; - - // Generate and cache variables used in this function so that on the second - // call to pathauto_cleanstring() we focus on processing. - if (!isset($cache)) { - $cache = array( - 'separator' => variable_get('pathauto_separator', '-'), - 'strings' => array(), - 'transliterate' => variable_get('pathauto_transliterate', FALSE) && module_exists('transliteration'), - 'punctuation' => array(), - 'reduce_ascii' => (bool) variable_get('pathauto_reduce_ascii', FALSE), - 'ignore_words_regex' => FALSE, - 'lowercase' => (bool) variable_get('pathauto_case', PATHAUTO_CASE_LOWER), - 'maxlength' => min(variable_get('pathauto_max_component_length', 100), _pathauto_get_schema_alias_maxlength()), - ); - - // Generate and cache the punctuation replacements for strtr(). - $punctuation = pathauto_punctuation_chars(); - foreach ($punctuation as $name => $details) { - $action = variable_get('pathauto_punctuation_' . $name, PATHAUTO_PUNCTUATION_REMOVE); - switch ($action) { - case PATHAUTO_PUNCTUATION_REMOVE: - $cache['punctuation'][$details['value']] = ''; - break; - case PATHAUTO_PUNCTUATION_REPLACE: - $cache['punctuation'][$details['value']] = $cache['separator']; - break; - case PATHAUTO_PUNCTUATION_DO_NOTHING: - // Literally do nothing. - break; - } - } - - // Generate and cache the ignored words regular expression. - $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) { - $cache['ignore_words_regex'] = '\b' . $ignore_words_regex . '\b'; - if (function_exists('mb_eregi_replace')) { - $cache['ignore_words_callback'] = 'mb_eregi_replace'; - } - else { - $cache['ignore_words_callback'] = 'preg_replace'; - $cache['ignore_words_regex'] = '/' . $cache['ignore_words_regex'] . '/i'; - } - } - } - - // Empty strings do not need any proccessing. - if ($string === '' || $string === NULL) { - return ''; - } - - $langcode = NULL; - if (!empty($options['language']->language)) { - $langcode = $options['language']->language; - } - elseif (!empty($options['langcode'])) { - $langcode = $options['langcode']; - } - - // Check if the string has already been processed, and if so return the - // cached result. - if (isset($cache['strings'][$langcode][$string])) { - return $cache['strings'][$langcode][$string]; - } - - // Remove all HTML tags from the string. - $output = strip_tags(decode_entities($string)); - - // Optionally transliterate (by running through the Transliteration module) - if ($cache['transliterate']) { - // If the reduce strings to letters and numbers is enabled, don't bother - // replacing unknown characters with a question mark. Use an empty string - // instead. - $output = transliteration_get($output, $cache['reduce_ascii'] ? '' : '?', $langcode); - } - - // Replace or drop punctuation based on user settings - $output = strtr($output, $cache['punctuation']); - - // Reduce strings to letters and numbers - if ($cache['reduce_ascii']) { - $output = preg_replace('/[^a-zA-Z0-9\/]+/', $cache['separator'], $output); - } - - // Get rid of words that are on the ignore list - if ($cache['ignore_words_regex']) { - $words_removed = $cache['ignore_words_callback']($cache['ignore_words_regex'], '', $output); - if (drupal_strlen(trim($words_removed)) > 0) { - $output = $words_removed; - } - } - - // Always replace whitespace with the separator. - $output = preg_replace('/\s+/', $cache['separator'], $output); - - // Trim duplicates and remove trailing and leading separators. - $output = _pathauto_clean_separators($output, $cache['separator']); - - // Optionally convert to lower case. - if ($cache['lowercase']) { - $output = drupal_strtolower($output); - } - - // Shorten to a logical place based on word boundaries. - $output = truncate_utf8($output, $cache['maxlength'], TRUE); - - // Cache this result in the static array. - $cache['strings'][$langcode][$string] = $output; - - 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) { - static $default_separator; - - if (!isset($separator)) { - if (!isset($default_separator)) { - $default_separator = variable_get('pathauto_separator', '-'); - } - $separator = $default_separator; - } - - $output = $string; - - if (strlen($separator)) { - // Trim any leading or trailing separators. - $output = trim($output, $separator); - - // Escape the separator for use in regular expressions. - $seppattern = preg_quote($separator, '/'); - - // Replace multiple separators with a single one. - $output = preg_replace("/$seppattern+/", $separator, $output); - - // Replace trailing separators around slashes. - if ($separator !== '/') { - $output = preg_replace("/\/+$seppattern\/+|$seppattern\/+|\/+$seppattern/", "/", $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) { - $cache = &drupal_static(__FUNCTION__); - - if (!isset($cache)) { - $cache = array( - 'maxlength' => min(variable_get('pathauto_max_length', 100), _pathauto_get_schema_alias_maxlength()), - ); - } - - $output = $alias; - - // Trim duplicate, leading, and trailing separators. Do this before cleaning - // backslashes since a pattern like "[token1]/[token2]-[token3]/[token4]" - // could end up like "value1/-/value2" and if backslashes were cleaned first - // this would result in a duplicate blackslash. - $output = _pathauto_clean_separators($output); - - // Trim duplicate, leading, and trailing backslashes. - $output = _pathauto_clean_separators($output, '/'); - - // Shorten to a logical place based on word boundaries. - $output = truncate_utf8($output, $cache['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 array|null|false - * The alias array that was created, NULL if an empty alias was generated, or - * FALSE if the alias generation was not possible. - * - * @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); - - // Allow other modules to alter the pattern. - $context = array( - 'module' => $module, - 'op' => $op, - 'source' => $source, - 'data' => $data, - 'type' => $type, - 'language' => &$language, - ); - drupal_alter('pathauto_pattern', $pattern, $context); - - if (empty($pattern)) { - // No pattern? Do nothing (otherwise we may blow away existing aliases...) - return FALSE; - } - - // Special handling when updating an item which is already aliased. - $existing_alias = NULL; - if ($op != 'insert') { - 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 FALSE; - } - } - } - - // 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['source'] = &$source; - $context['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. - $original_alias = $alias; - pathauto_alias_uniquify($alias, $source, $language); - if ($original_alias != $alias) { - // 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; -} - -/** - * Check to ensure a path alias is unique and add suffix variants if necessary. - * - * Given an alias 'content/test' if a path alias with the exact alias already - * exists, the function will change the alias to 'content/test-0' and will - * increase the number suffix until it finds a unique alias. - * - * @param $alias - * A string with the alias. Can be altered by reference. - * @param $source - * A string with the path source. - * @param $langcode - * A string with a language code. - */ -function pathauto_alias_uniquify(&$alias, $source, $langcode) { - if (!pathauto_is_alias_reserved($alias, $source, $langcode)) { - return; - } - - // If the alias already exists, generate a new, hopefully unique, variant - $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_is_alias_reserved($alias, $source, $langcode)); -} - -/** - * 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) { - // We need to use a try/catch here because of a core bug which will throw an - // exception if $path is something like 'node/foo/bar'. - // @todo Remove when http://drupal.org/node/1003788 is fixed in core. - try { - $menu = menu_get_item($path); - } - catch (Exception $e) { - return FALSE; - } - - if (isset($menu['path']) && $menu['path'] == $path) { - return TRUE; - } - elseif (is_file(DRUPAL_ROOT . '/' . $path) || is_dir(DRUPAL_ROOT . '/' . $path)) { - // Do not allow existing files or directories to get assigned an automatic - // alias. Note that we do not need to use is_link() to check for symbolic - // links since this returns TRUE for either is_file() or is_dir() already. - 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 FALSE 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 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 FALSE; - } - - // 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 FALSE; - 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, $options); - } - } -} - -/** - * 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() { - $punctuation = &drupal_static(__FUNCTION__); - - if (!isset($punctuation)) { - $cid = 'pathauto:punctuation:' . $GLOBALS['language']->language; - if ($cache = cache_get($cid)) { - $punctuation = $cache->data; - } - else { - $punctuation = array(); - $punctuation['double_quotes'] = array('value' => '"', 'name' => t('Double quotation marks')); - $punctuation['quotes'] = array('value' => '\'', 'name' => t("Single quotation marks (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('Vertical bar (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 sign')); - $punctuation['equal'] = array('value' => '=', 'name' => t('Equal sign')); - $punctuation['asterisk'] = array('value' => '*', 'name' => t('Asterisk')); - $punctuation['ampersand'] = array('value' => '&', 'name' => t('Ampersand')); - $punctuation['percent'] = array('value' => '%', 'name' => t('Percent sign')); - $punctuation['caret'] = array('value' => '^', 'name' => t('Caret')); - $punctuation['dollar'] = array('value' => '$', 'name' => t('Dollar sign')); - $punctuation['hash'] = array('value' => '#', 'name' => t('Number sign (pound sign, hash)')); - $punctuation['at'] = array('value' => '@', 'name' => t('At sign')); - $punctuation['exclamation'] = array('value' => '!', 'name' => t('Exclamation mark')); - $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 sign')); - $punctuation['greater_than'] = array('value' => '>', 'name' => t('Greater-than sign')); - $punctuation['slash'] = array('value' => '/', 'name' => t('Slash')); - $punctuation['back_slash'] = array('value' => '\\', 'name' => t('Backslash')); - - // Allow modules to alter the punctuation list and cache the result. - drupal_alter('pathauto_punctuation_chars', $punctuation); - cache_set($cid, $punctuation); - } - } - - 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; -} diff --git a/pathauto.info b/pathauto.info deleted file mode 100644 index fbfc18a..0000000 --- a/pathauto.info +++ /dev/null @@ -1,8 +0,0 @@ -name = Pathauto -description = Provides a mechanism for modules to automatically generate aliases for the content they manage. -dependencies[] = path -dependencies[] = token -core = 7.x -files[] = pathauto.test -configure = admin/config/search/path/patterns -recommends[] = redirect diff --git a/pathauto.info.yml b/pathauto.info.yml new file mode 100644 index 0000000..2532cbb --- /dev/null +++ b/pathauto.info.yml @@ -0,0 +1,13 @@ +name : 'Pathauto' +description : 'Provides a mechanism for modules to automatically generate aliases for the content they manage.' +core: 8.x +type: module + +dependencies: +- path +- token + +configure: pathauto.patterns.form + +recommends: +- redirect \ No newline at end of file diff --git a/pathauto.install b/pathauto.install index cd5211c..ffc1490 100644 --- a/pathauto.install +++ b/pathauto.install @@ -11,172 +11,7 @@ * Implements hook_install(). */ function pathauto_install() { - // Set some default variables necessary for the module to perform. - variable_set('pathauto_node_pattern', 'content/[node:title]'); - variable_set('pathauto_taxonomy_term_pattern', '[term:vocabulary]/[term:name]'); - variable_set('pathauto_forum_pattern', '[term:vocabulary]/[term:name]'); - variable_set('pathauto_user_pattern', 'users/[user:name]'); - variable_set('pathauto_blog_pattern', 'blogs/[user:name]'); - - // Set the default separator character to replace instead of remove (default). - variable_set('pathauto_punctuation_hyphen', 1); - // Set the weight to 1 - db_update('system') - ->fields(array('weight' => 1)) - ->condition('type', 'module') - ->condition('name', 'pathauto') - ->execute(); -} - -/** - * Implements hook_uninstall(). - */ -function pathauto_uninstall() { - // Delete all the pathauto variables and then clear the variable cache. - db_query("DELETE FROM {variable} WHERE name LIKE 'pathauto_%'"); - cache_clear_all('variables', 'cache'); -} - -/** - * Remove the unsupported user/%/contact and user/%/tracker pattern variables. - */ -function pathauto_update_6200() { - variable_del('pathauto_contact_bulkupdate'); - variable_del('pathauto_contact_pattern'); - variable_del('pathauto_contact_supportsfeeds'); - variable_del('pathauto_contact_applytofeeds'); - variable_del('pathauto_tracker_bulkupdate'); - variable_del('pathauto_tracker_pattern'); - variable_del('pathauto_tracker_supportsfeeds'); - variable_del('pathauto_tracker_applytofeeds'); -} - -/** - * Empty update since it is handled by pathauto_update_7000(). - */ -function pathauto_update_6201() { -} - -/** - * Empty update since it is handled by pathauto_update_7004(). - */ -function pathauto_update_6202() { -} - -/** - * Remove obsolete variables since batch API is now used. - */ -function pathauto_update_7000() { - variable_del('pathauto_max_bulk_update'); - variable_del('pathauto_node_bulkupdate'); - variable_del('pathauto_taxonomy_bulkupdate'); - variable_del('pathauto_forum_bulkupdate'); - variable_del('pathauto_user_bulkupdate'); - variable_del('pathauto_blog_bulkupdate'); - variable_del('pathauto_modulelist'); - variable_del('pathauto_indexaliases'); - variable_del('pathauto_indexaliases_bulkupdate'); -} + module_set_weight('pathauto', 1); -/** - * Empty update since feed paths are no longer supported. - */ -function pathauto_update_7001() { -} - -/** - * Update pathauto_taxonomy_[vid]_pattern variables to pathauto_taxonomy_[machinename]_pattern. - */ -function pathauto_update_7002() { - if (module_exists('taxonomy')) { - $vocabularies = taxonomy_get_vocabularies(); - foreach ($vocabularies as $vid => $vocabulary) { - if ($vid == variable_get('forum_nav_vocabulary', '')) { - // Skip the forum vocabulary. - continue; - } - if ($pattern = variable_get('pathauto_taxonomy_' . $vid . '_pattern', '')) { - variable_set('pathauto_taxonomy_' . $vocabulary->machine_name . '_pattern', $pattern); - } - variable_del('pathauto_taxonomy_' . $vid . '_pattern'); - } - } -} - -/** - * Rename 'taxonomy' variables to use the entity type 'taxonomy_term'. - */ -function pathauto_update_7003() { - $variables = db_select('variable', 'v') - ->fields('v', array('name')) - ->condition(db_and() - ->condition('name', db_like("pathauto_taxonomy_") . '%', 'LIKE') - ->condition('name', db_like("pathauto_taxonomy_term_") . '%', 'NOT LIKE') - ) - ->execute() - ->fetchCol(); - foreach ($variables as $variable) { - $value = variable_get($variable); - variable_del($variable); - $variable = strtr($variable, array('pathauto_taxonomy_' => 'pathauto_taxonomy_term_')); - variable_set($variable, $value); - } -} - -/** - * Remove obsolete variables for removed feed handling. - */ -function pathauto_update_7004() { - variable_del('pathauto_node_supportsfeeds'); - variable_del('pathauto_node_applytofeeds'); - variable_del('pathauto_taxonomy_supportsfeeds'); - variable_del('pathauto_taxonomy_applytofeeds'); - variable_del('pathauto_forum_supportsfeeds'); - variable_del('pathauto_forum_applytofeeds'); - variable_del('pathauto_user_supportsfeeds'); - variable_del('pathauto_user_applytofeeds'); - variable_del('pathauto_blog_supportsfeeds'); - variable_del('pathauto_blog_applytofeeds'); -} - -/** - * Fix original incorrect tokens in taxonomy and forum patterns. - */ -function pathauto_update_7005() { - $replacements = array( - '[vocabulary:name]' => '[term:vocabulary]', - '[vocabulary:' => '[term:vocabulary:', - '[term:catpath]' => '[term:name]', - '[term:path]' => '[term:name]', - ); - $variables = db_select('variable', 'v') - ->fields('v', array('name')) - ->condition(db_or() - ->condition('name', db_like("pathauto_taxonomy_term_") . '%' . db_like('pattern'), 'LIKE') - ->condition('name', db_like("pathauto_forum_") . '%' . db_like('pattern'), 'LIKE') - ) - ->execute() - ->fetchCol(); - foreach ($variables as $variable) { - if ($pattern = variable_get($variable)) { - $pattern = strtr($pattern, $replacements); - variable_set($variable, $pattern); - } - } - - return 'Your Pathauto taxonomy and forum patterns have been corrected. You may wish to regenerate your taxonomy and forum term URL aliases.'; -} - -/** - * Build a list of Drupal 6 tokens and their Drupal 7 token names. - */ -function _pathauto_upgrade_token_list() { - $tokens = array( - //'catpath' => 'node:term-lowest:parent:path][node:term-lowest', - //'catalias' => 'node:term-lowest:path', - //'termpath' => 'term:parent:path][term:name', - //'termalias' => 'term:url:alias', - //'bookpathalias' => 'node:book:parent:path', - ); } diff --git a/pathauto.libraries.yml b/pathauto.libraries.yml new file mode 100755 index 0000000..3734b17 --- /dev/null +++ b/pathauto.libraries.yml @@ -0,0 +1,8 @@ +widget: + version: 1.0 + js: + pathauto.js: {} + dependencies: + - core/jquery + - core/drupal + - core/drupalSettings diff --git a/pathauto.links.task.yml b/pathauto.links.task.yml new file mode 100644 index 0000000..2ce9963 --- /dev/null +++ b/pathauto.links.task.yml @@ -0,0 +1,23 @@ +pathauto.patterns.form: + route_name: pathauto.patterns.form + base_route: path.admin_overview + title: 'Patterns' + weight: 10 + +pathauto.settings.form: + route_name: pathauto.settings.form + base_route: path.admin_overview + title: 'Settings' + weight: 20 + +pathauto.bulk.update.form: + route_name: pathauto.bulk.update.form + base_route: path.admin_overview + title: 'Bulk generate' + weight: 30 + +pathauto.admin.delete: + route_name: pathauto.admin.delete + base_route: path.admin_overview + title: 'Delete aliases' + weight: 40 diff --git a/pathauto.module b/pathauto.module index 9bb132d..a640498 100644 --- a/pathauto.module +++ b/pathauto.module @@ -1,5 +1,4 @@ 'pathauto')); } + /** * Implements hook_module_implements_alter(). * * Adds pathauto support for core modules. */ function pathauto_module_implements_alter(&$implementations, $hook) { - if (in_array($hook, array('pathauto', 'path_alias_types'))) { - $modules = array('node', 'taxonomy', 'user', 'forum', 'blog'); + if (in_array($hook, array('pathauto'))) { + $modules = array('node', 'taxonomy', 'user', 'forum'); foreach ($modules as $module) { - if (module_exists($module)) { + if (\Drupal::moduleHandler()->moduleExists($module)) { $implementations[$module] = TRUE; } } @@ -59,121 +69,25 @@ function pathauto_module_implements_alter(&$implementations, $hook) { /** * Implements hook_help(). */ -function pathauto_help($path, $arg) { - switch ($path) { - case 'admin/help#pathauto': - module_load_include('inc', 'pathauto'); +function pathauto_help($route_name, RouteMatchInterface $route_match) { + switch ($route_name) { + case 'help.page.pathauto': $output = '

' . t('About') . '

'; $output .= '

' . t('Provides a mechanism for modules to automatically generate aliases for the content they manage.') . '

'; $output .= '

' . t('Settings') . '

'; $output .= '
'; $output .= '
' . t('Maximum alias and component length') . '
'; - $output .= '
' . t('The maximum alias length and maximum component length values default to 100 and have a limit of @max from Pathauto. This length is limited by the length of the "alias" column of the url_alias database table. The default database schema for this column is @max. If you set a length that is equal to that of the one set in the "alias" column it will cause problems in situations where the system needs to append additional words to the aliased URL. You should enter a value that is the length of the "alias" column minus the length of any strings that might get added to the end of the URL. The length of strings that might get added to the end of your URLs depends on which modules you have enabled and on your Pathauto settings. The recommended and default value is 100.', array('@max' => _pathauto_get_schema_alias_maxlength())) . '
'; + $output .= '
' . t('The maximum alias length and maximum component length values default to 100 and have a limit of @max from Pathauto. This length is limited by the length of the "alias" column of the url_alias database table. The default database schema for this column is @max. If you set a length that is equal to that of the one set in the "alias" column it will cause problems in situations where the system needs to append additional words to the aliased URL. You should enter a value that is the length of the "alias" column minus the length of any strings that might get added to the end of the URL. The length of strings that might get added to the end of your URLs depends on which modules you have enabled and on your Pathauto settings. The recommended and default value is 100.', array('@max' => \Drupal::service('pathauto.alias_storage_helper')->getAliasSchemaMaxlength())) . '
'; $output .= '
'; return $output; - case 'admin/config/search/path/update_bulk': + + case 'pathauto.bulk.update.form': $output = '

' . t('Bulk generation will only generate URL aliases for items that currently have no aliases. This is typically used when installing Pathauto on a site that has existing un-aliased content that needs to be aliased in bulk.') . '

'; return $output; } } /** - * Implements hook_permission(). - */ -function pathauto_permission() { - return array( - 'administer pathauto' => array( - 'title' => t('Administer pathauto'), - 'description' => t('Allows a user to configure patterns for automated aliases and bulk delete URL-aliases.'), - ), - 'notify of path changes' => array( - 'title' => t('Notify of Path Changes'), - 'description' => t('Determines whether or not users are notified.'), - ), - ); -} - -/** - * Implements hook_menu(). - */ -function pathauto_menu() { - $items['admin/config/search/path/patterns'] = array( - 'title' => 'Patterns', - 'page callback' => 'drupal_get_form', - 'page arguments' => array('pathauto_patterns_form'), - 'access arguments' => array('administer pathauto'), - 'type' => MENU_LOCAL_TASK, - 'weight' => 10, - 'file' => 'pathauto.admin.inc', - ); - $items['admin/config/search/path/settings'] = array( - 'title' => 'Settings', - 'page callback' => 'drupal_get_form', - 'page arguments' => array('pathauto_settings_form'), - 'access arguments' => array('administer pathauto'), - 'type' => MENU_LOCAL_TASK, - 'weight' => 20, - 'file' => 'pathauto.admin.inc', - ); - $items['admin/config/search/path/update_bulk'] = array( - 'title' => 'Bulk generate', - 'page callback' => 'drupal_get_form', - 'page arguments' => array('pathauto_bulk_update_form'), - 'access arguments' => array('administer url aliases'), - 'type' => MENU_LOCAL_TASK, - 'weight' => 30, - 'file' => 'pathauto.admin.inc', - ); - $items['admin/config/search/path/delete_bulk'] = array( - 'title' => 'Delete aliases', - 'page callback' => 'drupal_get_form', - 'page arguments' => array('pathauto_admin_delete'), - 'access arguments' => array('administer url aliases'), - 'type' => MENU_LOCAL_TASK, - 'weight' => 40, - 'file' => 'pathauto.admin.inc', - ); - - return $items; -} - -/** - * Load an URL alias pattern by entity, bundle, and language. - * - * @param $entity - * An entity (e.g. node, taxonomy, user, etc.) - * @param $bundle - * A bundle (e.g. content type, vocabulary ID, etc.) - * @param $language - * A language code, defaults to the LANGUAGE_NONE constant. - */ -function pathauto_pattern_load_by_entity($entity, $bundle = '', $language = LANGUAGE_NONE) { - $patterns = &drupal_static(__FUNCTION__, array()); - - $pattern_id = "$entity:$bundle:$language"; - if (!isset($patterns[$pattern_id])) { - $variables = array(); - if ($language != LANGUAGE_NONE) { - $variables[] = "pathauto_{$entity}_{$bundle}_{$language}_pattern"; - } - if ($bundle) { - $variables[] = "pathauto_{$entity}_{$bundle}_pattern"; - } - $variables[] = "pathauto_{$entity}_pattern"; - - foreach ($variables as $variable) { - if ($pattern = trim(variable_get($variable, ''))) { - break; - } - } - - $patterns[$pattern_id] = $pattern; - } - - return $patterns[$pattern_id]; -} - -/** * Delete multiple URL aliases. * * Intent of this is to abstract a potential path_delete_multiple() function @@ -184,7 +98,7 @@ function pathauto_pattern_load_by_entity($entity, $bundle = '', $language = LANG */ function pathauto_path_delete_multiple($pids) { foreach ($pids as $pid) { - path_delete(array('pid' => $pid)); + \Drupal::service('path.alias_storage')->delete(array('pid' => $pid)); } } @@ -211,687 +125,196 @@ function pathauto_path_delete_all($source) { * This function also checks to see if the default entity URI is different from * the current entity URI and will delete any of the default aliases. * - * @param $entity_type - * A string with the entity type. - * @param $entity + * @param EntityInterface $entity * An entity object. - * @param $default_uri + * @param string $default_uri * The optional default uri path for the entity. */ -function pathauto_entity_path_delete_all($entity_type, $entity, $default_uri = NULL) { - $uri = entity_uri($entity_type, $entity); - pathauto_path_delete_all($uri['path']); - if (isset($default_uri) && $uri['path'] != $default_uri) { +function pathauto_entity_path_delete_all(EntityInterface $entity, $default_uri = NULL) { + pathauto_path_delete_all($entity->getSystemPath()); + if (isset($default_uri) && $entity->getSystemPath() != $default_uri) { pathauto_path_delete_all($default_uri); } } -/** - * Implements hook_field_attach_rename_bundle(). - * - * Respond to machine name changes for pattern variables. - */ -function pathauto_field_attach_rename_bundle($entity_type, $bundle_old, $bundle_new) { - $variables = db_select('variable', 'v') - ->fields('v', array('name')) - ->condition('name', db_like("pathauto_{$entity_type}_{$bundle_old}_") . '%', 'LIKE') - ->execute() - ->fetchCol(); - foreach ($variables as $variable) { - $value = variable_get($variable, ''); - variable_del($variable); - $variable = strtr($variable, array("{$entity_type}_{$bundle_old}" => "{$entity_type}_{$bundle_new}")); - variable_set($variable, $value); - } -} /** - * Implements hook_field_attach_delete_bundle(). - * - * Respond to sub-types being deleted, their patterns can be removed. + * Implements hook_entity_bundle_rename(). */ -function pathauto_field_attach_delete_bundle($entity_type, $bundle) { - $variables = db_select('variable', 'v') - ->fields('v', array('name')) - ->condition('name', db_like("pathauto_{$entity_type}_{$bundle}_") . '%', 'LIKE') - ->execute() - ->fetchCol(); - foreach ($variables as $variable) { - variable_del($variable); +function pathauto_entity_bundle_rename($entity_type_id, $bundle_old, $bundle_new) { + $config = \Drupal::configFactory()->getEditable('pathauto.pattern'); + $bundle_settings = $config->get('patterns.' . $entity_type_id . '.bundles'); + + if (isset($bundle_settings[$bundle_old])) { + $bundle_settings[$bundle_new] = $bundle_settings[$bundle_old]; + unset($bundle_settings[$bundle_old]); + $config->set('patterns.' . $entity_type_id . '.bundles', $bundle_settings); + $config->save(); } } /** - * Implements hook_field_attach_form(). - * - * Add the automatic alias form elements to an existing path form fieldset. + * Implements hook__entity_bundle_delete(). */ -function pathauto_field_attach_form($entity_type, $entity, &$form, &$form_state, $langcode) { - list($id, , $bundle) = entity_extract_ids($entity_type, $entity); - - if (!isset($form['path'])) { - // This entity must be supported by core's path.module first. - // @todo Investigate removing this and supporting all fieldable entities. - return; - } - else { - // Taxonomy terms do not have an actual fieldset for path settings. - // Merge in the defaults. - $form['path'] += array( - '#type' => 'fieldset', - '#title' => t('URL path settings'), - '#collapsible' => TRUE, - '#collapsed' => empty($form['path']['alias']), - '#group' => 'additional_settings', - '#attributes' => array( - 'class' => array('path-form'), - ), - '#access' => user_access('create url aliases') || user_access('administer url aliases'), - '#weight' => 30, - '#tree' => TRUE, - '#element_validate' => array('path_form_element_validate'), - ); - } - - $pattern = pathauto_pattern_load_by_entity($entity_type, $bundle, $langcode); - if (empty($pattern)) { - return; - } - - if (!isset($entity->path['pathauto'])) { - if (!empty($id)) { - module_load_include('inc', 'pathauto'); - $uri = entity_uri($entity_type, $entity); - $pathauto_alias = pathauto_create_alias($entity_type, 'return', $uri['path'], array($entity_type => $entity), $bundle, $langcode); - if ($pathauto_alias === FALSE) { - // If Pathauto is not going to be able to generate an alias, then we - // should not bother to show the checkbox since it wouldn't do anything. - // Note that if a pattern does apply, but all the tokens currently - // evaluate to empty strings, then $pathauto_alias would equal null and - // not false. - return; - } - else { - $path = drupal_get_path_alias($uri['path'], $langcode); - $entity->path['pathauto'] = ($path != $uri['path'] && $path == $pathauto_alias); - } - } - else { - $entity->path['pathauto'] = TRUE; - } - } - - // Add JavaScript that will disable the path textfield when the automatic - // alias checkbox is checked. - $form['path']['alias']['#states']['!enabled']['input[name="path[pathauto]"]'] = array('checked' => TRUE); - - // Override path.module's vertical tabs summary. - $form['path']['#attached']['js'] = array( - 'vertical-tabs' => drupal_get_path('module', 'pathauto') . '/pathauto.js' - ); - - $form['path']['pathauto'] = array( - '#type' => 'checkbox', - '#title' => t('Generate automatic URL alias'), - '#default_value' => $entity->path['pathauto'], - '#description' => t('Uncheck this to create a custom alias below.'), - '#weight' => -1, - ); - - // Add a shortcut link to configure URL alias patterns. - if (drupal_valid_path('admin/config/search/path/patterns')) { - $form['path']['pathauto']['#description'] .= ' ' . l(t('Configure URL alias patterns.'), 'admin/config/search/path/patterns'); - } - - if ($entity->path['pathauto'] && !empty($entity->old_alias) && empty($entity->path['alias'])) { - $form['path']['alias']['#default_value'] = $entity->old_alias; - $entity->path['alias'] = $entity->old_alias; - } - - // For Pathauto to remember the old alias and prevent the Path module from - // deleting it when Pathauto wants to preserve it. - if (!empty($entity->path['alias'])) { - $form['path']['old_alias'] = array( - '#type' => 'value', - '#value' => $entity->path['alias'], - ); - } +function pathauto_entity_bundle_delete($entity_type, $bundle) { + $config = \Drupal::configFactory()->getEditable('pathauto.pattern'); + $config->clear('patterns.' . $entity_type . '.bundles.' . $bundle); + $config->save(); } + /** * Implements hook_entity_presave(). */ -function pathauto_entity_presave($entity, $type) { +function pathauto_entity_presave($entity) { + if (!($entity instanceof ContentEntityInterface) || $entity->hasField('path')) { + return; + } // About to be saved (before insert/update) - if (!empty($entity->path['pathauto']) && isset($entity->path['old_alias']) - && $entity->path['alias'] == '' && $entity->path['old_alias'] != '') { - /** + if (!empty($entity->path->pathauto) && isset($entity->path->old_alias) + && $entity->path->alias == '' && $entity->path->old_alias != '') { + /* * There was an old alias, but when pathauto_perform_alias was checked * the javascript disabled the textbox which led to an empty value being * submitted. Restoring the old path-value here prevents the Path module * from deleting any old alias before Pathauto gets control. */ - $entity->path['alias'] = $entity->path['old_alias']; + $entity->path->alias = $entity->path->old_alias; } // Help prevent errors with progromatically creating entities by defining // path['alias'] as an empty string. // @see http://drupal.org/node/1328180 // @see http://drupal.org/node/1576552 - if (isset($entity->path['pathauto']) && !isset($entity->path['alias'])) { - $entity->path['alias'] = ''; + if (isset($entity->path->pathauto) && !isset($entity->path->alias)) { + $entity->path->alias = ''; } } /** - * Implements hook_action_info(). + * Implements hook_entity_insert(). */ -function pathauto_action_info() { - $info['pathauto_node_update_action'] = array( - 'type' => 'node', - 'label' => t('Update node alias'), - 'configurable' => FALSE, - 'triggers' => array(), - ); - $info['pathauto_taxonomy_term_update_action'] = array( - 'type' => 'taxonomy_term', - 'label' => t('Update taxonomy term alias'), - 'configurable' => FALSE, - 'triggers' => array(), - ); - $info['pathauto_user_update_action'] = array( - 'type' => 'user', - 'label' => t('Update user alias'), - 'configurable' => FALSE, - 'triggers' => array(), - ); - - return $info; +function pathauto_entity_insert(EntityInterface $entity) { + \Drupal::service('pathauto.manager')->updateAlias($entity, 'insert'); } /** - * Returns the language code of the given entity. - * - * Backward compatibility layer to ensure that installations running an older - * version of core where entity_language() is not avilable do not break. - * - * @param string $entity_type - * An entity type. - * @param object $entity - * An entity object. - * @param bool $check_language_property - * A boolean if TRUE, will attempt to fetch the language code from - * $entity->language if the entity_language() function failed or does not - * exist. Default is TRUE. + * Implements hook_entity_update(). */ -function pathauto_entity_language($entity_type, $entity, $check_language_property = TRUE) { - $langcode = NULL; - - if (function_exists('entity_language')) { - $langcode = entity_language($entity_type, $entity); - } - elseif ($check_language_property && !empty($entity->language)) { - $langcode = $entity->language; - } - - return !empty($langcode) ? $langcode : LANGUAGE_NONE; +function pathauto_entity_update(EntityInterface $entity) { + \Drupal::service('pathauto.manager')->updateAlias($entity, 'update'); } -function pathauto_is_alias_reserved($alias, $source, $langcode = LANGUAGE_NONE) { - foreach (module_implements('pathauto_is_alias_reserved') as $module) { - $result = module_invoke($module, 'pathauto_is_alias_reserved', $alias, $source, $langcode); - if (!empty($result)) { - // As soon as the first module says that an alias is in fact reserved, - // then there is no point in checking the rest of the modules. - return TRUE; - } - } - - return FALSE; -} - -/** - * Implements hook_pathauto_is_alias_reserved() on behalf of path.module. - */ -function path_pathauto_is_alias_reserved($alias, $source, $langcode) { - return (bool) db_query_range("SELECT pid FROM {url_alias} WHERE source <> :source AND alias = :alias AND language IN (:language, :language_none) ORDER BY language DESC, pid DESC", 0, 1, array( - ':source' => $source, - ':alias' => $alias, - ':language' => $langcode, - ':language_none' => LANGUAGE_NONE, - ))->fetchField(); -} - -/** - * Implements hook_pathauto_is_alias_reserved(). - */ -function pathauto_pathauto_is_alias_reserved($alias, $source, $langcode) { - module_load_include('inc', 'pathauto'); - return _pathauto_path_is_callback($alias); -} - -if (!function_exists('path_field_extra_fields')) { -/** - * Implements hook_field_extra_fields() on behalf of path.module. - * - * Add support for the 'URL path settings' to be re-ordered by the user on the - * 'Manage Fields' tab of content types and vocabularies. - */ -function path_field_extra_fields() { - $info = array(); - - foreach (node_type_get_types() as $node_type) { - if (!isset($info['node'][$node_type->type]['form']['path'])) { - $info['node'][$node_type->type]['form']['path'] = array( - 'label' => t('URL path settings'), - 'description' => t('Path module form elements'), - 'weight' => 30, - ); - } - } - - if (module_exists('taxonomy')) { - $vocabularies = taxonomy_get_vocabularies(); - foreach ($vocabularies as $vocabulary) { - if (!isset($info['taxonomy_term'][$vocabulary->machine_name]['form']['path'])) { - $info['taxonomy_term'][$vocabulary->machine_name]['form']['path'] = array( - 'label' => t('URL path settings'), - 'description' => t('Path module form elements'), - 'weight' => 30, - ); - } - } - } - - return $info; -} -} - -/** - * @name pathauto_node Pathauto integration for the core node module. - * @{ - */ - -/** - * Implements hook_node_insert(). - */ -function pathauto_node_insert($node) { - // @todo Remove the next line when http://drupal.org/node/1025870 is fixed. - unset($node->uri); - pathauto_node_update_alias($node, 'insert'); -} - -/** - * Implements hook_node_update(). - */ -function pathauto_node_update($node) { - pathauto_node_update_alias($node, 'update'); -} - -/** - * Implements hook_node_delete(). - */ -function pathauto_node_delete($node) { - pathauto_entity_path_delete_all('node', $node, "node/{$node->nid}"); -} - -/** - * Implements hook_form_BASE_FORM_ID_alter(). - * - * Add the Pathauto settings to the node form. - */ -function pathauto_form_node_form_alter(&$form, &$form_state) { - $node = $form_state['node']; - $langcode = pathauto_entity_language('node', $node); - pathauto_field_attach_form('node', $node, $form, $form_state, $langcode); -} - -/** - * Implements hook_node_operations(). - */ -function pathauto_node_operations() { - $operations['pathauto_update_alias'] = array( - 'label' => t('Update URL alias'), - 'callback' => 'pathauto_node_update_alias_multiple', - 'callback arguments' => array('bulkupdate', array('message' => TRUE)), - ); - return $operations; -} - -/** - * Update the URL aliases for an individual node. - * - * @param $node - * A node object. - * @param $op - * Operation being performed on the node ('insert', 'update' or 'bulkupdate'). - * @param $options - * An optional array of additional options. - */ -function pathauto_node_update_alias(stdClass $node, $op, array $options = array()) { - // Skip processing if the user has disabled pathauto for the node. - if (isset($node->path['pathauto']) && empty($node->path['pathauto']) && empty($options['force'])) { - return FALSE; - } - - $options += array('language' => pathauto_entity_language('node', $node)); - - // Skip processing if the node has no pattern. - if (!pathauto_pattern_load_by_entity('node', $node->type, $options['language'])) { - return FALSE; - } - - module_load_include('inc', 'pathauto'); - $uri = entity_uri('node', $node); - return pathauto_create_alias('node', $op, $uri['path'], array('node' => $node), $node->type, $options['language']); -} /** * Update the URL aliases for multiple nodes. * - * @param $nids + * @param array $nids * An array of node IDs. - * @param $op + * @param string $op * Operation being performed on the nodes ('insert', 'update' or * 'bulkupdate'). - * @param $options + * @param array $options * An optional array of additional options. */ function pathauto_node_update_alias_multiple(array $nids, $op, array $options = array()) { $options += array('message' => FALSE); - $nodes = node_load_multiple($nids); + $nodes = Node::loadMultiple($nids); foreach ($nodes as $node) { - pathauto_node_update_alias($node, $op, $options); + \Drupal::service('pathauto.manager')->updateAlias($node, $op, $options); } if (!empty($options['message'])) { - drupal_set_message(format_plural(count($nids), 'Updated URL alias for 1 node.', 'Updated URL aliases for @count nodes.')); - } -} - -/** - * Update action wrapper for pathauto_node_update_alias(). - */ -function pathauto_node_update_action($node, $context = array()) { - pathauto_node_update_alias($node, 'bulkupdate', array('message' => TRUE)); -} - -/** - * @} End of "name pathauto_node". - */ - -/** - * @name pathauto_taxonomy Pathauto integration for the core taxonomy module. - * @{ - */ - -/** - * Implements hook_taxonomy_term_insert(). - */ -function pathauto_taxonomy_term_insert($term) { - pathauto_taxonomy_term_update_alias($term, 'insert'); -} - -/** - * Implements hook_taxonomy_term_update(). - */ -function pathauto_taxonomy_term_update($term) { - pathauto_taxonomy_term_update_alias($term, 'update', array('alias children' => TRUE)); -} - -/** - * Implements hook_taxonomy_term_delete(). - */ -function pathauto_taxonomy_term_delete($term) { - pathauto_entity_path_delete_all('taxonomy_term', $term, "taxonomy/term/{$term->tid}"); -} - -/** - * Implements hook_form_FORM_ID_alter(). - * - * Add the Pathauto settings to the taxonomy term form. - */ -function pathauto_form_taxonomy_form_term_alter(&$form, $form_state) { - $term = $form_state['term']; - $langcode = pathauto_entity_language('taxonomy_term', $term); - pathauto_field_attach_form('taxonomy_term', $term, $form, $form_state, $langcode); -} - -/** - * Update the URL aliases for an individual taxonomy term. - * - * @param $term - * A taxonomy term object. - * @param $op - * Operation being performed on the term ('insert', 'update' or 'bulkupdate'). - * @param $options - * An optional array of additional options. - */ -function pathauto_taxonomy_term_update_alias(stdClass $term, $op, array $options = array()) { - // Skip processing if the user has disabled pathauto for the term. - if (isset($term->path['pathauto']) && empty($term->path['pathauto']) && empty($options['force'])) { - return FALSE; - } - - $module = 'taxonomy_term'; - if ($term->vid == variable_get('forum_nav_vocabulary', '')) { - if (module_exists('forum')) { - $module = 'forum'; - } - else { - return FALSE; - } - } - - // Check that the term has its bundle, which is the vocabulary's machine name. - if (!isset($term->vocabulary_machine_name)) { - $vocabulary = taxonomy_vocabulary_load($term->vid); - $term->vocabulary_machine_name = $vocabulary->machine_name; - } - - $options += array( - 'alias children' => FALSE, - 'language' => pathauto_entity_language('taxonomy_term', $term), - ); - - // Skip processing if the term has no pattern. - if (!pathauto_pattern_load_by_entity($module, $term->vocabulary_machine_name)) { - return FALSE; + drupal_set_message(\Drupal::translation()->formatPlural(count($nids), 'Updated URL alias for 1 node.', 'Updated URL aliases for @count nodes.')); } - - module_load_include('inc', 'pathauto'); - $uri = entity_uri('taxonomy_term', $term); - $result = pathauto_create_alias($module, $op, $uri['path'], array('term' => $term), $term->vocabulary_machine_name, $options['language']); - - if (!empty($options['alias children'])) { - // For all children generate new aliases. - $options['alias children'] = FALSE; - unset($options['language']); - foreach (taxonomy_get_tree($term->vid, $term->tid) as $subterm) { - pathauto_taxonomy_term_update_alias($subterm, $op, $options); - } - } - - return $result; } /** * Update the URL aliases for multiple taxonomy terms. * - * @param $tids + * @param array $tids * An array of term IDs. - * @param $op + * @param string $op * Operation being performed on the nodes ('insert', 'update' or * 'bulkupdate'). - * @param $options + * @param array $options * An optional array of additional options. */ function pathauto_taxonomy_term_update_alias_multiple(array $tids, $op, array $options = array()) { $options += array('message' => FALSE); - $terms = taxonomy_term_load_multiple($tids); + $terms = entity_load_multiple('taxonomy_term', $tids); foreach ($terms as $term) { - pathauto_taxonomy_term_update_alias($term, $op, $options); + \Drupal::service('pathauto.manager')->updateAlias($term, $op, $options); } if (!empty($options['message'])) { - drupal_set_message(format_plural(count($tids), 'Updated URL alias for 1 term.', 'Updated URL aliases for @count terms.')); + drupal_set_message(\Drupal::translation()->formatPlural(count($tids), 'Updated URL alias for 1 term.', 'Updated URL aliases for @count terms.')); } } /** - * Update action wrapper for pathauto_taxonomy_term_update_alias(). - */ -function pathauto_taxonomy_term_update_action($term, $context = array()) { - pathauto_taxonomy_term_update_alias($term, 'bulkupdate', array('message' => TRUE)); -} - -/** - * @} End of "name pathauto_taxonomy". - */ - -/** - * @name pathauto_user Pathauto integration for the core user and blog modules. - * @{ - */ - -/** - * Implements hook_user_insert(). - */ -function pathauto_user_insert(&$edit, $account, $category) { - pathauto_user_update_alias($account, 'insert'); -} - -/** - * Implements hook_user_update(). - */ -function pathauto_user_update(&$edit, $account, $category) { - pathauto_user_update_alias($account, 'update'); -} - -/** - * Implements hook_user_delete(). - */ -function pathauto_user_delete($account) { - pathauto_entity_path_delete_all('user', $account, "user/{$account->uid}"); - pathauto_path_delete_all("blog/{$account->uid}"); -} - -/** - * Implements hook_user_operations(). - */ -function pathauto_user_operations() { - $operations['pathauto_update_alias'] = array( - 'label' => t('Update URL alias'), - 'callback' => 'pathauto_user_update_alias_multiple', - 'callback arguments' => array('bulkupdate', array('message' => TRUE)), - ); - return $operations; -} - -/** - * Update the URL aliases for an individual user account. - * - * @param $account - * A user account object. - * @param $op - * Operation being performed on the account ('insert', 'update' or - * 'bulkupdate'). - * @param $options - * An optional array of additional options. - */ -function pathauto_user_update_alias(stdClass $account, $op, array $options = array()) { - // Skip processing if the user has disabled pathauto for the account. - if (isset($account->path['pathauto']) && empty($account->path['pathauto']) && empty($options['force'])) { - return FALSE; - } - - $options += array( - 'alias blog' => module_exists('blog'), - // $user->language is not the user entity language, thus we need to skip - // the property fallback check. - 'language' => pathauto_entity_language('user', $account, FALSE), - ); - - // Skip processing if the account has no pattern. - if (!pathauto_pattern_load_by_entity('user', '', $options['language'])) { - return FALSE; - } - - module_load_include('inc', 'pathauto'); - $uri = entity_uri('user', $account); - $return = pathauto_create_alias('user', $op, $uri['path'], array('user' => $account), NULL, $options['language']); - - // Because blogs are also associated with users, also generate the blog paths. - if (!empty($options['alias blog'])) { - pathauto_blog_update_alias($account, $op, $options); - } - - return $return; -} - -/** * Update the URL aliases for multiple user accounts. * - * @param $uids + * @param array $uids * An array of user account IDs. - * @param $op + * @param string $op * Operation being performed on the accounts ('insert', 'update' or * 'bulkupdate'). - * @param $options + * @param array $options * An optional array of additional options. */ function pathauto_user_update_alias_multiple(array $uids, $op, array $options = array()) { $options += array('message' => FALSE); - $accounts = user_load_multiple($uids); + $accounts = User::loadMultiple($uids); foreach ($accounts as $account) { - pathauto_user_update_alias($account, $op, $options); + \Drupal::service('pathauto.manager')->updateAlias($account, $op, $options); } if (!empty($options['message'])) { - drupal_set_message(format_plural(count($uids), 'Updated URL alias for 1 user account.', 'Updated URL aliases for @count user accounts.')); + drupal_set_message(\Drupal::translation()->formatPlural(count($uids), 'Updated URL alias for 1 user account.', 'Updated URL aliases for @count user accounts.')); } } /** - * Update action wrapper for pathauto_user_update_alias(). + * Implements hook_field_info_alter(). */ -function pathauto_user_update_action($account, $context = array()) { - pathauto_user_update_alias($account, 'bulkupdate', array('message' => TRUE)); +function pathauto_field_info_alter(&$info) { + $info['path']['class'] = '\Drupal\pathauto\PathautoItem'; } /** - * Update the blog URL aliases for an individual user account. - * - * @param $account - * A user account object. - * @param $op - * Operation being performed on the blog ('insert', 'update' or - * 'bulkupdate'). - * @param $options - * An optional array of additional options. + * Implements hook_entity_base_field_info_alter(). */ -function pathauto_blog_update_alias(stdClass $account, $op, array $options = array()) { - // Skip processing if the blog has no pattern. - if (!pathauto_pattern_load_by_entity('blog')) { - return FALSE; - } - - $options += array( - 'language' => LANGUAGE_NONE, - ); - - module_load_include('inc', 'pathauto'); - if (node_access('create', 'blog', $account)) { - return pathauto_create_alias('blog', $op, "blog/{$account->uid}", array('user' => $account), NULL, $options['language']); - } - else { - pathauto_path_delete_all("blog/{$account->uid}"); +function pathauto_entity_base_field_info_alter(&$fields, EntityTypeInterface $entity_type) { + if (isset($fields['path']) && $fields['path']->getType() == 'path') { + $fields['path']->setDisplayOptions('form', array( + 'type' => 'pathauto', + 'weight' => 30, + )); } } + /** - * @} End of "name pathauto_user". + * Implements hook_entity_base_field_info(). */ +function pathauto_entity_base_field_info(EntityTypeInterface $entity_type) { + // @todo: Make this configurable and/or remove if + // https://drupal.org/node/476294 is resolved. + if ($entity_type->id() === 'user') { + $fields['path'] = BaseFieldDefinition::create('path') + ->setCustomStorage(TRUE) + ->setLabel(t('URL alias')) + ->setTranslatable(TRUE) + ->setDisplayOptions('form', array( + 'type' => 'pathauto', + 'weight' => 30, + )) + ->setDisplayConfigurable('form', TRUE); + + return $fields; + } +} diff --git a/pathauto.pathauto.inc b/pathauto.pathauto.inc index 28d3ce0..c4390ce 100644 --- a/pathauto.pathauto.inc +++ b/pathauto.pathauto.inc @@ -6,6 +6,7 @@ * * @ingroup pathauto */ +use Drupal\Core\Language\Language; /** * Implements hook_path_alias_types(). @@ -15,13 +16,10 @@ function pathauto_path_alias_types() { $objects['user/'] = t('Users'); $objects['node/'] = t('Content'); - if (module_exists('blog')) { - $objects['blog/'] = t('User blogs'); - } - if (module_exists('taxonomy')) { + if (\Drupal::moduleHandler()->moduleExists('taxonomy')) { $objects['taxonomy/term/'] = t('Taxonomy terms'); } - if (module_exists('forum')) { + if (\Drupal::moduleHandler()->moduleExists('forum')) { $objects['forum/'] = t('Forums'); } return $objects; @@ -37,7 +35,7 @@ function pathauto_path_alias_types() { * exist, than this file will not get included and the core implementations * will never get run. * - * @see pathauto_module_implements_alter(). + * @see pathauto_module_implements_alter() */ function pathauto_pathauto() { // Empty hook; see the above comment. @@ -59,12 +57,12 @@ function node_pathauto($op) { $settings['batch_file'] = drupal_get_path('module', 'pathauto') . '/pathauto.pathauto.inc'; $languages = array(); - if (module_exists('locale')) { - $languages = array(LANGUAGE_NONE => t('language neutral')) + locale_language_list('name'); + if (\Drupal::moduleHandler()->moduleExists('locale')) { + $languages = array(Language::LANGCODE_NOT_SPECIFIED => t('language neutral')) + \Drupal::languageManager()->getLanguages('name'); } foreach (node_type_get_names() as $node_type => $node_name) { - if (count($languages) && variable_get('language_content_type_' . $node_type, 0)) { + if (count($languages) && \Drupal::moduleHandler()->moduleExists('content_translation') && content_translation_enabled('node', $node_type)) { $settings['patternitems'][$node_type] = t('Default path pattern for @node_type (applies to all @node_type content types with blank patterns below)', array('@node_type' => $node_name)); foreach ($languages as $lang_code => $lang_name) { $settings['patternitems'][$node_type . '_' . $lang_code] = t('Pattern for all @language @node_type paths', array('@node_type' => $node_name, '@language' => $lang_name)); @@ -75,6 +73,7 @@ function node_pathauto($op) { } } return (object) $settings; + default: break; } @@ -137,18 +136,20 @@ function taxonomy_pathauto($op) { $settings['batch_update_callback'] = 'taxonomy_pathauto_bulk_update_batch_process'; $settings['batch_file'] = drupal_get_path('module', 'pathauto') . '/pathauto.pathauto.inc'; - $vocabularies = taxonomy_get_vocabularies(); + $vocabularies = entity_load_multiple('taxonomy_vocabulary'); if (count($vocabularies)) { $settings['patternitems'] = array(); foreach ($vocabularies as $vid => $vocabulary) { - if ($vid == variable_get('forum_nav_vocabulary', '')) { + /*if ($vid == variable_get('forum_nav_vocabulary', '')) { // Skip the forum vocabulary. continue; - } - $settings['patternitems'][$vocabulary->machine_name] = t('Pattern for all %vocab-name paths', array('%vocab-name' => $vocabulary->name)); + }*/ + + $settings['patternitems'][$vocabulary->id()] = t('Pattern for all %vocab-name paths', array('%vocab-name' => $vocabulary->label())); } } return (object) $settings; + default: break; } @@ -169,7 +170,7 @@ function taxonomy_pathauto_bulk_update_batch_process(&$context) { $query->isNull('ua.source'); $query->condition('td.tid', $context['sandbox']['current'], '>'); // Exclude the forums terms. - if ($forum_vid = variable_get('forum_nav_vocabulary', '')) { + if ($forum_vid = 'forums') { $query->condition('td.vid', $forum_vid, '<>'); } $query->orderBy('td.tid'); @@ -215,6 +216,7 @@ function forum_pathauto($op) { $settings['batch_update_callback'] = 'forum_pathauto_bulk_update_batch_process'; $settings['batch_file'] = drupal_get_path('module', 'pathauto') . '/pathauto.pathauto.inc'; return (object) $settings; + default: break; } @@ -234,7 +236,7 @@ function forum_pathauto_bulk_update_batch_process(&$context) { $query->addField('td', 'tid'); $query->isNull('ua.source'); $query->condition('td.tid', $context['sandbox']['current'], '>'); - $query->condition('td.vid', variable_get('forum_nav_vocabulary', '')); + $query->condition('td.vid', 'forums'); $query->orderBy('td.tid'); $query->addTag('pathauto_bulk_update'); $query->addMetaData('entity', 'taxonomy_term'); @@ -278,6 +280,7 @@ function user_pathauto($op) { $settings['batch_update_callback'] = 'user_pathauto_bulk_update_batch_process'; $settings['batch_file'] = drupal_get_path('module', 'pathauto') . '/pathauto.pathauto.inc'; return (object) $settings; + default: break; } @@ -315,7 +318,7 @@ function user_pathauto_bulk_update_batch_process(&$context) { $query->range(0, 25); $uids = $query->execute()->fetchCol(); - pathauto_user_update_alias_multiple($uids, 'bulkupdate', array('alias blog' => FALSE)); + pathauto_user_update_alias_multiple($uids, 'bulkupdate', array()); $context['sandbox']['count'] += count($uids); $context['sandbox']['current'] = max($uids); $context['message'] = t('Updated alias for user @uid.', array('@uid' => end($uids))); @@ -324,69 +327,3 @@ function user_pathauto_bulk_update_batch_process(&$context) { $context['finished'] = $context['sandbox']['count'] / $context['sandbox']['total']; } } - -/** - * Implements hook_pathauto(). - */ -function blog_pathauto($op) { - switch ($op) { - case 'settings': - $settings = array(); - $settings['module'] = 'blog'; - $settings['token_type'] = 'user'; - $settings['groupheader'] = t('Blog paths'); - $settings['patterndescr'] = t('Pattern for blog page paths'); - $settings['patterndefault'] = 'blogs/[user:name]'; - $settings['batch_update_callback'] = 'blog_pathauto_bulk_update_batch_process'; - $settings['batch_file'] = drupal_get_path('module', 'pathauto') . '/pathauto.pathauto.inc'; - return (object) $settings; - default: - break; - } -} - -/** - * Batch processing callback; Generate aliases for blogs. - */ -function blog_pathauto_bulk_update_batch_process(&$context) { - if (!isset($context['sandbox']['current'])) { - $context['sandbox']['count'] = 0; - $context['sandbox']['current'] = 0; - } - - $query = db_select('users', 'u'); - $query->leftJoin('url_alias', 'ua', "CONCAT('blog/', u.uid) = ua.source"); - $query->addField('u', 'uid'); - $query->isNull('ua.source'); - $query->condition('u.uid', $context['sandbox']['current'], '>'); - $query->orderBy('u.uid'); - $query->addTag('pathauto_bulk_update'); - $query->addMetaData('entity', 'user'); - - // Get the total amount of items to process. - if (!isset($context['sandbox']['total'])) { - $context['sandbox']['total'] = $query->countQuery()->execute()->fetchField(); - - // If there are no nodes to update, the stop immediately. - if (!$context['sandbox']['total']) { - $context['finished'] = 1; - return; - } - } - - $query->range(0, 25); - $uids = $query->execute()->fetchCol(); - - $accounts = user_load_multiple($uids); - foreach ($accounts as $account) { - pathauto_blog_update_alias($account, 'bulkupdate'); - } - - $context['sandbox']['count'] += count($uids); - $context['sandbox']['current'] = max($uids); - $context['message'] = t('Updated alias for blog user @uid.', array('@uid' => end($uids))); - - if ($context['sandbox']['count'] != $context['sandbox']['total']) { - $context['finished'] = $context['sandbox']['count'] / $context['sandbox']['total']; - } -} diff --git a/pathauto.permissions.yml b/pathauto.permissions.yml new file mode 100644 index 0000000..ede9f28 --- /dev/null +++ b/pathauto.permissions.yml @@ -0,0 +1,6 @@ +administer pathauto: + title: 'Administer pathauto' + description: 'Allows a user to configure patterns for automated aliases and bulk delete URL-aliases.' +notify of path changes: + title: 'Notify of Path Changes' + description: 'Determines whether or not users are notified.' diff --git a/pathauto.routing.yml b/pathauto.routing.yml new file mode 100644 index 0000000..92c1efe --- /dev/null +++ b/pathauto.routing.yml @@ -0,0 +1,31 @@ +pathauto.patterns.form: + path: 'admin/config/search/path/patterns' + defaults: + _form: '\Drupal\pathauto\Form\PathautoPatternsForm' + _title: 'Patterns' + requirements: + _permission: 'administer pathauto' + +pathauto.settings.form: + path: 'admin/config/search/path/settings' + defaults: + _form: '\Drupal\pathauto\Form\PathautoSettingsForm' + _title: 'Settings' + requirements: + _permission: 'administer pathauto' + +pathauto.bulk.update.form: + path: 'admin/config/search/path/update_bulk' + defaults: + _form: '\Drupal\pathauto\Form\PathautoBulkUpdateForm' + _title: 'Bulk generate' + requirements: + _permission: 'administer url aliases' + +pathauto.admin.delete: + path: 'admin/config/search/path/delete_bulk' + defaults: + _form: '\Drupal\pathauto\Form\PathautoAdminDelete' + _title: 'Delete aliases' + requirements: + _permission: 'administer url aliases' \ No newline at end of file diff --git a/pathauto.services.yml b/pathauto.services.yml new file mode 100644 index 0000000..5e244db --- /dev/null +++ b/pathauto.services.yml @@ -0,0 +1,19 @@ +services: + pathauto.manager: + class: Drupal\pathauto\PathautoManager + arguments: ['@config.factory', '@language_manager', '@cache.default', '@module_handler', '@token', '@pathauto.alias_cleaner', '@pathauto.alias_storage_helper', '@pathauto.alias_uniquifier', ,'@pathauto.verbose_messenger', '@string_translation'] + pathauto.alias_cleaner: + class: Drupal\pathauto\AliasCleaner + arguments: ['@config.factory', '@pathauto.alias_storage_helper'] + pathauto.alias_storage_helper: + class: Drupal\pathauto\AliasStorageHelper + arguments: ['@config.factory', '@path.alias_storage', '@database','@pathauto.verbose_messenger', '@string_translation'] + pathauto.alias_uniquifier: + class: Drupal\pathauto\AliasUniquifier + arguments: ['@config.factory', '@pathauto.alias_storage_helper','@module_handler', '@router.no_access_checks'] + pathauto.verbose_messenger: + class: Drupal\pathauto\VerboseMessenger + arguments: ['@config.factory', '@current_user'] + plugin.manager.alias_type: + class: Drupal\pathauto\AliasTypeManager + parent: default_plugin_manager diff --git a/pathauto.test b/pathauto.test deleted file mode 100644 index 5a408e0..0000000 --- a/pathauto.test +++ /dev/null @@ -1,840 +0,0 @@ - $token), array($type => $object)); - $tokens += array($token => ''); - $this->assertIdentical($tokens[$token], $expected, t("Token value for [@type:@token] was '@actual', expected value '@expected'.", array('@type' => $type, '@token' => $token, '@actual' => $tokens[$token], '@expected' => $expected))); - } - - function saveAlias($source, $alias, $language = LANGUAGE_NONE) { - $alias = array( - 'source' => $source, - 'alias' => $alias, - 'language' => $language, - ); - path_save($alias); - return $alias; - } - - function saveEntityAlias($entity_type, $entity, $alias, $language = LANGUAGE_NONE) { - $uri = entity_uri($entity_type, $entity); - return $this->saveAlias($uri['path'], $alias, $language); - } - - function assertEntityAlias($entity_type, $entity, $expected_alias, $language = LANGUAGE_NONE) { - $uri = entity_uri($entity_type, $entity); - $this->assertAlias($uri['path'], $expected_alias, $language); - } - - function assertEntityAliasExists($entity_type, $entity) { - $uri = entity_uri($entity_type, $entity); - return $this->assertAliasExists(array('source' => $uri['path'])); - } - - function assertNoEntityAlias($entity_type, $entity, $language = LANGUAGE_NONE) { - $uri = entity_uri($entity_type, $entity); - $this->assertEntityAlias($entity_type, $entity, $uri['path'], $language); - } - - function assertNoEntityAliasExists($entity_type, $entity) { - $uri = entity_uri($entity_type, $entity); - $this->assertNoAliasExists(array('source' => $uri['path'])); - } - - function assertAlias($source, $expected_alias, $language = LANGUAGE_NONE) { - drupal_clear_path_cache($source); - $alias = drupal_get_path_alias($source, $language); - $this->assertIdentical($alias, $expected_alias, t("Alias for %source with language '@language' was %actual, expected %expected.", array('%source' => $source, '%actual' => $alias, '%expected' => $expected_alias, '@language' => $language))); - } - - function assertAliasExists($conditions) { - $path = path_load($conditions); - $this->assertTrue($path, t('Alias with conditions @conditions found.', array('@conditions' => var_export($conditions, TRUE)))); - return $path; - } - - function assertNoAliasExists($conditions) { - $alias = path_load($conditions); - $this->assertFalse($alias, t('Alias with conditions @conditions not found.', array('@conditions' => var_export($conditions, TRUE)))); - } - - function deleteAllAliases() { - db_delete('url_alias')->execute(); - drupal_clear_path_cache(); - } - - function addVocabulary(array $vocabulary = array()) { - $name = drupal_strtolower($this->randomName(5)); - $vocabulary += array( - 'name' => $name, - 'machine_name' => $name, - 'nodes' => array('article' => 'article'), - ); - $vocabulary = (object) $vocabulary; - taxonomy_vocabulary_save($vocabulary); - return $vocabulary; - } - - function addTerm(stdClass $vocabulary, array $term = array()) { - $term += array( - 'name' => drupal_strtolower($this->randomName(5)), - 'vocabulary_machine_name' => $vocabulary->machine_name, - 'vid' => $vocabulary->vid, - ); - $term = (object) $term; - taxonomy_term_save($term); - return $term; - } - - function assertEntityPattern($entity_type, $bundle, $language = LANGUAGE_NONE, $expected) { - drupal_static_reset('pathauto_pattern_load_by_entity'); - $this->refreshVariables(); - $pattern = pathauto_pattern_load_by_entity($entity_type, $bundle, $language); - $this->assertIdentical($expected, $pattern); - } - - function drupalGetTermByName($name, $reset = FALSE) { - $terms = entity_load('taxonomy_term', array(), array('name' => $name), $reset); - return !empty($terms) ? reset($terms) : FALSE; - } -} - -/** - * Unit tests for Pathauto functions. - */ -class PathautoUnitTestCase extends PathautoTestHelper { - public static function getInfo() { - return array( - 'name' => 'Pathauto unit tests', - 'description' => 'Unit tests for Pathauto functions.', - 'group' => 'Pathauto', - 'dependencies' => array('token'), - ); - } - - function setUp(array $modules = array()) { - parent::setUp($modules); - module_load_include('inc', 'pathauto'); - } - - /** - * Test _pathauto_get_schema_alias_maxlength(). - */ - function testGetSchemaAliasMaxLength() { - $this->assertIdentical(_pathauto_get_schema_alias_maxlength(), 255); - } - - /** - * Test pathauto_pattern_load_by_entity(). - */ - function testPatternLoadByEntity() { - variable_set('pathauto_node_story_en_pattern', ' story/en/[node:title] '); - variable_set('pathauto_node_story_pattern', 'story/[node:title]'); - variable_set('pathauto_node_pattern', 'content/[node:title]'); - variable_set('pathauto_user_pattern', 'users/[user:name]'); - - $tests = array( - array('entity' => 'node', 'bundle' => 'story', 'language' => 'fr', 'expected' => 'story/[node:title]'), - array('entity' => 'node', 'bundle' => 'story', 'language' => 'en', 'expected' => 'story/en/[node:title]'), - array('entity' => 'node', 'bundle' => 'story', 'language' => LANGUAGE_NONE, 'expected' => 'story/[node:title]'), - array('entity' => 'node', 'bundle' => 'page', 'language' => 'en', 'expected' => 'content/[node:title]'), - array('entity' => 'user', 'bundle' => 'user', 'language' => LANGUAGE_NONE, 'expected' => 'users/[user:name]'), - array('entity' => 'invalid-entity', 'bundle' => '', 'language' => LANGUAGE_NONE, 'expected' => ''), - ); - foreach ($tests as $test) { - $actual = pathauto_pattern_load_by_entity($test['entity'], $test['bundle'], $test['language']); - $this->assertIdentical($actual, $test['expected'], t("pathauto_pattern_load_by_entity('@entity', '@bundle', '@language') returned '@actual', expected '@expected'", array('@entity' => $test['entity'], '@bundle' => $test['bundle'], '@language' => $test['language'], '@actual' => $actual, '@expected' => $test['expected']))); - } - } - - /** - * Test pathauto_cleanstring(). - */ - function testCleanString() { - $tests = array(); - variable_set('pathauto_ignore_words', ', in, is,that, the , this, with, '); - variable_set('pathauto_max_component_length', 35); - - // Test the 'ignored words' removal. - $tests['this'] = 'this'; - $tests['this with that'] = 'this-with-that'; - $tests['this thing with that thing'] = 'thing-thing'; - - // Test length truncation and duplicate separator removal. - $tests[' - Pathauto is the greatest - module ever in Drupal history - '] = 'pathauto-greatest-module-ever'; - - // Test that HTML tags are removed. - $tests['This text has
HTML tags.'] = 'text-has-html-tags'; - $tests[check_plain('This text has
HTML tags.')] = 'text-has-html-tags'; - - foreach ($tests as $input => $expected) { - $output = pathauto_cleanstring($input); - $this->assertEqual($output, $expected, t("pathauto_cleanstring('@input') expected '@expected', actual '@output'", array('@input' => $input, '@expected' => $expected, '@output' => $output))); - } - } - - /** - * Test pathauto_clean_alias(). - */ - function testCleanAlias() { - $tests = array(); - $tests['one/two/three'] = 'one/two/three'; - $tests['/one/two/three/'] = 'one/two/three'; - $tests['one//two///three'] = 'one/two/three'; - $tests['one/two--three/-/--/-/--/four---five'] = 'one/two-three/four-five'; - $tests['one/-//three--/four'] = 'one/three/four'; - foreach ($tests as $input => $expected) { - $output = pathauto_clean_alias($input); - $this->assertEqual($output, $expected, t("pathauto_clean_alias('@input') expected '@expected', actual '@output'", array('@input' => $input, '@expected' => $expected, '@output' => $output))); - } - - } - - /** - * Test pathauto_path_delete_multiple(). - */ - function testPathDeleteMultiple() { - $this->saveAlias('node/1', 'node-1-alias'); - $this->saveAlias('node/1/view', 'node-1-alias/view'); - $this->saveAlias('node/1', 'node-1-alias-en', 'en'); - $this->saveAlias('node/1', 'node-1-alias-fr', 'fr'); - $this->saveAlias('node/2', 'node-2-alias'); - - pathauto_path_delete_all('node/1'); - $this->assertNoAliasExists(array('source' => "node/1")); - $this->assertNoAliasExists(array('source' => "node/1/view")); - $this->assertAliasExists(array('source' => "node/2")); - } - - /** - * Test the different update actions in pathauto_create_alias(). - */ - function testUpdateActions() { - // Test PATHAUTO_UPDATE_ACTION_NO_NEW with unaliased node and 'insert'. - variable_set('pathauto_update_action', PATHAUTO_UPDATE_ACTION_NO_NEW); - $node = $this->drupalCreateNode(array('title' => 'First title')); - $this->assertEntityAlias('node', $node, 'content/first-title'); - - // Default action is PATHAUTO_UPDATE_ACTION_DELETE. - variable_set('pathauto_update_action', PATHAUTO_UPDATE_ACTION_DELETE); - $node->title = 'Second title'; - pathauto_node_update($node); - $this->assertEntityAlias('node', $node, 'content/second-title'); - $this->assertNoAliasExists(array('alias' => 'content/first-title')); - - // Test PATHAUTO_UPDATE_ACTION_LEAVE - variable_set('pathauto_update_action', PATHAUTO_UPDATE_ACTION_LEAVE); - $node->title = 'Third title'; - pathauto_node_update($node); - $this->assertEntityAlias('node', $node, 'content/third-title'); - $this->assertAliasExists(array('source' => "node/{$node->nid}", 'alias' => 'content/second-title')); - - variable_set('pathauto_update_action', PATHAUTO_UPDATE_ACTION_DELETE); - $node->title = 'Fourth title'; - pathauto_node_update($node); - $this->assertEntityAlias('node', $node, 'content/fourth-title'); - $this->assertNoAliasExists(array('alias' => 'content/third-title')); - // The older second alias is not deleted yet. - $older_path = $this->assertAliasExists(array('source' => "node/{$node->nid}", 'alias' => 'content/second-title')); - path_delete($older_path); - - variable_set('pathauto_update_action', PATHAUTO_UPDATE_ACTION_NO_NEW); - $node->title = 'Fifth title'; - pathauto_node_update($node); - $this->assertEntityAlias('node', $node, 'content/fourth-title'); - $this->assertNoAliasExists(array('alias' => 'content/fifth-title')); - - // Test PATHAUTO_UPDATE_ACTION_NO_NEW with unaliased node and 'update'. - $this->deleteAllAliases(); - pathauto_node_update($node); - $this->assertEntityAlias('node', $node, 'content/fifth-title'); - - // Test PATHAUTO_UPDATE_ACTION_NO_NEW with unaliased node and 'bulkupdate'. - $this->deleteAllAliases(); - $node->title = 'Sixth title'; - pathauto_node_update_alias($node, 'bulkupdate'); - $this->assertEntityAlias('node', $node, 'content/sixth-title'); - } - - /** - * Test that pathauto_create_alias() will not create an alias for a pattern - * that does not get any tokens replaced. - */ - function testNoTokensNoAlias() { - $node = $this->drupalCreateNode(array('title' => '')); - $this->assertNoEntityAliasExists('node', $node); - - $node->title = 'hello'; - pathauto_node_update($node); - $this->assertEntityAlias('node', $node, 'content/hello'); - } - - /** - * Test the handling of path vs non-path tokens in pathauto_clean_token_values(). - */ - function testPathTokens() { - variable_set('pathauto_taxonomy_term_pattern', '[term:parent:url:path]/[term:name]'); - $vocab = $this->addVocabulary(); - - $term1 = $this->addTerm($vocab, array('name' => 'Parent term')); - $this->assertEntityAlias('taxonomy_term', $term1, 'parent-term'); - - $term2 = $this->addTerm($vocab, array('name' => 'Child term', 'parent' => $term1->tid)); - $this->assertEntityAlias('taxonomy_term', $term2, 'parent-term/child-term'); - - $this->saveEntityAlias('taxonomy_term', $term1, 'My Crazy/Alias/'); - pathauto_taxonomy_term_update($term2); - $this->assertEntityAlias('taxonomy_term', $term2, 'My Crazy/Alias/child-term'); - } - - function testEntityBundleRenamingDeleting() { - // Create a vocabulary and test that it's pattern variable works. - $vocab = $this->addVocabulary(array('machine_name' => 'old_name')); - variable_set('pathauto_taxonomy_term_pattern', 'base'); - variable_set("pathauto_taxonomy_term_old_name_pattern", 'bundle'); - $this->assertEntityPattern('taxonomy_term', 'old_name', LANGUAGE_NONE, 'bundle'); - - // Rename the vocabulary's machine name, which should cause its pattern - // variable to also be renamed. - $vocab->machine_name = 'new_name'; - taxonomy_vocabulary_save($vocab); - $this->assertEntityPattern('taxonomy_term', 'new_name', LANGUAGE_NONE, 'bundle'); - $this->assertEntityPattern('taxonomy_term', 'old_name', LANGUAGE_NONE, 'base'); - - // Delete the vocabulary, which should cause its pattern variable to also - // be deleted. - taxonomy_vocabulary_delete($vocab->vid); - $this->assertEntityPattern('taxonomy_term', 'new_name', LANGUAGE_NONE, 'base'); - } - - function testNoExistingPathAliases() { - variable_set('pathauto_node_page_pattern', '[node:title]'); - variable_set('pathauto_punctuation_period', PATHAUTO_PUNCTUATION_DO_NOTHING); - - // Check that Pathauto does not create an alias of '/admin'. - $node = $this->drupalCreateNode(array('title' => 'Admin', 'type' => 'page')); - $this->assertEntityAlias('node', $node, 'admin-0'); - - // Check that Pathauto does not create an alias of '/modules'. - $node->title = 'Modules'; - node_save($node); - $this->assertEntityAlias('node', $node, 'modules-0'); - - // Check that Pathauto does not create an alias of '/index.php'. - $node->title = 'index.php'; - node_save($node); - $this->assertEntityAlias('node', $node, 'index.php-0'); - - // Check that a safe value gets an automatic alias. This is also a control - // to ensure the above tests work properly. - $node->title = 'Safe value'; - node_save($node); - $this->assertEntityAlias('node', $node, 'safe-value'); - } -} - -/** - * Helper test class with some added functions for testing. - */ -class PathautoFunctionalTestHelper extends PathautoTestHelper { - protected $admin_user; - - function setUp(array $modules = array()) { - parent::setUp($modules); - - // Set pathauto settings we assume to be as-is in this test. - variable_set('pathauto_node_page_pattern', 'content/[node:title]'); - - // Allow other modules to add additional permissions for the admin user. - $permissions = array( - 'administer pathauto', - 'administer url aliases', - 'create url aliases', - 'administer nodes', - 'bypass node access', - 'access content overview', - 'administer taxonomy', - 'administer users', - ); - $args = func_get_args(); - if (isset($args[1]) && is_array($args[1])) { - $permissions = array_merge($permissions, $args[1]); - } - $this->admin_user = $this->drupalCreateUser($permissions); - - $this->drupalLogin($this->admin_user); - } -} - -/** - * Test basic pathauto functionality. - */ -class PathautoFunctionalTestCase extends PathautoFunctionalTestHelper { - public static function getInfo() { - return array( - 'name' => 'Pathauto basic tests', - 'description' => 'Test basic pathauto functionality.', - 'group' => 'Pathauto', - 'dependencies' => array('token'), - ); - } - - /** - * Basic functional testing of Pathauto. - */ - function testNodeEditing() { - // Delete the default node pattern. Only the page content type will have a pattern. - variable_del('pathauto_node_pattern'); - - // Ensure that the Pathauto checkbox is checked by default on the node add form. - $this->drupalGet('node/add/page'); - $this->assertFieldChecked('edit-path-pathauto'); - - // Create node for testing by previewing and saving the node form. - $title = ' Testing: node title ['; - $automatic_alias = 'content/testing-node-title'; - $this->drupalPost(NULL, array('title' => $title), 'Preview'); - $this->drupalPost(NULL, array(), 'Save'); - $node = $this->drupalGetNodeByTitle($title); - - // Look for alias generated in the form. - $this->drupalGet("node/{$node->nid}/edit"); - $this->assertFieldChecked('edit-path-pathauto'); - $this->assertFieldByName('path[alias]', $automatic_alias, 'Generated alias visible in the path alias field.'); - - // Check whether the alias actually works. - $this->drupalGet($automatic_alias); - $this->assertText($title, 'Node accessible through automatic alias.'); - - // Disable the update action. The checkbox should not be visible. - variable_set('pathauto_update_action', 0); - $this->drupalGet("node/{$node->nid}/edit"); - $this->assertNoFieldById('edit-path-pathauto'); - - // Reset the update action back to default. The checkbox should be visible. - variable_del('pathauto_update_action'); - $this->drupalGet("node/{$node->nid}/edit"); - $this->assertFieldChecked('edit-path-pathauto'); - - // Manually set the node's alias. - $manual_alias = 'content/' . $node->nid; - $edit = array( - 'path[pathauto]' => FALSE, - 'path[alias]' => $manual_alias, - ); - $this->drupalPost(NULL, $edit, t('Save')); - $this->assertText("Basic page $title has been updated."); - - // Check that the automatic alias checkbox is now unchecked by default. - $this->drupalGet("node/{$node->nid}/edit"); - $this->assertNoFieldChecked('edit-path-pathauto'); - $this->assertFieldByName('path[alias]', $manual_alias); - - // Submit the node form with the default values. - $this->drupalPost(NULL, array(), t('Save')); - $this->assertText("Basic page $title has been updated."); - - // Test that the old (automatic) alias has been deleted and only accessible - // through the new (manual) alias. - $this->drupalGet($automatic_alias); - $this->assertResponse(404, 'Node not accessible through automatic alias.'); - $this->drupalGet($manual_alias); - $this->assertText($title, 'Node accessible through manual alias.'); - - // Now attempt to create a node that has no pattern (article content type). - // The Pathauto checkbox should not exist. - $this->drupalGet('node/add/article'); - $this->assertNoFieldById('edit-path-pathauto'); - $this->assertFieldByName('path[alias]', ''); - - $edit = array(); - $edit['title'] = 'My test article'; - $this->drupalPost(NULL, $edit, t('Save')); - $node = $this->drupalGetNodeByTitle($edit['title']); - - // Pathauto checkbox should still not exist. - $this->drupalGet('node/' . $node->nid . '/edit'); - $this->assertNoFieldById('edit-path-pathauto'); - $this->assertFieldByName('path[alias]', ''); - $this->assertNoEntityAlias('node', $node); - - // Set the page pattern to use only tokens so we can test the checkbox - // behavior if none of the tokens have a value currently. - variable_set('pathauto_node_page_pattern', '[node:title]'); - - // Create a node with an empty title. The Pathauto checkbox should still be - // visible but unchecked. - $node = $this->drupalCreateNode(array('type' => 'page', 'title' => '')); - $this->drupalGet('node/' . $node->nid . '/edit'); - $this->assertNoFieldChecked('edit-path-pathauto'); - $this->assertFieldByName('path[alias]', ''); - $this->assertNoEntityAlias('node', $node); - - $edit = array(); - $edit['title'] = 'Valid title'; - $edit['path[pathauto]'] = TRUE; - $this->drupalPost(NULL, $edit, t('Save')); - $this->drupalGet('node/' . $node->nid . '/edit'); - $this->assertFieldChecked('edit-path-pathauto'); - $this->assertFieldByName('path[alias]', 'valid-title'); - } - - /** - * Test node operations. - */ - function testNodeOperations() { - $node1 = $this->drupalCreateNode(array('title' => 'node1')); - $node2 = $this->drupalCreateNode(array('title' => 'node2')); - - // Delete all current URL aliases. - $this->deleteAllAliases(); - - $edit = array( - 'operation' => 'pathauto_update_alias', - "nodes[{$node1->nid}]" => TRUE, - ); - $this->drupalPost('admin/content', $edit, t('Update')); - $this->assertText('Updated URL alias for 1 node.'); - - $this->assertEntityAlias('node', $node1, 'content/' . $node1->title); - $this->assertEntityAlias('node', $node2, 'node/' . $node2->nid); - } - - /** - * Basic functional testing of Pathauto with taxonomy terms. - */ - function testTermEditing() { - $this->drupalGet('admin/structure'); - $this->drupalGet('admin/structure/taxonomy'); - - // Create term for testing. - $name = ' Testing: term name [ '; - $automatic_alias = 'tags/testing-term-name'; - $this->drupalPost('admin/structure/taxonomy/tags/add', array('name' => $name), 'Save'); - $name = trim($name); - $this->assertText("Created new term $name."); - $term = $this->drupalGetTermByName($name); - - // Look for alias generated in the form. - $this->drupalGet("taxonomy/term/{$term->tid}/edit"); - $this->assertFieldChecked('edit-path-pathauto'); - $this->assertFieldByName('path[alias]', $automatic_alias, 'Generated alias visible in the path alias field.'); - - // Check whether the alias actually works. - $this->drupalGet($automatic_alias); - $this->assertText($name, 'Term accessible through automatic alias.'); - - // Manually set the term's alias. - $manual_alias = 'tags/' . $term->tid; - $edit = array( - 'path[pathauto]' => FALSE, - 'path[alias]' => $manual_alias, - ); - $this->drupalPost("taxonomy/term/{$term->tid}/edit", $edit, t('Save')); - $this->assertText("Updated term $name."); - - // Check that the automatic alias checkbox is now unchecked by default. - $this->drupalGet("taxonomy/term/{$term->tid}/edit"); - $this->assertNoFieldChecked('edit-path-pathauto'); - $this->assertFieldByName('path[alias]', $manual_alias); - - // Submit the term form with the default values. - $this->drupalPost(NULL, array(), t('Save')); - $this->assertText("Updated term $name."); - - // Test that the old (automatic) alias has been deleted and only accessible - // through the new (manual) alias. - $this->drupalGet($automatic_alias); - $this->assertResponse(404, 'Term not accessible through automatic alias.'); - $this->drupalGet($manual_alias); - $this->assertText($name, 'Term accessible through manual alias.'); - } - - /** - * Basic functional testing of Pathauto with users. - */ - function testUserEditing() { - // There should be no Pathauto checkbox on user forms. - $this->drupalGet('user/' . $this->admin_user->uid . '/edit'); - $this->assertNoFieldById('edit-path-pathauto'); - } - - /** - * Test user operations. - */ - function testUserOperations() { - $account = $this->drupalCreateUser(); - - // Delete all current URL aliases. - $this->deleteAllAliases(); - - $edit = array( - 'operation' => 'pathauto_update_alias', - "accounts[{$account->uid}]" => TRUE, - ); - $this->drupalPost('admin/people', $edit, t('Update')); - $this->assertText('Updated URL alias for 1 user account.'); - - $this->assertEntityAlias('user', $account, 'users/' . drupal_strtolower($account->name)); - $this->assertEntityAlias('user', $this->admin_user, 'user/' . $this->admin_user->uid); - } - - function testSettingsValidation() { - $edit = array(); - $edit['pathauto_max_length'] = 'abc'; - $edit['pathauto_max_component_length'] = 'abc'; - $this->drupalPost('admin/config/search/path/settings', $edit, 'Save configuration'); - $this->assertText('The field Maximum alias length is not a valid number.'); - $this->assertText('The field Maximum component length is not a valid number.'); - $this->assertNoText('The configuration options have been saved.'); - - $edit['pathauto_max_length'] = '0'; - $edit['pathauto_max_component_length'] = '0'; - $this->drupalPost('admin/config/search/path/settings', $edit, 'Save configuration'); - $this->assertText('The field Maximum alias length cannot be less than 1.'); - $this->assertText('The field Maximum component length cannot be less than 1.'); - $this->assertNoText('The configuration options have been saved.'); - - $edit['pathauto_max_length'] = '999'; - $edit['pathauto_max_component_length'] = '999'; - $this->drupalPost('admin/config/search/path/settings', $edit, 'Save configuration'); - $this->assertText('The field Maximum alias length cannot be greater than 255.'); - $this->assertText('The field Maximum component length cannot be greater than 255.'); - $this->assertNoText('The configuration options have been saved.'); - - $edit['pathauto_max_length'] = '50'; - $edit['pathauto_max_component_length'] = '50'; - $this->drupalPost('admin/config/search/path/settings', $edit, 'Save configuration'); - $this->assertText('The configuration options have been saved.'); - } - - function testPatternsValidation() { - $edit = array(); - $edit['pathauto_node_pattern'] = '[node:title]/[user:name]/[term:name]'; - $edit['pathauto_node_page_pattern'] = 'page'; - $this->drupalPost('admin/config/search/path/patterns', $edit, 'Save configuration'); - $this->assertText('The Default path pattern (applies to all content types with blank patterns below) is using the following invalid tokens: [user:name], [term:name].'); - $this->assertText('The Pattern for all Basic page paths cannot contain fewer than one token.'); - $this->assertNoText('The configuration options have been saved.'); - - $edit['pathauto_node_pattern'] = '[node:title]'; - $edit['pathauto_node_page_pattern'] = 'page/[node:title]'; - $edit['pathauto_node_article_pattern'] = ''; - $this->drupalPost('admin/config/search/path/patterns', $edit, 'Save configuration'); - $this->assertText('The configuration options have been saved.'); - } - - /** - * Test programmatic entity creation for aliases. - */ - function testProgrammaticEntityCreation() { - $node = $this->drupalCreateNode(array('title' => 'Test node', 'path' => array('pathauto' => TRUE))); - $this->assertEntityAlias('node', $node, 'content/test-node'); - - $vocabulary = $this->addVocabulary(array('name' => 'Tags')); - $term = $this->addTerm($vocabulary, array('name' => 'Test term', 'path' => array('pathauto' => TRUE))); - $this->assertEntityAlias('taxonomy_term', $term, 'tags/test-term'); - - $edit['name'] = 'Test user'; - $edit['mail'] = 'test-user@example.com'; - $edit['pass'] = user_password(); - $edit['path'] = array('pathauto' => TRUE); - $edit['status'] = 1; - $account = user_save(drupal_anonymous_user(), $edit); - $this->assertEntityAlias('user', $account, 'users/test-user'); - } -} - -class PathautoLocaleTestCase extends PathautoFunctionalTestHelper { - public static function getInfo() { - return array( - 'name' => 'Pathauto localization tests', - 'description' => 'Test pathauto functionality with localization and translation.', - 'group' => 'Pathauto', - 'dependencies' => array('token'), - ); - } - - function setUp(array $modules = array()) { - $modules[] = 'locale'; - $modules[] = 'translation'; - parent::setUp($modules, array('administer languages')); - - // Add predefined French language and reset the locale cache. - require_once DRUPAL_ROOT . '/includes/locale.inc'; - locale_add_language('fr', NULL, NULL, LANGUAGE_LTR, '', 'fr'); - drupal_language_initialize(); - } - - /** - * Test that when an English node is updated, its old English alias is - * updated and its newer French alias is left intact. - */ - function testLanguageAliases() { - $node = array( - 'title' => 'English node', - 'language' => 'en', - 'body' => array('en' => array(array())), - 'path' => array( - 'alias' => 'english-node', - 'pathauto' => FALSE, - ), - ); - $node = $this->drupalCreateNode($node); - $english_alias = path_load(array('alias' => 'english-node', 'language' => 'en')); - $this->assertTrue($english_alias, 'Alias created with proper language.'); - - // Also save a French alias that should not be left alone, even though - // it is the newer alias. - $this->saveEntityAlias('node', $node, 'french-node', 'fr'); - - // Add an alias with the soon-to-be generated alias, causing the upcoming - // alias update to generate a unique alias with the '-0' suffix. - $this->saveAlias('node/invalid', 'content/english-node', LANGUAGE_NONE); - - // Update the node, triggering a change in the English alias. - $node->path['pathauto'] = TRUE; - pathauto_node_update($node); - - // Check that the new English alias replaced the old one. - $this->assertEntityAlias('node', $node, 'content/english-node-0', 'en'); - $this->assertEntityAlias('node', $node, 'french-node', 'fr'); - $this->assertAliasExists(array('pid' => $english_alias['pid'], 'alias' => 'content/english-node-0')); - } -} - -/** - * Bulk update functionality tests. - */ -class PathautoBulkUpdateTestCase extends PathautoFunctionalTestHelper { - private $nodes; - - public static function getInfo() { - return array( - 'name' => 'Pathauto bulk updating', - 'description' => 'Tests bulk updating of URL aliases.', - 'group' => 'Pathauto', - 'dependencies' => array('token'), - ); - } - - function testBulkUpdate() { - // Create some nodes. - $this->nodes = array(); - for ($i = 1; $i <= 5; $i++) { - $node = $this->drupalCreateNode(); - $this->nodes[$node->nid] = $node; - } - - // Clear out all aliases. - $this->deleteAllAliases(); - - // Bulk create aliases. - $edit = array( - 'update[node_pathauto_bulk_update_batch_process]' => TRUE, - 'update[user_pathauto_bulk_update_batch_process]' => TRUE, - ); - $this->drupalPost('admin/config/search/path/update_bulk', $edit, t('Update')); - $this->assertText('Generated 7 URL aliases.'); // 5 nodes + 2 users - - // Check that aliases have actually been created. - foreach ($this->nodes as $node) { - $this->assertEntityAliasExists('node', $node); - } - $this->assertEntityAliasExists('user', $this->admin_user); - - // Add a new node. - $new_node = $this->drupalCreateNode(array('path' => array('alias' => '', 'pathauto' => FALSE))); - - // Run the update again which should only run against the new node. - $this->drupalPost('admin/config/search/path/update_bulk', $edit, t('Update')); - $this->assertText('Generated 1 URL alias.'); // 1 node + 0 users - - $this->assertEntityAliasExists('node', $new_node); - } -} - -/** - * Token functionality tests. - */ -class PathautoTokenTestCase extends PathautoFunctionalTestHelper { - public static function getInfo() { - return array( - 'name' => 'Pathauto tokens', - 'description' => 'Tests tokens provided by Pathauto.', - 'group' => 'Pathauto', - 'dependencies' => array('token'), - ); - } - - function testPathautoTokens() { - $array = array( - 'test first arg', - 'The Array / value', - ); - - $tokens = array( - 'join-path' => 'test-first-arg/array-value', - ); - $data['array'] = $array; - $replacements = $this->assertTokens('array', $data, $tokens); - - // Ensure that the pathauto_clean_token_values() function does not alter - // this token value. - module_load_include('inc', 'pathauto'); - pathauto_clean_token_values($replacements, $data, array()); - $this->assertEqual($replacements['[array:join-path]'], 'test-first-arg/array-value'); - } - - /** - * Function copied from TokenTestHelper::assertTokens(). - */ - function assertTokens($type, array $data, array $tokens, array $options = array()) { - $input = $this->mapTokenNames($type, array_keys($tokens)); - $replacements = token_generate($type, $input, $data, $options); - foreach ($tokens as $name => $expected) { - $token = $input[$name]; - if (!isset($expected)) { - $this->assertTrue(!isset($values[$token]), t("Token value for @token was not generated.", array('@type' => $type, '@token' => $token))); - } - elseif (!isset($replacements[$token])) { - $this->fail(t("Token value for @token was not generated.", array('@type' => $type, '@token' => $token))); - } - elseif (!empty($options['regex'])) { - $this->assertTrue(preg_match('/^' . $expected . '$/', $replacements[$token]), t("Token value for @token was '@actual', matching regular expression pattern '@expected'.", array('@type' => $type, '@token' => $token, '@actual' => $replacements[$token], '@expected' => $expected))); - } - else { - $this->assertIdentical($replacements[$token], $expected, t("Token value for @token was '@actual', expected value '@expected'.", array('@type' => $type, '@token' => $token, '@actual' => $replacements[$token], '@expected' => $expected))); - } - } - - return $replacements; - } - - function mapTokenNames($type, array $tokens = array()) { - $return = array(); - foreach ($tokens as $token) { - $return[$token] = "[$type:$token]"; - } - return $return; - } -} diff --git a/pathauto.tokens.inc b/pathauto.tokens.inc index dabcaa8..5660c67 100644 --- a/pathauto.tokens.inc +++ b/pathauto.tokens.inc @@ -5,6 +5,8 @@ * Token integration for the Pathauto module. */ +use Drupal\Core\Render\Element; + /** * Implements hook_token_info(). */ @@ -33,9 +35,9 @@ function pathauto_tokens($type, $tokens, array $data = array(), array $options = case 'join-path': module_load_include('inc', 'pathauto'); $values = array(); - foreach (element_children($array) as $key) { + foreach (token_element_children($array) as $key) { $value = is_array($array[$key]) ? render($array[$key]) : (string) $array[$key]; - $value = pathauto_cleanstring($value, $options); + $value = \Drupal::service('pathauto.manager')->cleanString($value, $options); $values[] = $value; } $replacements[$original] = implode('/', $values); diff --git a/src/AliasCleaner.php b/src/AliasCleaner.php new file mode 100644 index 0000000..b1ab5cf --- /dev/null +++ b/src/AliasCleaner.php @@ -0,0 +1,109 @@ +configFactory = $config_factory; + $this->aliasStorageHelper = $alias_storage_helper; + } + + /** + * {@inheritdoc} + */ + public function cleanAlias($alias) { + if (!isset($this->aliasMaxLength)) { + $config = $this->configFactory->get('pathauto.settings'); + $this->aliasMaxLength = min($config->get('max_length'), $this->aliasStorageHelper->getAliasSchemaMaxLength()); + } + + $output = $alias; + + // Trim duplicate, leading, and trailing separators. Do this before cleaning + // backslashes since a pattern like "[token1]/[token2]-[token3]/[token4]" + // could end up like "value1/-/value2" and if backslashes were cleaned first + // this would result in a duplicate blackslash. + $output = $this->getCleanSeparators($output); + + // Trim duplicate, leading, and trailing backslashes. + $output = $this->getCleanSeparators($output, '/'); + + // Shorten to a logical place based on word boundaries. + $output = Unicode::truncate($output, $this->aliasMaxLength, TRUE); + + return $output; + } + + /** + * {@inheritdoc} + */ + public function getCleanSeparators($string, $separator = NULL) { + $config = $this->configFactory->get('pathauto.settings'); + + if (!isset($separator)) { + $separator = $config->get('separator'); + } + + $output = $string; + + if (strlen($separator)) { + // Trim any leading or trailing separators. + $output = trim($output, $separator); + + // Escape the separator for use in regular expressions. + $seppattern = preg_quote($separator, '/'); + + // Replace multiple separators with a single one. + $output = preg_replace("/$seppattern+/", $separator, $output); + + // Replace trailing separators around slashes. + if ($separator !== '/') { + $output = preg_replace("/\/+$seppattern\/+|$seppattern\/+|\/+$seppattern/", "/", $output); + } + } + + return $output; + } + +} diff --git a/src/AliasCleanerInterface.php b/src/AliasCleanerInterface.php new file mode 100644 index 0000000..3ffed9a --- /dev/null +++ b/src/AliasCleanerInterface.php @@ -0,0 +1,45 @@ +configFactory = $config_factory; + $this->aliasStorage = $alias_storage; + $this->database = $database; + $this->messenger = $messenger; + $this->stringTranslation = $string_translation; + } + + /** + * {@inheritdoc} + */ + public function getAliasSchemaMaxLength() { + if (!isset($this->aliasSchemaMaxLength)) { + $schema = drupal_get_schema('url_alias'); + $this->aliasSchemaMaxLength = $schema['fields']['alias']['length']; + } + return $this->aliasSchemaMaxLength; + } + + /** + * {@inheritdoc} + */ + public function save(array $path, $existing_alias = NULL, $op = NULL) { + $config = $this->configFactory->get('pathauto.settings'); + + // Alert users if they are trying to create an alias that is the same as the + // internal path. + if ($path['source'] == $path['alias']) { + $this->messenger->addMessage($this->t('Ignoring alias %alias because it is the same as the internal path.', array('%alias' => $path['alias']))); + return NULL; + } + + // 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, + 'pid' => NULL, + ); + + // If there is already an alias, respect some update actions. + if (!empty($existing_alias)) { + switch ($config->get('update_action')) { + case PathautoManagerInterface::UPDATE_ACTION_NO_NEW: + // Do not create the alias. + return NULL; + + case PathautoManagerInterface::UPDATE_ACTION_LEAVE: + // Create a new alias instead of overwriting the existing by leaving + // $path['pid'] empty. + break; + + case PathautoManagerInterface::UPDATE_ACTION_DELETE: + // The delete actions should overwrite the existing alias. + $path['pid'] = $existing_alias['pid']; + break; + } + } + + // Save the path array. + $this->aliasStorage->save($path['source'], $path['alias'], $path['language'], $path['pid']); + + if (!empty($existing_alias['pid'])) { + $this->messenger->addMessage($this->t( + 'Created new alias %alias for %source, replacing %old_alias.', + array( + '%alias' => $path['alias'], + '%source' => $path['source'], + '%old_alias' => $existing_alias['alias'], + ) + ) + ); + } + else { + $this->messenger->addMessage($this->t('Created new alias %alias for %source.', array( + '%alias' => $path['alias'], + '%source' => $path['source'], + ))); + } + + return $path; + } + } + + /** + * {@inheritdoc} + */ + public function loadBySource($source, $language = LanguageInterface::LANGCODE_NOT_SPECIFIED) { + // @todo convert this to be a query on alias storage. + $pid = $this->database->queryRange("SELECT pid FROM {url_alias} WHERE source = :source AND langcode IN (:language, :language_none) ORDER BY langcode DESC, pid DESC", 0, 1, array( + ':source' => $source, + ':language' => $language, + ':language_none' => LanguageInterface::LANGCODE_NOT_SPECIFIED, + ))->fetchField(); + return $this->aliasStorage->load(array('pid' => $pid)); + } + + /** + * {@inheritdoc} + */ + public function exists($alias, $source, $language = LanguageInterface::LANGCODE_NOT_SPECIFIED) { + return (bool) $this->database->queryRange("SELECT pid FROM {url_alias} WHERE source <> :source AND alias = :alias AND langcode IN (:language, :language_none) ORDER BY langcode DESC, pid DESC", 0, 1, array( + ':source' => $source, + ':alias' => $alias, + ':language' => $language, + ':language_none' => LanguageInterface::LANGCODE_NOT_SPECIFIED, + ))->fetchField(); + } + +} diff --git a/src/AliasStorageHelperInterface.php b/src/AliasStorageHelperInterface.php new file mode 100644 index 0000000..3ad7460 --- /dev/null +++ b/src/AliasStorageHelperInterface.php @@ -0,0 +1,74 @@ +alterInfo('pathauto_alias_types'); + $this->setCacheBackend($cache_backend, 'pathauto_alias_types'); + } + +} diff --git a/src/AliasUniquifier.php b/src/AliasUniquifier.php new file mode 100644 index 0000000..09eaac7 --- /dev/null +++ b/src/AliasUniquifier.php @@ -0,0 +1,154 @@ +configFactory = $config_factory; + $this->aliasStorageHelper = $alias_storage_helper; + $this->moduleHandler = $module_handler; + $this->urlMatcher = $url_matcher; + } + + /** + * {@inheritdoc} + */ + public function uniquify(&$alias, $source, $langcode) { + $config = $this->configFactory->get('pathauto.settings'); + + if (!$this->isReserved($alias, $source, $langcode)) { + return; + } + + // If the alias already exists, generate a new, hopefully unique, variant. + $maxlength = min($config->get('max_length'), $this->aliasStorageHelper->getAliasSchemaMaxlength()); + $separator = $config->get('separator'); + $original_alias = $alias; + + $i = 0; + do { + // Append an incrementing numeric suffix until we find a unique alias. + $unique_suffix = $separator . $i; + $alias = Unicode::truncate($original_alias, $maxlength - Unicode::strlen($unique_suffix, TRUE)) . $unique_suffix; + $i++; + } while ($this->isReserved($alias, $source, $langcode)); + } + + /** + * {@inheritdoc} + */ + public function isReserved($alias, $source, $langcode = LanguageInterface::LANGCODE_NOT_SPECIFIED) { + // First check whether the alias exists for another source. + if ($this->aliasStorageHelper->exists($alias, $source, $langcode)) { + return TRUE; + } + // Then check if there is a route with the same path. + if ($this->isRoute($alias)) { + return TRUE; + } + // Finally check if any other modules have reserved the alias. + $args = array( + $alias, + $source, + $langcode, + ); + $implementations = $this->moduleHandler->getImplementations('pathauto_is_alias_reserved'); + foreach ($implementations as $module) { + + $result = $this->moduleHandler->invoke($module, 'pathauto_is_alias_reserved', $args); + + if (!empty($result)) { + // As soon as the first module says that an alias is in fact reserved, + // then there is no point in checking the rest of the modules. + return TRUE; + } + } + + return FALSE; + } + + /** + * Verify if the given path is a valid route. + * + * Taken from menu_execute_active_handler(). + * + * @param string $path + * A string containing a relative path. + * + * @return bool + * TRUE if the path already exists. + */ + public function isRoute($path) { + if (is_file(DRUPAL_ROOT . '/' . $path) || is_dir(DRUPAL_ROOT . '/' . $path)) { + // Do not allow existing files or directories to get assigned an automatic + // alias. Note that we do not need to use is_link() to check for symbolic + // links since this returns TRUE for either is_file() or is_dir() already. + return TRUE; + } + + try { + $this->urlMatcher->match('/' . $path); + return TRUE; + } + catch (ResourceNotFoundException $e) { + return FALSE; + } + } + +} diff --git a/src/AliasUniquifierInterface.php b/src/AliasUniquifierInterface.php new file mode 100644 index 0000000..96411eb --- /dev/null +++ b/src/AliasUniquifierInterface.php @@ -0,0 +1,46 @@ + 'fieldset', + '#title' => t('Choose aliases to delete'), + '#collapsible' => FALSE, + '#collapsed' => FALSE, + ); + + // First we do the "all" case. + $total_count = db_query('SELECT count(1) FROM {url_alias}')->fetchField(); + $form['delete']['all_aliases'] = array( + '#type' => 'checkbox', + '#title' => t('All aliases'), + '#default_value' => FALSE, + '#description' => t('Delete all aliases. Number of aliases which will be deleted: %count.', array('%count' => $total_count)), + ); + + // Next, iterate over an array of objects/alias types + // which can be deleted and provide checkboxes. + $args = func_get_args(); + // Remove $hook from the arguments. + unset($args[0]); + $objects = \Drupal::moduleHandler()->invokeAll('path_alias_types', $args); + + foreach ($objects as $internal_name => $label) { + $count = db_query("SELECT count(1) FROM {url_alias} WHERE source LIKE :src", array(':src' => "$internal_name%"))->fetchField(); + $form['delete'][$internal_name] = array( + '#type' => 'checkbox', + '#title' => $label, // This label is sent through t() in the hard coded function where it is defined. + '#default_value' => FALSE, + '#description' => t('Delete aliases for all @label. Number of aliases which will be deleted: %count.', array('@label' => $label, '%count' => $count)), + ); + } + + // Warn them and give a button that shows we mean business. + $form['warning'] = array('#value' => '

' . t('Note: there is no confirmation. Be sure of your action before clicking the "Delete aliases now!" button.
You may want to make a backup of the database and/or the url_alias table prior to using this feature.') . '

'); + $form['buttons']['submit'] = array( + '#type' => 'submit', + '#value' => t('Delete aliases now!'), + ); + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + foreach ($form_state->getValues() as $key => $value) { + if ($value) { + if ($key === 'all_aliases') { + db_delete('url_alias') + ->execute(); + drupal_set_message(t('All of your path aliases have been deleted.')); + } + $args = func_get_args(); + // Remove $hook from the arguments. + unset($args[0]); + $objects = \Drupal::moduleHandler()->invokeAll('path_alias_types', $args); + if (array_key_exists($key, $objects)) { + db_delete('url_alias') + ->condition('source', db_like($key) . '%', 'LIKE') + ->execute(); + drupal_set_message(t('All of your %type path aliases have been deleted.', array('%type' => $objects[$key]))); + } + } + } + $form_state->setRedirect('pathauto.bulk.update.form'); + } + +} diff --git a/src/Form/PathautoBulkUpdateForm.php b/src/Form/PathautoBulkUpdateForm.php new file mode 100644 index 0000000..8b7cd25 --- /dev/null +++ b/src/Form/PathautoBulkUpdateForm.php @@ -0,0 +1,127 @@ + 'checkboxes', + '#title' => t('Select the types of un-aliased paths for which to generate URL aliases'), + '#options' => array(), + '#default_value' => array(), + ); + + $pathauto_settings = \Drupal::moduleHandler()->invokeAll('pathauto', array('settings')); + + foreach ($pathauto_settings as $settings) { + if (!empty($settings->batch_update_callback)) { + $form['#update_callbacks'][$settings->batch_update_callback] = $settings; + $form['update']['#options'][$settings->batch_update_callback] = $settings->groupheader; + } + } + + $form['actions']['#type'] = 'actions'; + $form['actions']['submit'] = array( + '#type' => 'submit', + '#value' => t('Update'), + ); + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $batch = array( + 'title' => t('Bulk updating URL aliases'), + 'operations' => array( + array('Drupal\pathauto\Form\PathautoBulkUpdateForm::batchStart', array()), + ), + 'finished' => 'Drupal\pathauto\Form\PathautoBulkUpdateForm::batchFinished', + ); + + foreach ($form_state->getValue('update') as $callback) { + if (!empty($callback)) { + $settings = $form['#update_callbacks'][$callback]; + if (!empty($settings->batch_file)) { + $batch['operations'][] = array('Drupal\pathauto\Form\PathautoBulkUpdateForm::batchProcess', array($callback, $settings)); + } + else { + $batch['operations'][] = array($callback, array()); + } + } + } + + batch_set($batch); + } + + /** + * Batch callback; count the current number of URL aliases for comparison later. + */ + public static function batchStart(&$context) { + $context['results']['count_before'] = db_select('url_alias')->countQuery()->execute()->fetchField(); + } + + /** + * Common batch processing callback for all operations. + * + * Required to load our include the proper batch file. + */ + public static function batchProcess($callback, $settings, &$context) { + if (!empty($settings->batch_file)) { + require_once DRUPAL_ROOT . '/' . $settings->batch_file; + } + return $callback($context); + } + + /** + * Batch finished callback. + */ + public static function batchFinished($success, $results, $operations) { + if ($success) { + // Count the current number of URL aliases after the batch is completed + // and compare to the count before the batch started. + $results['count_after'] = db_select('url_alias')->countQuery()->execute()->fetchField(); + $results['count_changed'] = max($results['count_after'] - $results['count_before'], 0); + if ($results['count_changed']) { + drupal_set_message(\Drupal::translation()->formatPlural($results['count_changed'], 'Generated 1 URL alias.', 'Generated @count URL aliases.')); + } + else { + drupal_set_message(t('No new URL aliases to generate.')); + } + } + else { + $error_operation = reset($operations); + drupal_set_message(t('An error occurred while processing @operation with arguments : @args', array('@operation' => $error_operation[0], '@args' => print_r($error_operation[0], TRUE)))); + } + } + +} diff --git a/src/Form/PathautoPatternsForm.php b/src/Form/PathautoPatternsForm.php new file mode 100644 index 0000000..a00d683 --- /dev/null +++ b/src/Form/PathautoPatternsForm.php @@ -0,0 +1,158 @@ +aliasTypeManager = $alias_type_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('config.factory'), + $container->get('plugin.manager.alias_type') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'pathauto_patterns_form'; + } + + /** + * {@inheritdoc} + */ + protected function getEditableConfigNames() { + return ['pathauto.pattern']; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + + $definitions = $this->aliasTypeManager->getDefinitions(); + + $config = $this->config('pathauto.pattern'); + + $all_settings = \Drupal::moduleHandler()->invokeAll('pathauto', array('settings')); + + foreach ($all_settings as $settings) { + $module = $settings->module; + $patterndescr = $settings->patterndescr; + $groupheader = $settings->groupheader; + + $form[$module] = array( + '#type' => 'fieldset', + '#title' => $groupheader, + '#collapsible' => TRUE, + '#collapsed' => FALSE, + '#tree' => TRUE, + ); + + // Prompt for the default pattern for this module. + $key = 'default'; + + $form[$module][$key] = array( + '#type' => 'textfield', + '#title' => $patterndescr, + '#default_value' => $config->get('patterns.' . $module . '.' . $key), + '#size' => 65, + '#maxlength' => 1280, + '#element_validate' => array('token_element_validate'), + '#after_build' => array('token_element_validate'), + '#token_types' => array($settings->token_type), + '#min_tokens' => 1, + ); + + // If the module supports a set of specialized patterns, set + // them up here. + if (isset($settings->patternitems)) { + foreach ($settings->patternitems as $itemname => $itemlabel) { + $key = 'default'; + + $form[$module]['bundles'][$itemname][$key] = array( + '#type' => 'textfield', + '#title' => $itemlabel, + '#default_value' => $config->get('patterns.'. $module . '.bundles.' . $itemname . '.' . $key), + '#size' => 65, + '#maxlength' => 1280, + '#element_validate' => array('token_element_validate'), + '#after_build' => array('token_element_validate'), + '#token_types' => array($settings->token_type), + '#min_tokens' => 1, + ); + } + } + + // Display the user documentation of placeholders supported by + // this module, as a description on the last pattern. + $form[$module]['token_help'] = array( + '#title' => t('Replacement patterns'), + '#type' => 'fieldset', + '#collapsible' => TRUE, + '#collapsed' => TRUE, + ); + $form[$module]['token_help']['help'] = array( + '#theme' => 'token_tree', + '#token_types' => array($settings->token_type), + ); + } + + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + + $config = $this->config('pathauto.pattern'); + + $all_settings = \Drupal::moduleHandler()->invokeAll('pathauto', array('settings')); + + foreach ($all_settings as $settings) { + $module = $settings->module; + $config->set('patterns.' . $module, $form_state->getValue($module)); + } + + $config->save(); + + parent::submitForm($form, $form_state); + } + +} diff --git a/src/Form/PathautoSettingsForm.php b/src/Form/PathautoSettingsForm.php new file mode 100644 index 0000000..843f4cd --- /dev/null +++ b/src/Form/PathautoSettingsForm.php @@ -0,0 +1,192 @@ +config('pathauto.settings'); + + $form = array(); + + $form['verbose'] = array( + '#type' => 'checkbox', + '#title' => t('Verbose'), + '#default_value' => $config->get('verbose'), + '#description' => t('Display alias changes (except during bulk updates).'), + ); + + $form['separator'] = array( + '#type' => 'textfield', + '#title' => t('Separator'), + '#size' => 1, + '#maxlength' => 1, + '#default_value' => $config->get('separator'), + '#description' => t('Character used to separate words in titles. This will replace any spaces and punctuation characters. Using a space or + character can cause unexpected results.'), + ); + + $form['case'] = array( + '#type' => 'radios', + '#title' => t('Character case'), + '#default_value' => $config->get('case'), + '#options' => array( + self::CASE_LEAVE_ASIS => t('Leave case the same as source token values.'), + self::CASE_LOWER => t('Change to lower case'), + ), + ); + + $max_length = \Drupal::service('pathauto.alias_storage_helper')->getAliasSchemaMaxlength(); + + $form['max_length'] = array( + '#type' => 'number', + '#title' => t('Maximum alias length'), + '#size' => 3, + '#maxlength' => 3, + '#default_value' => $config->get('max_length'), + '#min' => 1, + '#max' => $max_length, + '#description' => t('Maximum length of aliases to generate. 100 is the recommended length. @max is the maximum possible length. See Pathauto help for details.', array('@pathauto-help' => $this->getUrlGenerator()->generateFromPath('admin/help/pathauto'), '@max' => $max_length)), + ); + + $form['max_component_length'] = array( + '#type' => 'number', + '#title' => t('Maximum component length'), + '#size' => 3, + '#maxlength' => 3, + '#default_value' => $config->get('max_component_length'), + '#min' => 1, + '#max' => $max_length, + '#description' => t('Maximum text length of any component in the alias (e.g., [title]). 100 is the recommended length. @max is the maximum possible length. See Pathauto help for details.', array('@pathauto-help' => $this->getUrlGenerator()->generateFromPath('admin/help/pathauto'), '@max' => $max_length)), + ); + + $description = t('What should Pathauto do when updating an existing content item which already has an alias?'); + if (\Drupal::moduleHandler()->moduleExists('redirect')) { + $description .= ' ' . t('The Redirect module settings affect whether a redirect is created when an alias is deleted.', array('!url' => url('admin/config/search/redirect/settings'))); + } + else { + $description .= ' ' . t('Considering installing the Redirect module to get redirects when your aliases change.', array('!url' => 'http://drupal.org/project/redirect')); + } + + $form['update_action'] = array( + '#type' => 'radios', + '#title' => t('Update action'), + '#default_value' => $config->get('update_action'), + '#options' => array( + PathautoManagerInterface::UPDATE_ACTION_NO_NEW => t('Do nothing. Leave the old alias intact.'), + PathautoManagerInterface::UPDATE_ACTION_LEAVE => t('Create a new alias. Leave the existing alias functioning.'), + PathautoManagerInterface::UPDATE_ACTION_DELETE => t('Create a new alias. Delete the old alias.'), + ), + '#description' => $description, + ); + + $form['transliterate'] = array( + '#type' => 'checkbox', + '#title' => t('Transliterate prior to creating alias'), + '#default_value' => $config->get('transliterate'), + '#description' => t('When a pattern includes certain characters (such as those with accents) should Pathauto attempt to transliterate them into the US-ASCII alphabet? Transliteration is handled by the Transliteration module.'), + ); + + $form['reduce_ascii'] = array( + '#type' => 'checkbox', + '#title' => t('Reduce strings to letters and numbers'), + '#default_value' => $config->get('reduce_ascii'), + '#description' => t('Filters the new alias to only letters and numbers found in the ASCII-96 set.'), + ); + + $form['ignore_words'] = array( + '#type' => 'textarea', + '#title' => t('Strings to Remove'), + '#default_value' => $config->get('ignore_words'), + '#description' => t('Words to strip out of the URL alias, separated by commas. Do not use this to remove punctuation.'), + '#wysiwyg' => FALSE, + ); + + $form['punctuation'] = array( + '#type' => 'fieldset', + '#title' => t('Punctuation'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#tree' => TRUE, + ); + + $punctuation = \Drupal::service('pathauto.manager')->getPunctuationCharacters(); + + foreach ($punctuation as $name => $details) { + $details['default'] = PathautoManagerInterface::PUNCTUATION_REMOVE; + if ($details['value'] == $config->get('separator')) { + $details['default'] = PathautoManagerInterface::PUNCTUATION_REPLACE; + } + $form['punctuation']['punctuation' . $name] = array( + '#type' => 'select', + '#title' => $details['name'] . ' (' . String::checkPlain($details['value']) . ')', + '#default_value' => $details['default'], + '#options' => array( + PathautoManagerInterface::PUNCTUATION_REMOVE => t('Remove'), + PathautoManagerInterface::PUNCTUATION_REPLACE => t('Replace by separator'), + PathautoManagerInterface::PUNCTUATION_DO_NOTHING => t('No action (do not replace)'), + ), + ); + } + + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + + $config = $this->config('pathauto.settings'); + + $form_state->cleanValues(); + foreach ($form_state->getValues() as $key => $value) { + $config->set($key, $value); + } + + $config->save(); + + parent::submitForm($form, $form_state); + } + +} diff --git a/src/MessengerInterface.php b/src/MessengerInterface.php new file mode 100644 index 0000000..4e43319 --- /dev/null +++ b/src/MessengerInterface.php @@ -0,0 +1,25 @@ +pathauto) || empty($this->pathauto)) { + parent::insert(); + } + } + + /** + * {@inheritdoc} + */ + public function update() { + // Only allow the parent implementation to act if pathauto will not create + // an alias. + if (!isset($this->pathauto) || empty($this->pathauto)) { + parent::update(); + } + } + + /** + * {@inheritdoc} + */ + public function delete() { + pathauto_entity_path_delete_all($this->getEntity()); + } + +} diff --git a/src/PathautoManager.php b/src/PathautoManager.php new file mode 100644 index 0000000..2ac89d8 --- /dev/null +++ b/src/PathautoManager.php @@ -0,0 +1,547 @@ +configFactory = $config_factory; + $this->languageManager = $language_manager; + $this->cacheBackend = $cache_backend; + $this->moduleHandler = $module_handler; + $this->token = $token; + $this->aliasCleaner = $alias_cleaner; + $this->aliasStorageHelper = $alias_storage_helper; + $this->aliasUniquifier = $alias_uniquifier; + $this->messenger = $messenger; + $this->stringTranslation = $string_translation; + } + + /** + * {@inheritdoc} + */ + public function cleanString($string, array $options = array()) { + if (empty($this->cleanStringCache)) { + // Generate and cache variables used in this method. + $config = $this->configFactory->get('pathauto.settings'); + $this->cleanStringCache = array( + 'separator' => $config->get('separator'), + 'strings' => array(), + 'transliterate' => $config->get('transliterate'), + 'punctuation' => array(), + 'reduce_ascii' => (bool) $config->get('reduce_ascii'), + 'ignore_words_regex' => FALSE, + 'lowercase' => (bool) $config->get('case'), + 'maxlength' => min($config->get('max_component_length'), $this->aliasStorageHelper->getAliasSchemaMaxLength()), + ); + + // Generate and cache the punctuation replacements for strtr(). + $punctuation = $this->getPunctuationCharacters(); + foreach ($punctuation as $name => $details) { + $action = $config->get('punctuation.' . $name); + switch ($action) { + case PathautoManagerInterface::PUNCTUATION_REMOVE: + $cache['punctuation'][$details['value']] = ''; + $this->cleanStringCache; + + case PathautoManagerInterface::PUNCTUATION_REPLACE: + $this->cleanStringCache['punctuation'][$details['value']] = $this->cleanStringCache['separator']; + break; + + case PathautoManagerInterface::PUNCTUATION_DO_NOTHING: + // Literally do nothing. + break; + } + } + + // Generate and cache the ignored words regular expression. + $ignore_words = $config->get('ignore_words'); + $ignore_words_regex = preg_replace(array('/^[,\s]+|[,\s]+$/', '/[,\s]+/'), array('', '\b|\b'), $ignore_words); + if ($ignore_words_regex) { + $this->cleanStringCache['ignore_words_regex'] = '\b' . $ignore_words_regex . '\b'; + if (function_exists('mb_eregi_replace')) { + $this->cleanStringCache['ignore_words_callback'] = 'mb_eregi_replace'; + } + else { + $this->cleanStringCache['ignore_words_callback'] = 'preg_replace'; + $this->cleanStringCache['ignore_words_regex'] = '/' . $this->cleanStringCache['ignore_words_regex'] . '/i'; + } + } + } + + // Empty strings do not need any proccessing. + if ($string === '' || $string === NULL) { + return ''; + } + + $langcode = NULL; + if (!empty($options['language']->language)) { + $langcode = $options['language']->language; + } + elseif (!empty($options['langcode'])) { + $langcode = $options['langcode']; + } + + // Check if the string has already been processed, and if so return the + // cached result. + if (isset($this->cleanStringCache['strings'][$langcode][$string])) { + return $this->cleanStringCache['strings'][$langcode][$string]; + } + + // Remove all HTML tags from the string. + $output = strip_tags(String::decodeEntities($string)); + + // Optionally transliterate. + if ($this->cleanStringCache['transliterate']) { + // If the reduce strings to letters and numbers is enabled, don't bother + // replacing unknown characters with a question mark. Use an empty string + // instead. + $output = \Drupal::service('transliteration')->transliterate($output, $this->cleanStringCache['reduce_ascii'] ? '' : '?', $langcode); + } + + // Replace or drop punctuation based on user settings. + $output = strtr($output, $this->cleanStringCache['punctuation']); + + // Reduce strings to letters and numbers. + if ($this->cleanStringCache['reduce_ascii']) { + $output = preg_replace('/[^a-zA-Z0-9\/]+/', $this->cleanStringCache['separator'], $output); + } + + // Get rid of words that are on the ignore list. + if ($this->cleanStringCache['ignore_words_regex']) { + $words_removed = $this->cleanStringCache['ignore_words_callback']($this->cleanStringCache['ignore_words_regex'], '', $output); + if (Unicode::strlen(trim($words_removed)) > 0) { + $output = $words_removed; + } + } + + // Always replace whitespace with the separator. + $output = preg_replace('/\s+/', $this->cleanStringCache['separator'], $output); + + // Trim duplicates and remove trailing and leading separators. + $output = $this->aliasCleaner->getCleanSeparators($this->aliasCleaner->getCleanSeparators($output, $this->cleanStringCache['separator'])); + + // Optionally convert to lower case. + if ($this->cleanStringCache['lowercase']) { + $output = Unicode::strtolower($output); + } + + // Shorten to a logical place based on word boundaries. + $output = Unicode::truncate($output, $this->cleanStringCache['maxlength'], TRUE); + + // Cache this result in the static array. + $this->cleanStringCache['strings'][$langcode][$string] = $output; + + return $output; + } + + + /** + * {@inheritdoc} + */ + public function getPunctuationCharacters() { + if (empty($this->punctuationCharacters)) { + $langcode = $this->languageManager->getCurrentLanguage()->getId(); + + $cid = 'pathauto:punctuation:' . $langcode; + if ($cache = $this->cacheBackend->get($cid)) { + $this->punctuationCharacters = $cache->data; + } + else { + $punctuation = array(); + $punctuation['double_quotes'] = array('value' => '"', 'name' => t('Double quotation marks')); + $punctuation['quotes'] = array('value' => '\'', 'name' => t("Single quotation marks (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('Vertical bar (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 sign')); + $punctuation['equal'] = array('value' => '=', 'name' => t('Equal sign')); + $punctuation['asterisk'] = array('value' => '*', 'name' => t('Asterisk')); + $punctuation['ampersand'] = array('value' => '&', 'name' => t('Ampersand')); + $punctuation['percent'] = array('value' => '%', 'name' => t('Percent sign')); + $punctuation['caret'] = array('value' => '^', 'name' => t('Caret')); + $punctuation['dollar'] = array('value' => '$', 'name' => t('Dollar sign')); + $punctuation['hash'] = array('value' => '#', 'name' => t('Number sign (pound sign, hash)')); + $punctuation['at'] = array('value' => '@', 'name' => t('At sign')); + $punctuation['exclamation'] = array('value' => '!', 'name' => t('Exclamation mark')); + $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 sign')); + $punctuation['greater_than'] = array('value' => '>', 'name' => t('Greater-than sign')); + $punctuation['slash'] = array('value' => '/', 'name' => t('Slash')); + $punctuation['back_slash'] = array('value' => '\\', 'name' => t('Backslash')); + + // Allow modules to alter the punctuation list and cache the result. + $this->moduleHandler->alter('pathauto_punctuation_chars', $punctuation); + $this->cacheBackend->set($cid, $punctuation); + $this->punctuationCharacters = $punctuation; + } + } + + return $this->punctuationCharacters; + } + + + /** + * {@inheritdoc} + */ + public function createAlias($module, $op, $source, $data, $type = NULL, $language = LanguageInterface::LANGCODE_NOT_SPECIFIED) { + $config = $this->configFactory->get('pathauto.settings'); + + // Retrieve and apply the pattern for this content type. + $pattern = $this->getPatternByEntity($module, $type, $language); + + // Allow other modules to alter the pattern. + $context = array( + 'module' => $module, + 'op' => $op, + 'source' => $source, + 'data' => $data, + 'type' => $type, + 'language' => &$language, + ); + $this->moduleHandler->alter('pathauto_pattern', $pattern, $context); + + if (empty($pattern)) { + // No pattern? Do nothing (otherwise we may blow away existing aliases...) + return NULL; + } + + // Special handling when updating an item which is already aliased. + $existing_alias = NULL; + if ($op == 'update' || $op == 'bulkupdate') { + if ($existing_alias = $this->aliasStorageHelper->loadBySource($source, $language)) { + switch ($config->get('update_action')) { + case PathautoManagerInterface::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 NULL; + } + } + } + + // Replace any tokens in the pattern. + // Uses callback option to clean replacements. No sanitization. + $alias = $this->token->replace($pattern, $data, array( + 'sanitize' => FALSE, + 'clear' => TRUE, + 'callback' => array($this, 'cleanTokenValues'), + '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 NULL; + } + + $alias = $this->aliasCleaner->cleanAlias($alias); + + // Allow other modules to alter the alias. + $context['source'] = &$source; + $context['pattern'] = $pattern; + $this->moduleHandler->alter('pathauto_alias', $alias, $context); + + // If we have arrived at an empty string, discontinue. + if (!Unicode::strlen($alias)) { + return NULL; + } + + // If the alias already exists, generate a new, hopefully unique, variant. + $original_alias = $alias; + $this->aliasUniquifier->uniquify($alias, $source, $language); + if ($original_alias != $alias) { + // Alert the user why this happened. + $this->messenger->addMessage($this->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, + ); + + return $this->aliasStorageHelper->save($path, $existing_alias, $op); + } + + /** + * {@inheritdoc} + */ + public function getPatternByEntity($entity_type_id, $bundle = '', $language = LanguageInterface::LANGCODE_NOT_SPECIFIED) { + $config = $this->configFactory->get('pathauto.pattern'); + + $pattern_id = "$entity_type_id:$bundle:$language"; + if (!isset($this->patterns[$pattern_id])) { + $pattern = ''; + $variables = array(); + if ($language != LanguageInterface::LANGCODE_NOT_SPECIFIED) { + $variables[] = "{$entity_type_id}.bundles.{$bundle}.languages.{$language}"; + } + if ($bundle) { + $variables[] = "{$entity_type_id}.bundles.{$bundle}.default"; + } + $variables[] = "{$entity_type_id}.default"; + + foreach ($variables as $variable) { + if ($pattern = trim($config->get('patterns.' . $variable))) { + break; + } + } + + $this->patterns[$pattern_id] = $pattern; + } + + return $this->patterns[$pattern_id]; + } + + /** + * Resets internal caches. + */ + public function resetCaches() { + $this->patterns = array(); + $this->cleanStringCache = array(); + } + + /** + * {@inheritdoc} + */ + public function updateAlias(EntityInterface $entity, $op, array $options = array()) { + // Skip if the entity does not have the path field. + if (!($entity instanceof ContentEntityInterface) || !$entity->hasField('path')) { + return NULL; + } + + // Skip if pathauto processing is disabled. + if (isset($entity->path->pathauto) && empty($entity->path->pathauto) && empty($options['force'])) { + return NULL; + } + + $options += array('language' => $entity->language()->getId()); + $type = $entity->getEntityTypeId(); + $bundle = $entity->bundle(); + + // Skip processing if the entity has no pattern. + if (!$this->getPatternByEntity($type, $bundle, $options['language'])) { + return NULL; + } + + // Deal with taxonomy specific logic. + if ($type == 'taxonomy_term') { + + $config_forum = $this->configFactory->get('forum.settings'); + if ($entity->getVocabularyId() == $config_forum->get('vocabulary')) { + $type = 'forum'; + } + } + + $result = $this->createAlias( + $type, $op, $entity->getSystemPath(), array($type => $entity), $bundle, $options['language']); + + if ($type == 'taxonomy_term' && empty($options['is_child'])) { + // For all children generate new aliases. + $options['is_child'] = TRUE; + unset($options['language']); + foreach ($this->getTermTree($entity->getVocabularyId(), $entity->id(), NULL, TRUE) as $subterm) { + $this->updateAlias($subterm, $op, $options); + } + } + + return $result; + } + + /** + * Create a hierarchical representation of a vocabulary. + * + * Wrapper of taxonomy_get_tree() for testing. + * + * @param int $vid + * The vocabulary ID to generate the tree for. + * @param int $parent + * The term ID under which to generate the tree. If 0, generate the tree + * for the entire vocabulary. + * @param int $max_depth + * The number of levels of the tree to return. Leave NULL to return all levels. + * @param bool $load_entities + * If TRUE, a full entity load will occur on the term objects. Otherwise they + * are partial objects queried directly from the {taxonomy_term_field_data} + * table to save execution time and memory consumption when listing large + * numbers of terms. Defaults to FALSE. + * + * @return array + * An array of all term objects in the tree. Each term object is extended + * to have "depth" and "parents" attributes in addition to its normal ones. + * Results are statically cached. Term objects will be partial or complete + * depending on the $load_entities parameter. + */ + protected function getTermTree($vid, $parent = 0, $max_depth = NULL, $load_entities = FALSE) { + return taxonomy_get_tree($vid, $parent, $max_depth, $load_entities); + } + + /** + * {@inheritdoc} + */ + public function cleanTokenValues(&$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] = $this->cleanString($value, $options); + } + } + } +} diff --git a/src/PathautoManagerInterface.php b/src/PathautoManagerInterface.php new file mode 100644 index 0000000..95a836f --- /dev/null +++ b/src/PathautoManagerInterface.php @@ -0,0 +1,167 @@ +type). + * @param string $language + * A string specify the path's language. + * + * @return array|string + * The alias that was created. + * + * @see _pathauto_set_alias() + */ + public function createAlias($module, $op, $source, $data, $type = NULL, $language = LanguageInterface::LANGCODE_NOT_SPECIFIED); + + /** + * 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 array + * An array of arrays for punctuation values keyed by a name, including the + * value and a textual description. + */ + public function getPunctuationCharacters(); + + /** + * Creates or updates an alias for the given entity. + * + * @param EntityInterface $entity + * Entity for which to update the alias. + * @param string $op + * The operation performed (insert, update) + * @param array $options + * - force: will force updating the path + * - language: the language for which to create the alias + * + * @return array|null + * - An array with alias data in case the alias has been created or updated. + * - NULL if no operation performed. + */ + public function updateAlias(EntityInterface $entity, $op, array $options = array()); + + /** + * Clean tokens so they are URL friendly. + * + * @param array $replacements + * An array of token replacements + * that need to be "cleaned" for use in the URL. + * @param array $data + * An array of objects used to generate the replacements. + * @param array $options + * An array of options used to generate the replacements. + */ + public function cleanTokenValues(&$replacements, $data = array(), $options = array()); + +} diff --git a/src/Plugin/Action/UpdateAction.php b/src/Plugin/Action/UpdateAction.php new file mode 100644 index 0000000..fd7b102 --- /dev/null +++ b/src/Plugin/Action/UpdateAction.php @@ -0,0 +1,39 @@ +path = new \stdClass(); + $entity->path->pathauto = TRUE; + \Drupal::service('pathauto.manager')->updateAlias($entity, 'bulkupdate', array('message' => TRUE)); + } + + /** + * {@inheritdoc} + */ + public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) { + $result = AccessResult::allowedIfHasPermission($account, 'create url aliases'); + return $return_as_object ? $result : $result->isAllowed(); + } +} diff --git a/src/Plugin/Field/FieldWidget/PathautoWidget.php b/src/Plugin/Field/FieldWidget/PathautoWidget.php new file mode 100644 index 0000000..8acde06 --- /dev/null +++ b/src/Plugin/Field/FieldWidget/PathautoWidget.php @@ -0,0 +1,105 @@ +getEntity(); + + // Taxonomy terms do not have an actual fieldset for path settings. + // Merge in the defaults. + // @todo Impossible to do this in widget, use another solution + /* + $form['path'] += array( + '#type' => 'fieldset', + '#title' => $this->t('URL path settings'), + '#collapsible' => TRUE, + '#collapsed' => empty($form['path']['alias']), + '#group' => 'additional_settings', + '#attributes' => array( + 'class' => array('path-form'), + ), + '#access' => \Drupal::currentUser()->hasPermission('create url aliases') || \Drupal::currentUser()->hasPermission('administer url aliases'), + '#weight' => 30, + '#tree' => TRUE, + '#element_validate' => array('path_form_element_validate'), + );*/ + + + + $pattern = \Drupal::service('pathauto.manager')->getPatternByEntity($entity->getEntityTypeId(), $entity->bundle(), $entity->language()->getId()); + if (empty($pattern)) { + return $element; + } + + + if (!isset($entity->path->pathauto)) { + if (!$entity->isNew()) { + module_load_include('inc', 'pathauto'); + $path = \Drupal::service('path.alias_manager')->getAliasByPath($entity->getSystemPath(), $entity->language()->getId()); + $pathauto_alias = \Drupal::service('pathauto.manager')->createAlias($entity->getEntityTypeId(), 'return', $entity->getSystemPath(), array($entity->getEntityType()->id() => $entity), $entity->bundle(), $entity->language()->getId()); + $entity->path->pathauto = ($path != $entity->getSystemPath() && $path == $pathauto_alias); + } + else { + $entity->path->pathauto = TRUE; + } + } + $element['pathauto'] = array( + '#type' => 'checkbox', + '#title' => $this->t('Generate automatic URL alias'), + '#default_value' => $entity->path->pathauto, + '#description' => $this->t('Uncheck this to create a custom alias below. Configure URL alias patterns.', array('@admin_link' => \Drupal::url('pathauto.patterns.form'))), + '#weight' => -1, + ); + + // Add JavaScript that will disable the path textfield when the automatic + // alias checkbox is checked. + $element['alias']['#states']['!enabled']['input[name="path[pathauto]"]'] = array('checked' => TRUE); + + + // Override path.module's vertical tabs summary. + $element['alias']['#attached']['library'] = ['pathauto/widget']; + + if ($entity->path->pathauto && !empty($entity->old_alias) && empty($entity->path->alias)) { + $element['alias']['#default_value'] = $entity->old_alias; + $entity->path->alias = $entity->old_alias; + } + + + // For Pathauto to remember the old alias and prevent the Path module from + // deleting it when Pathauto wants to preserve it. + if (!empty($entity->path->alias)) { + $element['old_alias'] = array( + '#type' => 'value', + '#value' => $entity->path->alias, + ); + } + + return $element; + } +} diff --git a/src/Plugin/pathauto/AliasType/AliasTypeBase.php b/src/Plugin/pathauto/AliasType/AliasTypeBase.php new file mode 100644 index 0000000..174e936 --- /dev/null +++ b/src/Plugin/pathauto/AliasType/AliasTypeBase.php @@ -0,0 +1,138 @@ +configuration; + } + + /** + * {@inheritdoc} + */ + public function setConfiguration(array $configuration) { + $this->configuration = $configuration; + } + + /** + * {@inheritdoc} + */ + public function defaultConfiguration() { + return array(); + } + + /** + * {@inheritdoc} + */ + public function getLabel() { + $definition = $this->getPluginDefinition(); + // Cast the admin label to a string since it is an object. + // @see \Drupal\Core\StringTranslation\TranslationWrapper + return (string) $definition['label']; + } + + /** + * {@inheritdoc} + */ + public function getTokenTypes() { + $definition = $this->getPluginDefinition(); + return $definition['types']; + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + $plugin_id = $this->getPluginId(); + + $form[$plugin_id] = array( + '#type' => 'fieldset', + '#title' => $this->getLabel(), + '#collapsible' => TRUE, + '#collapsed' => FALSE, + '#tree' => TRUE, + ); + + // Prompt for the default pattern for this module. + $key = '_default'; + + $form[$plugin_id][$key] = array( + '#type' => 'textfield', + '#title' => $this->getPatternDescription(), + '#default_value' => $this->configuration['patternitems'], + '#size' => 65, + '#maxlength' => 1280, + '#element_validate' => array('token_element_validate'), + '#after_build' => array('token_element_validate'), + '#token_types' => array($this->getTokenTypes()), + '#min_tokens' => 1, + ); + + // If the module supports a set of specialized patterns, set + // them up here. + $patterns = $this->getPatterns(); + foreach ($patterns as $itemname => $itemlabel) { + $key = '_default'; + + $form[$plugin_id][$itemname][$key] = array( + '#type' => 'textfield', + '#title' => $itemlabel, + '#default_value' => $this->configuration[$plugin_id . '.' . $itemname . '.' . $key], + '#size' => 65, + '#maxlength' => 1280, + '#element_validate' => array('token_element_validate'), + '#after_build' => array('token_element_validate'), + '#token_types' => array($this->getTokenTypes()), + '#min_tokens' => 1, + ); + } + + // Display the user documentation of placeholders supported by + // this module, as a description on the last pattern. + $form[$plugin_id]['token_help'] = array( + '#title' => t('Replacement patterns'), + '#type' => 'fieldset', + '#collapsible' => TRUE, + '#collapsed' => TRUE, + ); + $form[$plugin_id]['token_help']['help'] = array( + '#theme' => 'token_tree', + '#token_types' => array($this->getTokenTypes()), + ); + } + + /** + * {@inheritdoc} + */ + public function calculateDependencies() { + } + + /** + * {@inheritdoc} + */ + public function validateConfigurationForm(array &$form, FormStateInterface $form_state) { + } + + /** + * {@inheritdoc} + */ + public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { + } + +} diff --git a/src/Plugin/pathauto/AliasType/NodeAliasType.php b/src/Plugin/pathauto/AliasType/NodeAliasType.php new file mode 100644 index 0000000..535ac6e --- /dev/null +++ b/src/Plugin/pathauto/AliasType/NodeAliasType.php @@ -0,0 +1,146 @@ +moduleHandler = $module_handler; + $this->languageManager = $language_manager; + $this->entityManager = $entity_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('module_handler'), + $container->get('language_manager'), + $container->get('entity.manager') + ); + } + + /** + * {@inheritdoc} + */ + public function getPatternDescription() { + $this->t('Default path pattern (applies to all content types with blank patterns below)'); + } + + /** + * {@inheritdoc} + */ + public function getPatterns() { + $patterns = []; + $languages = $this->languageManager->getLanguages(); + foreach ($this->getNodeTypeNames() as $node_type => $node_type_name) { + if (count($languages) && $this->isContentTranslationEnabled($node_type)) { + $patterns[$node_type] = $this->t('Default path pattern for @node_type (applies to all @node_type content types with blank patterns below)', array('@node_type' => $node_type_name)); + foreach ($languages as $language) { + $patterns[$node_type . '_' . $language->getId()] = $this->t('Pattern for all @language @node_type paths', array('@node_type' => $node_type_name, '@language' => $language->getName())); + } + } + else { + $patterns[$node_type] = $this->t('Pattern for all @node_type paths', array('@node_type' => $node_type_name)); + } + } + return $patterns; + } + + /** + * {@inheritdoc} + */ + public function defaultConfiguration() { + return array('patternitems' => array('content/[node:title]')) + parent::defaultConfiguration(); + } + + /** + * Wraps node_type_get_names(). + * + * @return array + * An array of node type names, keyed by type. + */ + protected function getNodeTypeNames() { + return array_map(function ($bundle_info) { + return $bundle_info['label']; + }, $this->entityManager->getBundleInfo('node')); + } + + /** + * Wraps content_translation_enabled(). + * + * @param string $node_type + * the node type. + * + * @return bool + * tRUE if content translation is enabled for the content type. + */ + protected function isContentTranslationEnabled($node_type) { + return $this->moduleHandler->moduleExists('content_translation') && content_translation_enabled('node', $node_type); + } + +} diff --git a/src/Tests/AliasType/NodeAliasTest.php b/src/Tests/AliasType/NodeAliasTest.php new file mode 100644 index 0000000..61f186e --- /dev/null +++ b/src/Tests/AliasType/NodeAliasTest.php @@ -0,0 +1,53 @@ +container->get('plugin.manager.alias_type'); + + /** @var \Drupal\pathauto\AliasTypeInterface $node_type */ + $node_type = $manager->createInstance('node'); + + $patterns = $node_type->getPatterns(); + $this->assertTrue((array_key_exists('node', $patterns)), "Node pattern exists."); + $this->assertEqual($patterns['node'], 'Pattern for all Content paths', "Node pattern description matches."); + + $token_types = $node_type->getTokenTypes(); + $this->assertTrue(in_array('node', $token_types), "Node token type exists."); + + $label = $node_type->getLabel(); + $this->assertEqual($label, 'Content', "Plugin label matches."); + + $default_config = $node_type->defaultConfiguration(); + + $this->assertTrue(array_key_exists('patternitems', $default_config), "Patternitems key exists."); + $this->assertEqual($default_config['patternitems'][0], 'content/[node:title]', "Default content pattern matches."); + + } + +} diff --git a/src/Tests/PathautoBulkUpdateTest.php b/src/Tests/PathautoBulkUpdateTest.php new file mode 100644 index 0000000..01cd7d4 --- /dev/null +++ b/src/Tests/PathautoBulkUpdateTest.php @@ -0,0 +1,93 @@ +adminUser = $this->drupalCreateUser($permissions); + $this->drupalLogin($this->adminUser); + } + + + function testBulkUpdate() { + // Create some nodes. + $this->nodes = array(); + for ($i = 1; $i <= 5; $i++) { + $node = $this->drupalCreateNode(); + $this->nodes[$node->id()] = $node; + } + + // Clear out all aliases. + $this->deleteAllAliases(); + + // Bulk create aliases. + $edit = array( + 'update[node_pathauto_bulk_update_batch_process]' => TRUE, + 'update[user_pathauto_bulk_update_batch_process]' => TRUE, + ); + $this->drupalPostForm('admin/config/search/path/update_bulk', $edit, t('Update')); + $this->assertText('Generated 7 URL aliases.'); // 5 nodes + 2 users + + // Check that aliases have actually been created. + foreach ($this->nodes as $node) { + $this->assertEntityAliasExists($node); + } + $this->assertEntityAliasExists($this->adminUser); + + // Add a new node. + $new_node = $this->drupalCreateNode(array('path' => array('alias' => '', 'pathauto' => FALSE))); + + // Run the update again which should only run against the new node. + $this->drupalPostForm('admin/config/search/path/update_bulk', $edit, t('Update')); + $this->assertText('Generated 1 URL alias.'); // 1 node + 0 users + + $this->assertEntityAliasExists($new_node); + } +} diff --git a/src/Tests/PathautoLocaleTest.php b/src/Tests/PathautoLocaleTest.php new file mode 100644 index 0000000..0a1538a --- /dev/null +++ b/src/Tests/PathautoLocaleTest.php @@ -0,0 +1,75 @@ +save(); + + $node = array( + 'title' => 'English node', + 'langcode' => 'en', + 'path' => array(array( + 'alias' => 'english-node', + 'pathauto' => FALSE, + )), + ); + $node = $this->drupalCreateNode($node); + $english_alias = \Drupal::service('path.alias_storage')->load(array('alias' => 'english-node', 'langcode' => 'en')); + $this->assertTrue($english_alias, 'Alias created with proper language.'); + + // Also save a French alias that should not be left alone, even though + // it is the newer alias. + $this->saveEntityAlias($node, 'french-node', 'fr'); + + // Add an alias with the soon-to-be generated alias, causing the upcoming + // alias update to generate a unique alias with the '-0' suffix. + $this->saveAlias('node/invalid', 'content/english-node', Language::LANGCODE_NOT_SPECIFIED); + + // Update the node, triggering a change in the English alias. + $node->path->pathauto = TRUE; + $node->save(); + + // Check that the new English alias replaced the old one. + $this->assertEntityAlias($node, 'content/english-node-0', 'en'); + $this->assertEntityAlias($node, 'french-node', 'fr'); + $this->assertAliasExists(array('pid' => $english_alias['pid'], 'alias' => 'content/english-node-0')); + } +} diff --git a/src/Tests/PathautoNodeWebTest.php b/src/Tests/PathautoNodeWebTest.php new file mode 100644 index 0000000..1e2626a --- /dev/null +++ b/src/Tests/PathautoNodeWebTest.php @@ -0,0 +1,165 @@ +drupalCreateContentType(array('type' => 'page', 'name' => 'Basic page')); + $this->drupalCreateContentType(array('type' => 'article')); + + // Allow other modules to add additional permissions for the admin user. + $permissions = array( + 'administer pathauto', + 'administer url aliases', + 'create url aliases', + 'administer nodes', + 'bypass node access', + 'access content overview', + ); + $this->adminUser = $this->drupalCreateUser($permissions); + $this->drupalLogin($this->adminUser); + } + + /** + * Tests editing nodes with different settings. + */ + function testNodeEditing() { + // Ensure that the Pathauto checkbox is checked by default on the node add form. + $this->drupalGet('node/add/page'); + $this->assertFieldChecked('edit-path-0-pathauto'); + + // Create a node by saving the node form. + $title = ' Testing: node title ['; + $automatic_alias = 'content/testing-node-title'; + $this->drupalPostForm(NULL, array('title[0][value]' => $title), t('Save and publish')); + $node = $this->drupalGetNodeByTitle($title); + + // Look for alias generated in the form. + $this->drupalGet("node/{$node->id()}/edit"); + $this->assertFieldChecked('edit-path-0-pathauto'); + $this->assertFieldByName('path[0][alias]', $automatic_alias, 'Generated alias visible in the path alias field.'); + + // Check whether the alias actually works. + $this->drupalGet($automatic_alias); + $this->assertText($title, 'Node accessible through automatic alias.'); + + // Manually set the node's alias. + $manual_alias = 'content/' . $node->id(); + $edit = array( + 'path[0][pathauto]' => FALSE, + 'path[0][alias]' => $manual_alias, + ); + $this->drupalPostForm("{$node->getSystemPath()}/edit", $edit, t('Save and keep published')); + $this->assertRaw(t('@type %title has been updated.', array('@type' => 'page', '%title' => $title))); + + // Check that the automatic alias checkbox is now unchecked by default. + $this->drupalGet("node/{$node->id()}/edit"); + $this->assertNoFieldChecked('edit-path-0-pathauto'); + $this->assertFieldByName('path[0][alias]', $manual_alias); + + // Submit the node form with the default values. + $this->drupalPostForm(NULL, array('path[0][pathauto]' => FALSE), t('Save and keep published')); + $this->assertRaw(t('@type %title has been updated.', array('@type' => 'page', '%title' => $title))); + + // Test that the old (automatic) alias has been deleted and only accessible + // through the new (manual) alias. + $this->drupalGet($automatic_alias); + $this->assertResponse(404, 'Node not accessible through automatic alias.'); + $this->drupalGet($manual_alias); + $this->assertText($title, 'Node accessible through manual alias.'); + + // Test that the manual alias is not kept for new nodes when the pathauto + // checkbox is ticked. + $title = 'Automatic Title'; + $edit = array( + 'title[0][value]' => $title, + 'path[0][pathauto]' => TRUE, + 'path[0][alias]' => 'should-not-get-created', + ); + $this->drupalPostForm('node/add/page', $edit, t('Save and publish')); + $this->assertNoAliasExists(array('alias' => 'should-not-get-created')); + $node = $this->drupalGetNodeByTitle($title); + $this->assertEntityAlias($node, 'content/automatic-title'); + + // Remove the pattern for nodes, the pathauto checkbox should not be + // displayed. + $config = $this->config('pathauto.pattern'); + $config->set('patterns.node.default', ''); + $config->save(); + \Drupal::service('pathauto.manager')->resetCaches(); + + $this->drupalGet('node/add/article'); + $this->assertNoFieldById('edit-path-0-pathauto'); + $this->assertFieldByName('path[0][alias]', ''); + + $edit = array(); + $edit['title'] = 'My test article'; + $this->drupalCreateNode($edit); + //$this->drupalPostForm(NULL, $edit, t('Save and keep published')); + $node = $this->drupalGetNodeByTitle($edit['title']); + + // Pathauto checkbox should still not exist. + $this->drupalGet($node->getSystemPath() . '/edit'); + $this->assertNoFieldById('edit-path-0-pathauto'); + $this->assertFieldByName('path[0][alias]', ''); + $this->assertNoEntityAlias($node); + } + + /** + * Test node operations. + */ + function testNodeOperations() { + $node1 = $this->drupalCreateNode(array('title' => 'node1')); + $node2 = $this->drupalCreateNode(array('title' => 'node2')); + + // Delete all current URL aliases. + $this->deleteAllAliases(); + + $edit = array( + 'action' => 'pathauto_update_alias_node', + // @todo - here we expect the $node1 to be at 0 position, any better way? + 'node_bulk_form[0]' => TRUE, + ); + $this->drupalPostForm('admin/content', $edit, t('Apply')); + $this->assertRaw(\Drupal::translation()->formatPlural(1, '%action was applied to @count item.', '%action was applied to @count items.', array( + '%action' => 'Update URL-Alias', + ))); + + $this->assertEntityAlias($node1, 'content/' . $node1->getTitle()); + $this->assertEntityAlias($node2, 'node/' . $node2->id()); + } + +} diff --git a/src/Tests/PathautoTaxonomyWebTest.php b/src/Tests/PathautoTaxonomyWebTest.php new file mode 100644 index 0000000..46828a6 --- /dev/null +++ b/src/Tests/PathautoTaxonomyWebTest.php @@ -0,0 +1,106 @@ +adminUser = $this->drupalCreateUser($permissions); + $this->drupalLogin($this->adminUser); + } + + + /** + * Basic functional testing of Pathauto with taxonomy terms. + */ + function testTermEditing() { + $this->drupalGet('admin/structure'); + $this->drupalGet('admin/structure/taxonomy'); + + // Add vocabulary "tags". + $vocabulary = $this->addVocabulary(array('name' => 'tags', 'vid' => 'tags')); + + // Create term for testing. + $name = 'Testing: term name ['; + $automatic_alias = 'tags/testing-term-name'; + $this->drupalPostForm('admin/structure/taxonomy/manage/tags/add', array('name[0][value]' => $name), 'Save'); + $name = trim($name); + $this->assertText("Created new term $name."); + $term = $this->drupalGetTermByName($name); + + // Look for alias generated in the form. + $this->drupalGet("taxonomy/term/{$term->id()}/edit"); + $this->assertFieldChecked('edit-path-0-pathauto'); + $this->assertFieldByName('path[0][alias]', $automatic_alias, 'Generated alias visible in the path alias field.'); + + // Check whether the alias actually works. + $this->drupalGet($automatic_alias); + $this->assertText($name, 'Term accessible through automatic alias.'); + + // Manually set the term's alias. + $manual_alias = 'tags/' . $term->id(); + $edit = array( + 'path[0][pathauto]' => FALSE, + 'path[0][alias]' => $manual_alias, + ); + $this->drupalPostForm("taxonomy/term/{$term->id()}/edit", $edit, t('Save')); + $this->assertText("Updated term $name."); + + // Check that the automatic alias checkbox is now unchecked by default. + $this->drupalGet("taxonomy/term/{$term->id()}/edit"); + $this->assertNoFieldChecked('edit-path-0-pathauto'); + $this->assertFieldByName('path[0][alias]', $manual_alias); + + // Submit the term form with the default values. + $this->drupalPostForm(NULL, array('path[0][pathauto]' => FALSE), t('Save')); + $this->assertText("Updated term $name."); + + // Test that the old (automatic) alias has been deleted and only accessible + // through the new (manual) alias. + $this->drupalGet($automatic_alias); + $this->assertResponse(404, 'Term not accessible through automatic alias.'); + $this->drupalGet($manual_alias); + $this->assertText($name, 'Term accessible through manual alias.'); + } + + +} diff --git a/src/Tests/PathautoTestHelperTrait.php b/src/Tests/PathautoTestHelperTrait.php new file mode 100644 index 0000000..304cb50 --- /dev/null +++ b/src/Tests/PathautoTestHelperTrait.php @@ -0,0 +1,131 @@ +generate($type, array($token => $token), array($type => $object)); + $tokens += array($token => ''); + $this->assertIdentical($tokens[$token], $expected, t("Token value for [@type:@token] was '@actual', expected value '@expected'.", array('@type' => $type, '@token' => $token, '@actual' => $tokens[$token], '@expected' => $expected))); + } + + public function saveAlias($source, $alias, $langcode = Language::LANGCODE_NOT_SPECIFIED) { + \Drupal::service('path.alias_storage')->delete(array('source' => $source, 'language', 'langcode' => $langcode)); + return \Drupal::service('path.alias_storage')->save($source, $alias, $langcode); + } + + public function saveEntityAlias(EntityInterface $entity, $alias, $langcode = NULL) { + // By default, use the entity language. + if (!$langcode) { + $langcode = $entity->language()->getId(); + } + return $this->saveAlias($entity->getSystemPath(), $alias, $langcode); + } + + public function assertEntityAlias(EntityInterface $entity, $expected_alias, $langcode = NULL) { + // By default, use the entity language. + if (!$langcode) { + $langcode = $entity->language()->getId(); + } + $this->assertAlias($entity->getSystemPath(), $expected_alias, $langcode); + } + + public function assertEntityAliasExists(EntityInterface $entity) { + return $this->assertAliasExists(array('source' => $entity->getSystemPath())); + } + + public function assertNoEntityAlias(EntityInterface $entity, $langcode = NULL) { + // By default, use the entity language. + if (!$langcode) { + $langcode = $entity->language()->getId(); + } + $this->assertEntityAlias($entity, $entity->getSystemPath(), $langcode); + } + + public function assertNoEntityAliasExists(EntityInterface $entity) { + $this->assertNoAliasExists(array('source' => $entity->getSystemPath())); + } + + public function assertAlias($source, $expected_alias, $langcode = Language::LANGCODE_NOT_SPECIFIED) { + $alias = array('alias' => $source); + foreach (db_select('url_alias')->fields('url_alias')->condition('source', $source)->execute() as $row) { + $alias = (array) $row; + if ($row->alias == $expected_alias) { + break; + } + } + $this->assertIdentical($alias['alias'], $expected_alias, t("Alias for %source with language '@language' was %actual, expected %expected.", + array('%source' => $source, '%actual' => $alias['alias'], '%expected' => $expected_alias, '@language' => $langcode))); + } + + public function assertAliasExists($conditions) { + $path = \Drupal::service('path.alias_storage')->load($conditions); + $this->assertTrue($path, t('Alias with conditions @conditions found.', array('@conditions' => var_export($conditions, TRUE)))); + return $path; + } + + public function assertNoAliasExists($conditions) { + $alias = \Drupal::service('path.alias_storage')->load($conditions); + $this->assertFalse($alias, t('Alias with conditions @conditions not found.', array('@conditions' => var_export($conditions, TRUE)))); + } + + public function deleteAllAliases() { + db_delete('url_alias')->execute(); + \Drupal::service('path.alias_manager')->cacheClear(); + } + + /** + * @param array $values + * @return \Drupal\taxonomy\VocabularyInterface + */ + public function addVocabulary(array $values = array()) { + $name = Unicode::strtolower($this->randomMachineName(5)); + $values += array( + 'name' => $name, + 'vid' => $name, + ); + $vocabulary = entity_create('taxonomy_vocabulary', $values); + $vocabulary->save(); + + return $vocabulary; + } + + public function addTerm(VocabularyInterface $vocabulary, array $values = array()) { + $values += array( + 'name' => Unicode::strtolower($this->randomMachineName(5)), + 'vid' => $vocabulary->id(), + ); + + $term = entity_create('taxonomy_term', $values); + $term->save(); + return $term; + } + + public function assertEntityPattern($entity_type, $bundle, $langcode = Language::LANGCODE_NOT_SPECIFIED, $expected) { + \Drupal::service('pathauto.manager')->resetCaches(); + $pattern = \Drupal::service('pathauto.manager')->getPatternByEntity($entity_type, $bundle, $langcode); + $this->assertIdentical($expected, $pattern); + } + + public function drupalGetTermByName($name, $reset = FALSE) { + if ($reset) { + // @todo - implement cache reset. + } + $terms = \Drupal::entityManager()->getStorage('taxonomy_term')->loadByProperties(array('name' => $name)); + return !empty($terms) ? reset($terms) : FALSE; + } +} diff --git a/src/Tests/PathautoTokenTest.php b/src/Tests/PathautoTokenTest.php new file mode 100644 index 0000000..fbca356 --- /dev/null +++ b/src/Tests/PathautoTokenTest.php @@ -0,0 +1,80 @@ +installConfig(array('pathauto')); + + $array = array( + 'test first arg', + 'The Array / value', + ); + + $tokens = array( + 'join-path' => 'test-first-arg/array-value', + ); + $data['array'] = $array; + $replacements = $this->assertTokens('array', $data, $tokens); + + // Ensure that the cleanTokenValues() method does not alter this token value. + /* @var \Drupal\pathauto\PathautoManagerInterface $manager */ + $manager = \Drupal::service('pathauto.manager'); + $manager->cleanTokenValues($replacements, $data, array()); + $this->assertEqual($replacements['[array:join-path]'], 'test-first-arg/array-value'); + } + + /** + * Function copied from TokenTestHelper::assertTokens(). + */ + public function assertTokens($type, array $data, array $tokens, array $options = array()) { + $input = $this->mapTokenNames($type, array_keys($tokens)); + $replacements = \Drupal::token()->generate($type, $input, $data, $options); + foreach ($tokens as $name => $expected) { + $token = $input[$name]; + if (!isset($expected)) { + $this->assertTrue(!isset($values[$token]), t("Token value for @token was not generated.", array('@type' => $type, '@token' => $token))); + } + elseif (!isset($replacements[$token])) { + $this->fail(t("Token value for @token was not generated.", array('@type' => $type, '@token' => $token))); + } + elseif (!empty($options['regex'])) { + $this->assertTrue(preg_match('/^' . $expected . '$/', $replacements[$token]), t("Token value for @token was '@actual', matching regular expression pattern '@expected'.", array('@type' => $type, '@token' => $token, '@actual' => $replacements[$token], '@expected' => $expected))); + } + else { + $this->assertIdentical($replacements[$token], $expected, t("Token value for @token was '@actual', expected value '@expected'.", array('@type' => $type, '@token' => $token, '@actual' => $replacements[$token], '@expected' => $expected))); + } + } + + return $replacements; + } + + public function mapTokenNames($type, array $tokens = array()) { + $return = array(); + foreach ($tokens as $token) { + $return[$token] = "[$type:$token]"; + } + return $return; + } +} diff --git a/src/Tests/PathautoUiTest.php b/src/Tests/PathautoUiTest.php new file mode 100644 index 0000000..1415aa9 --- /dev/null +++ b/src/Tests/PathautoUiTest.php @@ -0,0 +1,103 @@ +drupalCreateContentType(array('type' => 'page', 'name' => 'Basic page')); + $this->drupalCreateContentType(array('type' => 'article')); + + // Allow other modules to add additional permissions for the admin user. + $permissions = array( + 'administer pathauto', + 'administer url aliases', + 'create url aliases', + 'administer nodes', + 'bypass node access', + 'access content overview', + ); + $this->adminUser = $this->drupalCreateUser($permissions); + $this->drupalLogin($this->adminUser); + } + + function testSettingsValidation() { + $edit = array(); + $edit['max_length'] = 'abc'; + $edit['max_component_length'] = 'abc'; + $this->drupalPostForm('admin/config/search/path/settings', $edit, 'Save configuration'); + /*$this->assertText('The field Maximum alias length is not a valid number.'); + $this->assertText('The field Maximum component length is not a valid number.');*/ + $this->assertNoText('The configuration options have been saved.'); + + $edit['max_length'] = '0'; + $edit['max_component_length'] = '0'; + $this->drupalPostForm('admin/config/search/path/settings', $edit, 'Save configuration'); + /*$this->assertText('The field Maximum alias length cannot be less than 1.'); + $this->assertText('The field Maximum component length cannot be less than 1.');*/ + $this->assertNoText('The configuration options have been saved.'); + + $edit['max_length'] = '999'; + $edit['max_component_length'] = '999'; + $this->drupalPostForm('admin/config/search/path/settings', $edit, 'Save configuration'); + /*$this->assertText('The field Maximum alias length cannot be greater than 255.'); + $this->assertText('The field Maximum component length cannot be greater than 255.');*/ + $this->assertNoText('The configuration options have been saved.'); + + $edit['max_length'] = '50'; + $edit['max_component_length'] = '50'; + $this->drupalPostForm('admin/config/search/path/settings', $edit, 'Save configuration'); + $this->assertText('The configuration options have been saved.'); + } + + function testPatternsValidation() { + $edit = array(); + $this->drupalGet('admin/config/search/path/patterns'); + $edit['node[default]'] = '[node:title]/[user:name]/[term:name]'; + $edit['node[bundles][page][default]'] = 'page'; + $this->drupalPostForm('admin/config/search/path/patterns', $edit, 'Save configuration'); + $this->assertText('The Default path pattern (applies to all content types with blank patterns below) is using the following invalid tokens: [user:name], [term:name].'); + $this->assertText('The Pattern for all Basic page paths cannot contain fewer than one token.'); + $this->assertNoText('The configuration options have been saved.'); + + $edit['node[default]'] = '[node:title]'; + $edit['node[bundles][page][default]'] = 'page/[node:title]'; + $edit['node[bundles][article][default]'] = ''; + $this->drupalPostForm('admin/config/search/path/patterns', $edit, 'Save configuration'); + $this->assertText('The configuration options have been saved.'); + } + +} diff --git a/src/Tests/PathautoUnitTest.php b/src/Tests/PathautoUnitTest.php new file mode 100644 index 0000000..04dfc99 --- /dev/null +++ b/src/Tests/PathautoUnitTest.php @@ -0,0 +1,370 @@ +installConfig(array('pathauto', 'taxonomy', 'system')); + + $this->installEntitySchema('user'); + $this->installEntitySchema('node'); + $this->installEntitySchema('taxonomy_term'); + + $this->installSchema('node', array('node_access')); + $this->installSchema('system', array('url_alias', 'sequences', 'router')); + + \Drupal::service('router.builder')->rebuild(); + + $this->currentUser = entity_create('user', array('name' => $this->randomMachineName())); + $this->currentUser->save(); + } + + /** + * Test _pathauto_get_schema_alias_maxlength(). + */ + public function testGetSchemaAliasMaxLength() { + $this->assertIdentical(\Drupal::service('pathauto.alias_storage_helper')->getAliasSchemaMaxlength(), 255); + } + + /** + * Test pathauto_pattern_load_by_entity(). + */ + public function testPatternLoadByEntity() { + $this->config('pathauto.pattern') + ->set('patterns.node.bundles.article.default', 'article/[node:title]') + ->set('patterns.node.bundles.article.languages.en', 'article/en/[node:title]') + ->set('patterns.node.bundles.page.default', '[node:title]') + ->save(); + + $tests = array( + array( + 'entity' => 'node', + 'bundle' => 'article', + 'language' => 'fr', + 'expected' => 'article/[node:title]', + ), + array( + 'entity' => 'node', + 'bundle' => 'article', + 'language' => 'en', + 'expected' => 'article/en/[node:title]', + ), + array( + 'entity' => 'node', + 'bundle' => 'article', + 'language' => Language::LANGCODE_NOT_SPECIFIED, + 'expected' => 'article/[node:title]', + ), + array( + 'entity' => 'node', + 'bundle' => 'page', + 'language' => 'en', + 'expected' => '[node:title]', + ), + array( + 'entity' => 'user', + 'bundle' => 'user', + 'language' => Language::LANGCODE_NOT_SPECIFIED, + 'expected' => 'users/[user:name]', + ), + array( + 'entity' => 'invalid-entity', + 'bundle' => '', + 'language' => Language::LANGCODE_NOT_SPECIFIED, + 'expected' => '', + ), + ); + foreach ($tests as $test) { + $actual = \Drupal::service('pathauto.manager')->getPatternByEntity($test['entity'], $test['bundle'], $test['language']); + $this->assertIdentical($actual, $test['expected'], t("pathauto_pattern_load_by_entity('@entity', '@bundle', '@language') returned '@actual', expected '@expected'", array( + '@entity' => $test['entity'], + '@bundle' => $test['bundle'], + '@language' => $test['language'], + '@actual' => $actual, + '@expected' => $test['expected'], + ))); + } + } + + /** + * Test pathauto_cleanstring(). + */ + public function testCleanString() { + + $config = $this->config('pathauto.settings'); + + $tests = array(); + $config->set('ignore_words', ', in, is,that, the , this, with, '); + $config->set('max_component_length', 35); + $config->set('transliterate', TRUE); + $config->save(); + \Drupal::service('pathauto.manager')->resetCaches(); + + // Test the 'ignored words' removal. + $tests['this'] = 'this'; + $tests['this with that'] = 'this-with-that'; + $tests['this thing with that thing'] = 'thing-thing'; + + // Test length truncation and duplicate separator removal. + $tests[' - Pathauto is the greatest - module ever in Drupal hiarticle - '] = 'pathauto-greatest-module-ever'; + + // Test that HTML tags are removed. + $tests['This text has
HTML tags.'] = 'text-has-html-tags'; + $tests[String::checkPlain('This text has
HTML tags.')] = 'text-has-html-tags'; + + // Transliteration. + $tests['ľščťžýáíéňô'] = 'lsctzyaieno'; + + foreach ($tests as $input => $expected) { + $output = \Drupal::service('pathauto.manager')->cleanString($input); + $this->assertEqual($output, $expected, t("Drupal::service('pathauto.manager')->cleanString('@input') expected '@expected', actual '@output'", array( + '@input' => $input, + '@expected' => $expected, + '@output' => $output, + ))); + } + } + + /** + * Test pathauto_clean_alias(). + */ + public function testCleanAlias() { + $tests = array(); + $tests['one/two/three'] = 'one/two/three'; + $tests['/one/two/three/'] = 'one/two/three'; + $tests['one//two///three'] = 'one/two/three'; + $tests['one/two--three/-/--/-/--/four---five'] = 'one/two-three/four-five'; + $tests['one/-//three--/four'] = 'one/three/four'; + + foreach ($tests as $input => $expected) { + $output = \Drupal::service('pathauto.alias_cleaner')->cleanAlias($input); + $this->assertEqual($output, $expected, t("Drupal::service('pathauto.manager')->cleanAlias('@input') expected '@expected', actual '@output'", array( + '@input' => $input, + '@expected' => $expected, + '@output' => $output, + ))); + } + } + + /** + * Test pathauto_path_delete_multiple(). + */ + public function testPathDeleteMultiple() { + $this->saveAlias('node/1', 'node-1-alias'); + $this->saveAlias('node/1/view', 'node-1-alias/view'); + $this->saveAlias('node/1', 'node-1-alias-en', 'en'); + $this->saveAlias('node/1', 'node-1-alias-fr', 'fr'); + $this->saveAlias('node/2', 'node-2-alias'); + + pathauto_path_delete_all('node/1'); + $this->assertNoAliasExists(array('source' => "node/1")); + $this->assertNoAliasExists(array('source' => "node/1/view")); + $this->assertAliasExists(array('source' => "node/2")); + } + + /** + * Test the different update actions in \Drupal::service('pathauto.manager')->createAlias(). + */ + public function testUpdateActions() { + $config = $this->config('pathauto.settings'); + + // Test PATHAUTO_UPDATE_ACTION_NO_NEW with unaliased node and 'insert'. + $config->set('update_action', PathautoManagerInterface::UPDATE_ACTION_NO_NEW); + $config->save(); + $node = $this->drupalCreateNode(array('title' => 'First title')); + $this->assertEntityAlias($node, 'content/first-title'); + + $node->path->pathauto = TRUE; + + // Default action is PATHAUTO_UPDATE_ACTION_DELETE. + $config->set('update_action', PathautoManagerInterface::UPDATE_ACTION_DELETE); + $config->save(); + $node->setTitle('Second title'); + $node->save(); + $this->assertEntityAlias($node, 'content/second-title'); + $this->assertNoAliasExists(array('alias' => 'content/first-title')); + + // Test PATHAUTO_UPDATE_ACTION_LEAVE + $config->set('update_action', PathautoManagerInterface::UPDATE_ACTION_LEAVE); + $config->save(); + $node->setTitle('Third title'); + $node->save(); + $this->assertEntityAlias($node, 'content/third-title'); + $this->assertAliasExists(array('source' => $node->getSystemPath(), 'alias' => 'content/second-title')); + + $config->set('update_action', PathautoManagerInterface::UPDATE_ACTION_DELETE); + $config->save(); + $node->setTitle('Fourth title'); + $node->save(); + $this->assertEntityAlias($node, 'content/fourth-title'); + $this->assertNoAliasExists(array('alias' => 'content/third-title')); + // The older second alias is not deleted yet. + $older_path = $this->assertAliasExists(array('source' => $node->getSystemPath(), 'alias' => 'content/second-title')); + \Drupal::service('path.alias_storage')->delete($older_path); + + $config->set('update_action', PathautoManagerInterface::UPDATE_ACTION_NO_NEW); + $config->save(); + $node->setTitle('Fifth title'); + $node->save(); + $this->assertEntityAlias($node, 'content/fourth-title'); + $this->assertNoAliasExists(array('alias' => 'content/fifth-title')); + + // Test PATHAUTO_UPDATE_ACTION_NO_NEW with unaliased node and 'update'. + $this->deleteAllAliases(); + $node->save(); + $this->assertEntityAlias($node, 'content/fifth-title'); + + // Test PATHAUTO_UPDATE_ACTION_NO_NEW with unaliased node and 'bulkupdate'. + $this->deleteAllAliases(); + $node->setTitle('Sixth title'); + \Drupal::service('pathauto.manager')->updateAlias($node, 'bulkupdate'); + $this->assertEntityAlias($node, 'content/sixth-title'); + } + + /** + * Test that \Drupal::service('pathauto.manager')->createAlias() will not create an alias for a pattern + * that does not get any tokens replaced. + */ + public function testNoTokensNoAlias() { + $node = $this->drupalCreateNode(array('title' => '')); + $this->assertNoEntityAliasExists($node); + + $node->setTitle('hello'); + $node->save(); + $this->assertEntityAlias($node, 'content/hello'); + } + + /** + * Test the handling of path vs non-path tokens in pathauto_clean_token_values(). + */ + public function testPathTokens() { + $config = $this->config('pathauto.pattern'); + $config->set('patterns.taxonomy_term.default', '[term:parent:url:path]/[term:name]'); + $config->save(); + + $vocab = $this->addVocabulary(); + + $term1 = $this->addTerm($vocab, array('name' => 'Parent term')); + $this->assertEntityAlias($term1, 'parent-term'); + + $term2 = $this->addTerm($vocab, array('name' => 'Child term', 'parent' => $term1->id())); + $this->assertEntityAlias($term2, 'parent-term/child-term'); + + $this->saveEntityAlias($term1, 'My Crazy/Alias/'); + $term2->save(); + $this->assertEntityAlias($term2, 'My Crazy/Alias/child-term'); + } + + public function testEntityBundleRenamingDeleting() { + $config = $this->config('pathauto.pattern'); + + // Create a vocabulary and test that it's pattern variable works. + $vocab = $this->addVocabulary(array('vid' => 'old_name')); + $config->set('patterns.taxonomy_term.default', 'base'); + $config->set('patterns.taxonomy_term.bundles.old_name.default', 'bundle'); + $config->save(); + + $this->assertEntityPattern('taxonomy_term', 'old_name', Language::LANGCODE_NOT_SPECIFIED, 'bundle'); + + // Rename the vocabulary's machine name, which should cause its pattern + // variable to also be renamed. + $vocab->set('vid', 'new_name'); + $vocab->save(); + $this->assertEntityPattern('taxonomy_term', 'new_name', Language::LANGCODE_NOT_SPECIFIED, 'bundle'); + $this->assertEntityPattern('taxonomy_term', 'old_name', Language::LANGCODE_NOT_SPECIFIED, 'base'); + + // Delete the vocabulary, which should cause its pattern variable to also + // be deleted. + $vocab->delete(); + $this->assertEntityPattern('taxonomy_term', 'new_name', Language::LANGCODE_NOT_SPECIFIED, 'base'); + } + + function testNoExistingPathAliases() { + + $this->config('pathauto.settings') + ->set('punctuation.period', PathautoManagerInterface::PUNCTUATION_DO_NOTHING) + ->save(); + $this->config('pathauto.pattern') + ->set('patterns.node.bundles.page.default', '[node:title]') + ->save(); + \Drupal::service('pathauto.manager')->resetCaches(); + + // Check that Pathauto does not create an alias of '/admin'. + $node = $this->drupalCreateNode(array('title' => 'Admin', 'type' => 'page')); + $this->assertEntityAlias($node, 'admin-0'); + + // Check that Pathauto does not create an alias of '/modules'. + $node->setTitle('Modules'); + $node->save(); + $this->assertEntityAlias($node, 'modules-0'); + + // Check that Pathauto does not create an alias of '/index.php'. + $node->setTitle('index.php'); + $node->save(); + $this->assertEntityAlias($node, 'index.php-0'); + + // Check that a safe value gets an automatic alias. This is also a control + // to ensure the above tests work properly. + $node->setTitle('Safe value'); + $node->save(); + $this->assertEntityAlias($node, 'safe-value'); + } + + /** + * Test programmatic entity creation for aliases. + */ + function testProgrammaticEntityCreation() { + $node = $this->drupalCreateNode(array('title' => 'Test node', 'path' => array('pathauto' => TRUE))); + $this->assertEntityAlias($node, 'content/test-node'); + + $vocabulary = $this->addVocabulary(array('name' => 'Tags')); + $term = $this->addTerm($vocabulary, array('name' => 'Test term', 'path' => array('pathauto' => TRUE))); + $this->assertEntityAlias($term, 'tags/test-term'); + + $edit['name'] = 'Test user'; + $edit['mail'] = 'test-user@example.com'; + $edit['pass'] = user_password(); + $edit['path'] = array('pathauto' => TRUE); + $edit['status'] = 1; + $account = entity_create('user', $edit); + $account->save(); + $this->assertEntityAlias($account, 'users/test-user'); + } + + protected function drupalCreateNode(array $settings = array()) { + // Populate defaults array. + $settings += array( + 'title' => $this->randomMachineName(8), + 'type' => 'page', + ); + + $node = entity_create('node', $settings); + $node->save(); + + return $node; + } +} diff --git a/src/Tests/PathautoUserWebTest.php b/src/Tests/PathautoUserWebTest.php new file mode 100644 index 0000000..af87f4a --- /dev/null +++ b/src/Tests/PathautoUserWebTest.php @@ -0,0 +1,95 @@ +adminUser = $this->drupalCreateUser($permissions); + $this->drupalLogin($this->adminUser); + } + + + /** + * Basic functional testing of Pathauto with users. + */ + function testUserEditing() { + // There should be no Pathauto checkbox on user forms. + $this->drupalGet('user/' . $this->adminUser->id() . '/edit'); + $this->assertNoFieldById('path[0][pathauto]'); + } + + /** + * Test user operations. + */ + function testUserOperations() { + $account = $this->drupalCreateUser(); + + // Delete all current URL aliases. + $this->deleteAllAliases(); + + // Find the position of just created account in the user_admin_people view. + $view = Views::getView('user_admin_people'); + $view->initDisplay(); + $view->preview('page_1'); + foreach ($view->result as $key => $row) { + if ($row->users_field_data_name == $account->getUsername()) { + break; + } + } + + $edit = array( + 'action' => 'pathauto_update_alias_user', + "user_bulk_form[$key]" => TRUE, + ); + $this->drupalPostForm('admin/people', $edit, t('Apply')); + $this->assertRaw(\Drupal::translation()->formatPlural(1, '%action was applied to @count item.', '%action was applied to @count items.', array( + '%action' => 'Update URL-Alias', + ))); + + $this->assertEntityAlias($account, 'users/' . Unicode::strtolower($account->getUsername())); + $this->assertEntityAlias($this->adminUser, 'user/' . $this->adminUser->id()); + } + +} diff --git a/src/VerboseMessenger.php b/src/VerboseMessenger.php new file mode 100644 index 0000000..4bef8c9 --- /dev/null +++ b/src/VerboseMessenger.php @@ -0,0 +1,68 @@ +configFactory = $config_factory; + $this->account = $account; + } + + /** + * {@inheritdoc} + */ + public function addMessage($message, $op = NULL) { + + if (!isset($this->isVerbose)) { + $config = $this->configFactory->get('pathauto.settings'); + $this->isVerbose = $config->get('verbose') && $this->account->hasPermission('notify of path changes'); + } + + if (!$this->isVerbose || (isset($op) && in_array($op, array('bulkupdate', 'return')))) { + return FALSE; + } + + if ($message) { + drupal_set_message($message); + } + + return TRUE; + } + +} diff --git a/tests/src/VerboseMessengerTest.php b/tests/src/VerboseMessengerTest.php new file mode 100644 index 0000000..25b0f20 --- /dev/null +++ b/tests/src/VerboseMessengerTest.php @@ -0,0 +1,67 @@ +getConfigFactoryStub(array('pathauto.settings' => array('verbose' => TRUE))); + $account = $this->getMock('\Drupal\Core\Session\AccountInterface'); + $account->expects($this->once()) + ->method('hasPermission') + ->withAnyParameters() + ->willReturn(TRUE); + + // The messenger under test. + $this->messenger = new VerboseMessenger($config_factory, $account); + } + + /** + * Tests add messages. + * @covers ::addMessage + */ + public function testAddMessage() { + // Test adding a message. + $this->assertTrue($this->messenger->addMessage("Test message"), "The message was added"); + } + + /** + * Tests add messages. + * @covers ::addMessage + */ + public function testNotAddMessage() { + // Test adding a message. + $this->assertFalse($this->messenger->addMessage("Test message", "bulkupdate"), "The message was NOT added"); + } + } + +} +namespace { + // @todo Delete after https://drupal.org/node/1858196 is in. + if (!function_exists('drupal_set_message')) { + function drupal_set_message() { + } + } +}