├── .travis-before-script.sh
├── .travis.yml
├── README.md
├── composer.json
├── config
├── install
│ └── pathauto.settings.yml
├── optional
│ ├── system.action.pathauto_update_alias_node.yml
│ └── system.action.pathauto_update_alias_user.yml
└── schema
│ ├── pathauto.schema.yml
│ └── pathauto_pattern.schema.yml
├── pathauto.api.php
├── pathauto.info.yml
├── pathauto.install
├── pathauto.js
├── pathauto.libraries.yml
├── pathauto.links.action.yml
├── pathauto.links.task.yml
├── pathauto.module
├── pathauto.permissions.yml
├── pathauto.routing.yml
├── pathauto.services.yml
├── pathauto.tokens.inc
├── src
├── AliasCleaner.php
├── AliasCleanerInterface.php
├── AliasStorageHelper.php
├── AliasStorageHelperInterface.php
├── AliasTypeBatchUpdateInterface.php
├── AliasTypeInterface.php
├── AliasTypeManager.php
├── AliasUniquifier.php
├── AliasUniquifierInterface.php
├── Annotation
│ └── AliasType.php
├── Entity
│ └── PathautoPattern.php
├── Form
│ ├── PathautoAdminDelete.php
│ ├── PathautoBulkUpdateForm.php
│ ├── PathautoSettingsForm.php
│ └── PatternEditForm.php
├── MessengerInterface.php
├── PathautoGenerator.php
├── PathautoGeneratorInterface.php
├── PathautoItem.php
├── PathautoPatternInterface.php
├── PathautoPatternListBuilder.php
├── PathautoState.php
├── PathautoWidget.php
├── Plugin
│ ├── Action
│ │ └── UpdateAction.php
│ ├── Deriver
│ │ └── EntityAliasTypeDeriver.php
│ └── pathauto
│ │ └── AliasType
│ │ ├── EntityAliasTypeBase.php
│ │ └── ForumAliasType.php
├── Tests
│ ├── PathautoBulkUpdateTest.php
│ ├── PathautoLocaleTest.php
│ ├── PathautoMassDeleteTest.php
│ ├── PathautoNodeWebTest.php
│ ├── PathautoTaxonomyWebTest.php
│ ├── PathautoTestHelperTrait.php
│ ├── PathautoTokenTest.php
│ ├── PathautoUiTest.php
│ ├── PathautoUnitTest.php
│ └── PathautoUserWebTest.php
└── VerboseMessenger.php
└── tests
└── src
└── VerboseMessengerTest.php
/.travis-before-script.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e $DRUPAL_TI_DEBUG
4 |
5 | # Ensure the right Drupal version is installed.
6 | # Note: This function is re-entrant.
7 | drupal_ti_ensure_drupal
8 |
9 | # Add needed dependencies.
10 | cd "$DRUPAL_TI_DRUPAL_DIR"
11 |
12 | # These variables come from environments/drupal-*.sh
13 | mkdir -p "$DRUPAL_TI_MODULES_PATH"
14 | cd "$DRUPAL_TI_MODULES_PATH"
15 |
16 | # Download token 8.x-1.x and ctools 8.x-3.x
17 | git clone --depth 1 --branch 8.x-1.x http://git.drupal.org/project/token.git
18 | git clone --depth 1 --branch 8.x-3.x http://git.drupal.org/project/ctools.git
19 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | # @file
2 | # .travis.yml - Drupal for Travis CI Integration
3 | #
4 | # Template provided by https://github.com/LionsAd/drupal_ti.
5 | #
6 | # Based for simpletest upon:
7 | # https://github.com/sonnym/travis-ci-drupal-module-example
8 |
9 | language: php
10 |
11 | sudo: false
12 |
13 | php:
14 | - 5.5
15 | - 5.6
16 | - 7
17 | - hhvm
18 |
19 | matrix:
20 | fast_finish: true
21 | allow_failures:
22 | - php: 7
23 | - php: hhvm
24 |
25 | env:
26 | global:
27 | # add composer's global bin directory to the path
28 | # see: https://github.com/drush-ops/drush#install---composer
29 | - PATH="$PATH:$HOME/.composer/vendor/bin"
30 |
31 | # Configuration variables.
32 | - DRUPAL_TI_MODULE_NAME="pathauto"
33 | - DRUPAL_TI_SIMPLETEST_GROUP="pathauto"
34 |
35 | # Define runners and environment vars to include before and after the
36 | # main runners / environment vars.
37 | #- DRUPAL_TI_SCRIPT_DIR_BEFORE="./.drupal_ti/before"
38 | #- DRUPAL_TI_SCRIPT_DIR_AFTER="./drupal_ti/after"
39 |
40 | # The environment to use, supported are: drupal-7, drupal-8
41 | - DRUPAL_TI_ENVIRONMENT="drupal-8"
42 |
43 | # Drupal specific variables.
44 | - DRUPAL_TI_DB="drupal_travis_db"
45 | - DRUPAL_TI_DB_URL="mysql://root:@127.0.0.1/drupal_travis_db"
46 | # Note: Do not add a trailing slash here.
47 | - DRUPAL_TI_WEBSERVER_URL="http://127.0.0.1"
48 | - DRUPAL_TI_WEBSERVER_PORT="8080"
49 |
50 | # Simpletest specific commandline arguments, the DRUPAL_TI_SIMPLETEST_GROUP is appended at the end.
51 | - DRUPAL_TI_SIMPLETEST_ARGS="--verbose --color --concurrency 4 --url $DRUPAL_TI_WEBSERVER_URL:$DRUPAL_TI_WEBSERVER_PORT"
52 |
53 | # === Behat specific variables.
54 | # This is relative to $TRAVIS_BUILD_DIR
55 | - DRUPAL_TI_BEHAT_DIR="./tests/behat"
56 | # These arguments are passed to the bin/behat command.
57 | - DRUPAL_TI_BEHAT_ARGS=""
58 | # Specify the filename of the behat.yml with the $DRUPAL_TI_DRUPAL_DIR variables.
59 | - DRUPAL_TI_BEHAT_YML="behat.yml.dist"
60 | # This is used to setup Xvfb.
61 | - DRUPAL_TI_BEHAT_SCREENSIZE_COLOR="1280x1024x16"
62 | # The version of seleniumthat should be used.
63 | - DRUPAL_TI_BEHAT_SELENIUM_VERSION="2.44"
64 | # Set DRUPAL_TI_BEHAT_DRIVER to "selenium" to use "firefox" or "chrome" here.
65 | - DRUPAL_TI_BEHAT_DRIVER="phantomjs"
66 | - DRUPAL_TI_BEHAT_BROWSER="firefox"
67 |
68 | # PHPUnit specific commandline arguments.
69 | - DRUPAL_TI_PHPUNIT_ARGS="--verbose --debug"
70 | # Specifying the phpunit-core src/ directory is useful when e.g. a vendor/
71 | # directory is present in the module directory, which phpunit would then
72 | # try to find tests in. This option is relative to $TRAVIS_BUILD_DIR.
73 | #- DRUPAL_TI_PHPUNIT_CORE_SRC_DIRECTORY="./tests/src"
74 |
75 | # Code coverage via coveralls.io
76 | - DRUPAL_TI_COVERAGE="satooshi/php-coveralls:0.6.*"
77 | # This needs to match your .coveralls.yml file.
78 | - DRUPAL_TI_COVERAGE_FILE="build/logs/clover.xml"
79 |
80 | # Debug options
81 | #- DRUPAL_TI_DEBUG="-x -v"
82 | # Set to "all" to output all files, set to e.g. "xvfb selenium" or "selenium",
83 | # etc. to only output those channels.
84 | #- DRUPAL_TI_DEBUG_FILE_OUTPUT="selenium xvfb webserver"
85 |
86 | matrix:
87 | # [[[ SELECT ANY OR MORE OPTIONS ]]]
88 | #- DRUPAL_TI_RUNNERS="phpunit"
89 | #- DRUPAL_TI_RUNNERS="simpletest"
90 | #- DRUPAL_TI_RUNNERS="behat"
91 | - DRUPAL_TI_RUNNERS="phpunit-core simpletest"
92 |
93 | mysql:
94 | database: drupal_travis_db
95 | username: root
96 | encoding: utf8
97 |
98 | before_install:
99 | - composer self-update
100 | - composer global require "lionsad/drupal_ti:1.*"
101 | - drupal-ti before_install
102 |
103 | install:
104 | - drupal-ti install
105 |
106 | before_script:
107 | # Install token 8.x-1.x
108 | - drupal-ti --include .travis-before-script.sh
109 | - drupal-ti before_script
110 |
111 | script:
112 | - drupal-ti script
113 |
114 | after_script:
115 | - drupal-ti after_script
116 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | #Pathauto [](https://travis-ci.org/md-systems/pathauto)
2 |
3 | If you are developing for this module, have a look at pathauto.api.php.
4 |
5 | ##Description
6 |
7 | The Pathauto module provides support functions for other modules to
8 | automatically generate aliases based on appropriate criteria, with a
9 | central settings path for site administrators.
10 |
11 | Implementations are provided for core entity types: content, taxonomy terms,
12 | and users (including blogs and forum pages).
13 |
14 | Pathauto also provides a way to delete large numbers of aliases. This feature
15 | is available at Administer > Configuration > Search and metadata > URL aliases >
16 | Delete aliases.
17 |
18 | ##Benefits
19 |
20 | Besides making the page address more reflective of its content than
21 | "node/138", it's important to know that modern search engines give
22 | heavy weight to search terms which appear in a page's URL. By
23 | automatically using keywords based directly on the page content in the URL,
24 | relevant search engine hits for your page can be significantly
25 | enhanced.
26 |
27 | ##Notices
28 |
29 | Pathauto just adds URL aliases to content, users, and taxonomy terms.
30 | Because it's an alias, the standard Drupal URL (for example node/123 or
31 | taxonomy/term/1) will still function as normal. If you have external links
32 | to your site pointing to standard Drupal URLs, or hardcoded links in a module,
33 | template, content or menu which point to standard Drupal URLs it will bypass
34 | the alias set by Pathauto.
35 |
36 | There are reasons you might not want two URLs for the same content on your
37 | site. If this applies to you, please note that you will need to update any
38 | hard coded links in your content or blocks.
39 |
40 | If you use the "system path" (i.e. node/10) for menu items and settings like
41 | that, Drupal will replace it with the url_alias.
42 |
43 | For external links, you might want to consider the Path Redirect or
44 | Global Redirect modules, which allow you to set forwarding either per item or
45 | across the site to your aliased URLs.
46 |
47 | ###URLs (not) Getting Replaced With Aliases:
48 | Please bear in mind that only URLs passed through Drupal's Drupal's URL and
49 | Link APIs will be replaced with their aliases during page output. If
50 | a module or your template contains hardcoded links, such as
51 | 'href="node/$node->nid"', those won't get replaced with their corresponding
52 | aliases.
53 |
54 | ## Disabling Pathauto for a specific content type (or taxonomy)
55 |
56 | When the pattern for a content type is left blank, the default pattern will be
57 | used. But if the default pattern is also blank, Pathauto will be disabled
58 | for that content type.
59 |
60 | ##Credits:
61 |
62 | The original module combined the functionality of Mike Ryan's autopath with
63 | Tommy Sundstrom's path_automatic.
64 |
65 | Significant enhancements were contributed by jdmquin @ www.bcdems.net.
66 |
67 | Matt England added the tracker support (tracker support has been removed in
68 | recent changes).
69 |
70 | Other suggestions and patches contributed by the Drupal community.
71 |
72 | Current maintainers:
73 |
74 | - Dave Reid - http://www.davereid.net
75 | - Greg Knaddison - http://www.knaddison.com
76 | - Mike Ryan - http://mikeryan.name
77 | - Frederik 'Freso' S. Olesen - http://freso.dk
78 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "drupal/pathauto",
3 | "description": "Provides a generic set of views plugins a mechanism for modules to automatically generate aliases for the content they manage.",
4 | "type": "drupal-module",
5 | "license": "GPL-2.0+",
6 |
7 | "repositories": [
8 | {
9 | "type": "composer",
10 | "url": "https://packagist.drupal-composer.org"
11 | }
12 | ],
13 |
14 | "require": {
15 | "drupal/token": "8.1.x-dev",
16 | "drupal/ctools": "8.3.x-dev"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/config/install/pathauto.settings.yml:
--------------------------------------------------------------------------------
1 | punctuation:
2 | hyphen: 1
3 | verbose : FALSE
4 | separator : '-'
5 | max_length : 100
6 | max_component_length: 100
7 | transliterate : FALSE
8 | reduce_ascii : FALSE
9 | ignore_words : ', in, is,that, the , this, with, '
10 | case : TRUE
11 | 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'
12 | update_action : 2
13 |
--------------------------------------------------------------------------------
/config/optional/system.action.pathauto_update_alias_node.yml:
--------------------------------------------------------------------------------
1 | id: pathauto_update_alias_node
2 | label: 'Update URL-Alias'
3 | status: true
4 | langcode: en
5 | type: node
6 | plugin: pathauto_update_alias
7 | dependencies:
8 | module:
9 | - node
10 |
--------------------------------------------------------------------------------
/config/optional/system.action.pathauto_update_alias_user.yml:
--------------------------------------------------------------------------------
1 | id: pathauto_update_alias_user
2 | label: 'Update URL-Alias'
3 | status: true
4 | langcode: en
5 | type: user
6 | plugin: pathauto_update_alias
7 | dependencies:
8 | module:
9 | - user
10 |
--------------------------------------------------------------------------------
/config/schema/pathauto.schema.yml:
--------------------------------------------------------------------------------
1 | pathauto.settings:
2 | type: config_object
3 | mapping:
4 | punctuation:
5 | type: sequence
6 | sequence:
7 | type: integer
8 | verbose:
9 | type: boolean
10 | separator:
11 | type: string
12 | max_length:
13 | type: integer
14 | max_component_length:
15 | type: integer
16 | transliterate:
17 | type: boolean
18 | reduce_ascii:
19 | type: boolean
20 | ignore_words:
21 | type: string
22 | case:
23 | type: boolean
24 | ignore_words:
25 | type: string
26 | update_action:
27 | type: integer
28 |
29 | action.configuration.pathauto_update_alias:
30 | type: action_configuration_default
31 | label: 'Update URL-Alias'
32 |
--------------------------------------------------------------------------------
/config/schema/pathauto_pattern.schema.yml:
--------------------------------------------------------------------------------
1 | pathauto.pattern.*:
2 | type: config_entity
3 | label: 'Pathauto pattern config'
4 | mapping:
5 | id:
6 | type: string
7 | label: 'ID'
8 | label:
9 | type: label
10 | label: 'Label'
11 | uuid:
12 | type: string
13 | type:
14 | type: string
15 | label: 'Pattern type'
16 | pattern:
17 | type: string
18 | label: 'Pattern'
19 | selection_criteria:
20 | type: sequence
21 | label: 'Selection criteria'
22 | sequence:
23 | type: condition.plugin.[id]
24 | label: 'Selection condition'
25 | selection_logic:
26 | type: string
27 | label: 'Selection logic'
28 | weight:
29 | type: integer
30 | label: 'Weight'
31 | context_definitions:
32 | type: sequence
33 | label: 'Context definitions'
34 | sequence:
35 | - type: ctools.relationship
36 | label: 'Context'
37 |
--------------------------------------------------------------------------------
/pathauto.api.php:
--------------------------------------------------------------------------------
1 | $alias))->fetchField();
87 | }
88 |
89 | /**
90 | * Alter the pattern to be used before an alias is generated by Pathauto.
91 | *
92 | * This hook will only be called if a default pattern is configured (on
93 | * admin/config/search/path/patterns).
94 | *
95 | * @param string $pattern
96 | * The alias pattern for Pathauto to pass to token_replace() to generate the
97 | * URL alias.
98 | * @param array $context
99 | * An associative array of additional options, with the following elements:
100 | * - 'module': The module or entity type being aliased.
101 | * - 'op': A string with the operation being performed on the object being
102 | * aliased. Can be either 'insert', 'update', 'return', or 'bulkupdate'.
103 | * - 'source': A string of the source path for the alias (e.g. 'node/1').
104 | * - 'data': An array of keyed objects to pass to token_replace().
105 | * - 'type': The sub-type or bundle of the object being aliased.
106 | * - 'language': A string of the language code for the alias (e.g. 'en').
107 | * This can be altered by reference.
108 | */
109 | function hook_pathauto_pattern_alter(&$pattern, array $context) {
110 | // Switch out any [node:created:*] tokens with [node:updated:*] on update.
111 | if ($context['module'] == 'node' && ($context['op'] == 'update')) {
112 | $pattern = preg_replace('/\[node:created(\:[^]]*)?\]/', '[node:updated$1]', $pattern);
113 | }
114 | }
115 |
116 | /**
117 | * Alter Pathauto-generated aliases before saving.
118 | *
119 | * @param string $alias
120 | * The automatic alias after token replacement and strings cleaned.
121 | * @param array $context
122 | * An associative array of additional options, with the following elements:
123 | * - 'module': The module or entity type being aliased.
124 | * - 'op': A string with the operation being performed on the object being
125 | * aliased. Can be either 'insert', 'update', 'return', or 'bulkupdate'.
126 | * - 'source': A string of the source path for the alias (e.g. 'node/1').
127 | * This can be altered by reference.
128 | * - 'data': An array of keyed objects to pass to token_replace().
129 | * - 'type': The sub-type or bundle of the object being aliased.
130 | * - 'language': A string of the language code for the alias (e.g. 'en').
131 | * This can be altered by reference.
132 | * - 'pattern': A string of the pattern used for aliasing the object.
133 | */
134 | function hook_pathauto_alias_alter(&$alias, array &$context) {
135 | // Add a suffix so that all aliases get saved as 'content/my-title.html'
136 | $alias .= '.html';
137 |
138 | // Force all aliases to be saved as language neutral.
139 | $context['language'] = Language::LANGCODE_NOT_SPECIFIED;
140 | }
141 |
142 | /**
143 | * Alter the list of punctuation characters for Pathauto control.
144 | *
145 | * @param $punctuation
146 | * An array of punctuation to be controlled by Pathauto during replacement
147 | * keyed by punctuation name. Each punctuation record should be an array
148 | * with the following key/value pairs:
149 | * - value: The raw value of the punctuation mark.
150 | * - name: The human-readable name of the punctuation mark. This must be
151 | * translated using t() already.
152 | */
153 | function hook_pathauto_punctuation_chars_alter(array &$punctuation) {
154 | // Add the trademark symbol.
155 | $punctuation['trademark'] = array('value' => '™', 'name' => t('Trademark symbol'));
156 |
157 | // Remove the dollar sign.
158 | unset($punctuation['dollar']);
159 | }
160 |
--------------------------------------------------------------------------------
/pathauto.info.yml:
--------------------------------------------------------------------------------
1 | name : 'Pathauto'
2 | description : 'Provides a mechanism for modules to automatically generate aliases for the content they manage.'
3 | core: 8.x
4 | type: module
5 |
6 | dependencies:
7 | - ctools
8 | - path
9 | - token
10 |
11 | configure: entity.pathauto_pattern.collection
12 |
13 | recommends:
14 | - redirect
15 |
--------------------------------------------------------------------------------
/pathauto.install:
--------------------------------------------------------------------------------
1 | get('entity.definitions.installed');
32 | foreach ($collection->getAll() as $key => $definitions) {
33 | if (!is_array($definitions) || empty($definitions['path'])) {
34 | continue;
35 | }
36 |
37 | // Retrieve and change path base field definition.
38 | $path_definition = $definitions['path'];
39 | if (($options = $path_definition->getDisplayOptions('form')) && $options['type'] = 'pathauto') {
40 | $options['type'] = 'path';
41 | $path_definition->setDisplayOptions('form', $options);
42 | // Save the new value.
43 | $collection->set($key, $definitions);
44 | }
45 |
46 | }
47 |
48 | foreach (EntityFormDisplay::loadMultiple() as $form_display) {
49 | if ($component = $form_display->getComponent('path')) {
50 | if (isset($component['type']) && $component['type'] == 'pathauto') {
51 | $component['type'] = 'path';
52 | $form_display->setComponent('path', $component);
53 | $form_display->save();
54 | }
55 | }
56 | }
57 | }
58 |
59 | /**
60 | * Converts patterns from configuration objects to configuration entities.
61 | */
62 | function pathauto_update_8100(&$sandbox) {
63 | if (!\Drupal::service('module_handler')->moduleExists('ctools')) {
64 | throw new UpdateException('Install Chaos tools suite (https://www.drupal.org/project/ctools) before running this database update.');
65 | }
66 |
67 | $messages = array();
68 | $entity_manager = \Drupal::service('entity.manager');
69 | $entity_type_manager = \Drupal::service('entity_type.manager');
70 | $language_manager = \Drupal::service('language_manager');
71 | $entity_types = $entity_manager->getDefinitions();
72 |
73 | // 1. Load all patterns.
74 | $config = \Drupal::service('config.factory')->getEditable('pathauto.pattern');
75 | $patterns = $config->get('patterns');
76 |
77 | // 2. Create a configuration entity per pattern.
78 | foreach ($patterns as $entity_type => $entity_patterns) {
79 | if (!array_key_exists($entity_type, $entity_types)) {
80 | // We found an unknown entity type. Report it.
81 | $messages[] = t('Entity of type @type was not processed. It defines the following patterns: @patterns', array(
82 | '@type' => $entity_type,
83 | '@patterns' => print_r($entity_patterns, TRUE),
84 | ));
85 | continue;
86 | }
87 | $entity_label = $entity_types[$entity_type]->getLabel();
88 |
89 | if (isset($entity_patterns['default'])) {
90 | // This is a pattern for an entity type, such as "node".
91 | $pattern = PathautoPattern::create([
92 | 'id' => $entity_type,
93 | 'label' => $entity_label,
94 | 'type' => 'canonical_entities:' . $entity_type,
95 | 'pattern' => $entity_patterns['default'],
96 | 'weight' => 0,
97 | ]);
98 | $pattern->save();
99 | }
100 |
101 | // Loop over bundles and create patterns if they have a value.
102 | // Bundle keys may have a language suffix for language-dependant patterns.
103 | if (isset($entity_patterns['bundles'])) {
104 | $bundle_info = $entity_manager->getBundleInfo($entity_type);
105 | foreach ($entity_patterns['bundles'] as $bundle => $bundle_patterns) {
106 | if (empty($bundle_patterns['default'])) {
107 | // This bundle does not define a pattern. Move on to the next one.
108 | continue;
109 | }
110 |
111 | if (isset($bundle_info[$bundle])) {
112 | // This is a pattern for a bundle, such as "node_article".
113 | $pattern = PathautoPattern::create([
114 | 'id' => $entity_type . '_' . $bundle,
115 | 'label' => $entity_label . ' ' . $bundle_info[$bundle]['label'],
116 | 'type' => 'canonical_entities:' . $entity_type,
117 | 'pattern' => $bundle_patterns['default'],
118 | 'weight' => -5,
119 | ]);
120 |
121 | // Add the bundle condition.
122 | $pattern->addSelectionCondition([
123 | 'id' => 'entity_bundle:' . $entity_type,
124 | 'bundles' => array($bundle),
125 | 'negate' => FALSE,
126 | 'context_mapping' => [ $entity_type => $entity_type ],
127 | ]);
128 |
129 | $pattern->save();
130 | }
131 | else {
132 | // This is either a language dependent pattern such as "article_es" or
133 | // an unknown bundle or langcode. Let's figure it out.
134 | $matches = NULL;
135 | $langcode = NULL;
136 | preg_match('/^(.*)_([a-z-]*)$/', $bundle, $matches);
137 | if (count($matches) == 3) {
138 | list(, $extracted_bundle, $langcode) = $matches;
139 | $language = $language_manager->getLanguage($langcode);
140 | }
141 | // Validate bundle, langcode and language.
142 | if (!isset($bundle_info[$extracted_bundle]) || ($langcode == NULL) || ($language == NULL)) {
143 | $messages[] = t('Unrecognized entity bundle @entity:@bundle was not processed. It defines the following patterns: @patterns', array(
144 | '@entity' => $entity_type,
145 | '@bundle' => $bundle,
146 | '@patterns' => print_r($entity_patterns, TRUE),
147 | ));
148 | continue;
149 | }
150 |
151 | // This is a pattern for a bundle and a language, such as "node_article_es".
152 | $pattern = PathautoPattern::create([
153 | 'id' => $entity_type . '_' . $extracted_bundle . '_' . str_replace('-', '_', $langcode),
154 | 'label' => $entity_label . ' ' . $bundle_info[$extracted_bundle]['label'] . ' ' . $language->getName(),
155 | 'type' => 'canonical_entities:' . $entity_type,
156 | 'pattern' => $bundle_patterns['default'],
157 | 'weight' => -10,
158 | ]);
159 |
160 | // Add the bundle condition.
161 | $pattern->addSelectionCondition([
162 | 'id' => 'entity_bundle:' . $entity_type,
163 | 'bundles' => array($extracted_bundle => $extracted_bundle),
164 | 'negate' => FALSE,
165 | 'context_mapping' => [ $entity_type => $entity_type ],
166 | ]);
167 |
168 | // Add the language condition.
169 | $language_mapping = $entity_type . ':' . $entity_type_manager->getDefinition($entity_type)->getKey('langcode') . ':language';
170 | $pattern->addSelectionCondition([
171 | 'id' => 'language',
172 | 'langcodes' => [ $langcode => $langcode ],
173 | 'negate' => FALSE,
174 | 'context_mapping' => [
175 | 'language' => $language_mapping,
176 | ]
177 | ]);
178 |
179 | // Add the context relationship for this language.
180 | $new_definition = new ContextDefinition('language', 'Language');
181 | $new_context = new Context($new_definition);
182 | $pattern->addContext($language_mapping, $new_context);
183 |
184 | $pattern->save();
185 | }
186 | }
187 | }
188 | }
189 |
190 | // 3. Delete the old configuration object that stores patterns.
191 | $config->delete();
192 |
193 | // 4. Print out messages.
194 | if (!empty($messages)) {
195 | return implode('', $messages);
196 | }
197 | }
198 |
--------------------------------------------------------------------------------
/pathauto.js:
--------------------------------------------------------------------------------
1 | (function ($) {
2 |
3 | Drupal.behaviors.pathFieldsetSummaries = {
4 | attach: function (context) {
5 | $('fieldset.path-form', context).drupalSetSummary(function (context) {
6 | var path = $('.form-item-path-alias input', context).val();
7 | var automatic = $('.form-item-path-pathauto input', context).attr('checked');
8 |
9 | if (automatic) {
10 | return Drupal.t('Automatic alias');
11 | }
12 | else if (path) {
13 | return Drupal.t('Alias: @alias', { '@alias': path });
14 | }
15 | else {
16 | return Drupal.t('No alias');
17 | }
18 | });
19 | }
20 | };
21 |
22 | })(jQuery);
23 |
--------------------------------------------------------------------------------
/pathauto.libraries.yml:
--------------------------------------------------------------------------------
1 | widget:
2 | version: 1.0
3 | js:
4 | pathauto.js: {}
5 | dependencies:
6 | - core/jquery
7 | - core/drupal
8 | - core/drupalSettings
9 |
--------------------------------------------------------------------------------
/pathauto.links.action.yml:
--------------------------------------------------------------------------------
1 | entity.pathauto_pattern.add_form:
2 | route_name: 'entity.pathauto_pattern.add_form'
3 | title: 'Add Pathauto pattern'
4 | appears_on:
5 | - entity.pathauto_pattern.collection
6 |
7 |
--------------------------------------------------------------------------------
/pathauto.links.task.yml:
--------------------------------------------------------------------------------
1 | pathauto.patterns.form:
2 | route_name: entity.pathauto_pattern.collection
3 | base_route: path.admin_overview
4 | title: 'Patterns'
5 | weight: 10
6 |
7 | pathauto.settings.form:
8 | route_name: pathauto.settings.form
9 | base_route: path.admin_overview
10 | title: 'Settings'
11 | weight: 20
12 |
13 | pathauto.bulk.update.form:
14 | route_name: pathauto.bulk.update.form
15 | base_route: path.admin_overview
16 | title: 'Bulk generate'
17 | weight: 30
18 |
19 | pathauto.admin.delete:
20 | route_name: pathauto.admin.delete
21 | base_route: path.admin_overview
22 | title: 'Delete aliases'
23 | weight: 40
24 |
--------------------------------------------------------------------------------
/pathauto.module:
--------------------------------------------------------------------------------
1 | 'pathauto'));
41 | }
42 |
43 | /**
44 | * Implements hook_help().
45 | */
46 | function pathauto_help($route_name, RouteMatchInterface $route_match) {
47 | switch ($route_name) {
48 | case 'help.page.pathauto':
49 | $output = '
' . t('About') . '
';
50 | $output .= '' . t('Provides a mechanism for modules to automatically generate aliases for the content they manage.') . '
';
51 | $output .= '' . t('Settings') . '
';
52 | $output .= '';
53 | $output .= '- ' . t('Maximum alias and component length') . '
';
54 | $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())) . '
';
55 | $output .= '
';
56 | return $output;
57 |
58 | case 'pathauto.bulk.update.form':
59 | $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.') . '
';
60 | return $output;
61 | }
62 | }
63 |
64 | /**
65 | * Implements hook_entity_presave().
66 | */
67 | function pathauto_entity_presave($entity) {
68 | if (!($entity instanceof ContentEntityInterface) || !$entity->hasField('path')) {
69 | return;
70 | }
71 |
72 | // About to be saved (before insert/update)
73 | if ($entity->path->pathauto == PathautoState::SKIP && $entity->path->old_alias != '') {
74 | /*
75 | * There was an old alias, but when pathauto_perform_alias was checked
76 | * the javascript disabled the textbox which led to an empty value being
77 | * submitted. Restoring the old path-value here prevents the Path module
78 | * from deleting any old alias before Pathauto gets control.
79 | */
80 | $entity->path->alias = $entity->path->old_alias;
81 | }
82 | }
83 |
84 | /**
85 | * Implements hook_entity_insert().
86 | */
87 | function pathauto_entity_insert(EntityInterface $entity) {
88 | \Drupal::service('pathauto.generator')->updateEntityAlias($entity, 'insert');
89 | }
90 |
91 | /**
92 | * Implements hook_entity_update().
93 | */
94 | function pathauto_entity_update(EntityInterface $entity) {
95 | \Drupal::service('pathauto.generator')->updateEntityAlias($entity, 'update');
96 | }
97 |
98 |
99 | /**
100 | * Implements hook_entity_update().
101 | */
102 | function pathauto_entity_delete(EntityInterface $entity) {
103 | if ($entity->hasLinkTemplate('canonical')) {
104 | \Drupal::service('pathauto.alias_storage_helper')->deleteEntityPathAll($entity);
105 | }
106 | if ($entity instanceof ContentEntityInterface && $entity->hasField('path')) {
107 | $entity->path->first()->get('pathauto')->purge();
108 | }
109 | }
110 |
111 | /**
112 | * Implements hook_field_info_alter().
113 | */
114 | function pathauto_field_info_alter(&$info) {
115 | $info['path']['class'] = '\Drupal\pathauto\PathautoItem';
116 | }
117 |
118 | /**
119 | * Implements hook_field_widget_info_alter().
120 | */
121 | function pathauto_field_widget_info_alter(&$widgets) {
122 | $widgets['path']['class'] = 'Drupal\pathauto\PathautoWidget';
123 | }
124 |
125 | /**
126 | * Implements hook_entity_base_field_info().
127 | */
128 | function pathauto_entity_base_field_info(EntityTypeInterface $entity_type) {
129 | // @todo: Make this configurable and/or remove if
130 | // https://drupal.org/node/476294 is resolved.
131 | if ($entity_type->id() === 'user') {
132 | $fields['path'] = BaseFieldDefinition::create('path')
133 | ->setCustomStorage(TRUE)
134 | ->setLabel(t('URL alias'))
135 | ->setTranslatable(TRUE)
136 | ->setComputed(TRUE)
137 | ->setDisplayOptions('form', array(
138 | 'type' => 'path',
139 | 'weight' => 30,
140 | ))
141 | ->setDisplayConfigurable('form', TRUE);
142 |
143 | return $fields;
144 | }
145 | }
146 |
147 | /**
148 | * Implements hook_entity_base_field_info_alter().
149 | */
150 | function pathauto_entity_base_field_info_alter(&$fields, EntityTypeInterface $entity_type) {
151 | if (isset($fields['path'])) {
152 | // Path fields need to be computed so that the pathauto state can be
153 | // accessed even if there is no alias being set.
154 | $fields['path']->setComputed(TRUE);
155 | }
156 | }
157 |
--------------------------------------------------------------------------------
/pathauto.permissions.yml:
--------------------------------------------------------------------------------
1 | administer pathauto:
2 | title: 'Administer pathauto'
3 | description: 'Allows a user to configure patterns for automated aliases and bulk delete URL-aliases.'
4 | notify of path changes:
5 | title: 'Notify of Path Changes'
6 | description: 'Determines whether or not users are notified.'
7 |
--------------------------------------------------------------------------------
/pathauto.routing.yml:
--------------------------------------------------------------------------------
1 | entity.pathauto_pattern.collection:
2 | path: '/admin/config/search/path/patterns'
3 | defaults:
4 | _entity_list: 'pathauto_pattern'
5 | _title: 'Patterns'
6 | requirements:
7 | _permission: 'administer pathauto'
8 |
9 | entity.pathauto_pattern.add_form:
10 | path: '/admin/config/search/path/patterns/add'
11 | defaults:
12 | _entity_form: 'pathauto_pattern.default'
13 | _title: 'Add Pathauto pattern'
14 | tempstore_id: 'pathauto.pattern'
15 | requirements:
16 | _permission: 'administer pathauto'
17 |
18 | pathauto.settings.form:
19 | path: '/admin/config/search/path/settings'
20 | defaults:
21 | _form: '\Drupal\pathauto\Form\PathautoSettingsForm'
22 | _title: 'Settings'
23 | requirements:
24 | _permission: 'administer pathauto'
25 |
26 | pathauto.bulk.update.form:
27 | path: '/admin/config/search/path/update_bulk'
28 | defaults:
29 | _form: '\Drupal\pathauto\Form\PathautoBulkUpdateForm'
30 | _title: 'Bulk generate'
31 | requirements:
32 | _permission: 'administer url aliases'
33 |
34 | pathauto.admin.delete:
35 | path: '/admin/config/search/path/delete_bulk'
36 | defaults:
37 | _form: '\Drupal\pathauto\Form\PathautoAdminDelete'
38 | _title: 'Delete aliases'
39 | requirements:
40 | _permission: 'administer url aliases'
41 |
--------------------------------------------------------------------------------
/pathauto.services.yml:
--------------------------------------------------------------------------------
1 | services:
2 | pathauto.generator:
3 | class: Drupal\pathauto\PathautoGenerator
4 | arguments: ['@config.factory', '@module_handler', '@token', '@pathauto.alias_cleaner', '@pathauto.alias_storage_helper', '@pathauto.alias_uniquifier', '@pathauto.verbose_messenger', '@string_translation', '@token.entity_mapper']
5 | pathauto.alias_cleaner:
6 | class: Drupal\pathauto\AliasCleaner
7 | arguments: ['@config.factory', '@pathauto.alias_storage_helper', '@language_manager', '@cache.discovery', '@transliteration', '@module_handler']
8 | pathauto.alias_storage_helper:
9 | class: Drupal\pathauto\AliasStorageHelper
10 | arguments: ['@config.factory', '@path.alias_storage', '@database','@pathauto.verbose_messenger', '@string_translation']
11 | pathauto.alias_uniquifier:
12 | class: Drupal\pathauto\AliasUniquifier
13 | arguments: ['@config.factory', '@pathauto.alias_storage_helper','@module_handler', '@router.no_access_checks', '@path.alias_manager']
14 | pathauto.verbose_messenger:
15 | class: Drupal\pathauto\VerboseMessenger
16 | arguments: ['@config.factory', '@current_user']
17 | plugin.manager.alias_type:
18 | class: Drupal\pathauto\AliasTypeManager
19 | parent: default_plugin_manager
20 |
--------------------------------------------------------------------------------
/pathauto.tokens.inc:
--------------------------------------------------------------------------------
1 | t('Joined path'),
19 | 'description' => t('The array values each cleaned by Pathauto and then joined with the slash into a string that resembles an URL.'),
20 | );
21 |
22 | return $info;
23 | }
24 |
25 | /**
26 | * Implements hook_tokens().
27 | */
28 | function pathauto_tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) {
29 | $replacements = array();
30 |
31 | if ($type == 'array' && !empty($data['array'])) {
32 | $array = $data['array'];
33 |
34 | foreach ($tokens as $name => $original) {
35 | switch ($name) {
36 | case 'join-path':
37 | module_load_include('inc', 'pathauto');
38 | $values = array();
39 | foreach (token_element_children($array) as $key) {
40 | $value = is_array($array[$key]) ? render($array[$key]) : (string) $array[$key];
41 | $value = \Drupal::service('pathauto.alias_cleaner')->cleanString($value, $options);
42 | $values[] = $value;
43 | }
44 | $replacements[$original] = implode('/', $values);
45 | break;
46 | }
47 | }
48 | }
49 |
50 | return $replacements;
51 | }
52 |
--------------------------------------------------------------------------------
/src/AliasCleaner.php:
--------------------------------------------------------------------------------
1 | configFactory = $config_factory;
93 | $this->aliasStorageHelper = $alias_storage_helper;
94 | $this->languageManager = $language_manager;
95 | $this->cacheBackend = $cache_backend;
96 | $this->transliteration = $transliteration;
97 | $this->moduleHandler = $module_handler;
98 | }
99 |
100 | /**
101 | * {@inheritdoc}
102 | */
103 | public function cleanAlias($alias) {
104 | $config = $this->configFactory->get('pathauto.settings');
105 | $alias_max_length = min($config->get('max_length'), $this->aliasStorageHelper->getAliasSchemaMaxLength());
106 |
107 | $output = $alias;
108 |
109 | // Trim duplicate, leading, and trailing separators. Do this before cleaning
110 | // backslashes since a pattern like "[token1]/[token2]-[token3]/[token4]"
111 | // could end up like "value1/-/value2" and if backslashes were cleaned first
112 | // this would result in a duplicate blackslash.
113 | $output = $this->getCleanSeparators($output);
114 |
115 | // Trim duplicate, leading, and trailing backslashes.
116 | $output = $this->getCleanSeparators($output, '/');
117 |
118 | // Shorten to a logical place based on word boundaries.
119 | $output = Unicode::truncate($output, $alias_max_length, TRUE);
120 |
121 | return $output;
122 | }
123 |
124 | /**
125 | * {@inheritdoc}
126 | */
127 | public function getCleanSeparators($string, $separator = NULL) {
128 | $config = $this->configFactory->get('pathauto.settings');
129 |
130 | if (!isset($separator)) {
131 | $separator = $config->get('separator');
132 | }
133 |
134 | $output = $string;
135 |
136 | if (strlen($separator)) {
137 | // Trim any leading or trailing separators.
138 | $output = trim($output, $separator);
139 |
140 | // Escape the separator for use in regular expressions.
141 | $seppattern = preg_quote($separator, '/');
142 |
143 | // Replace multiple separators with a single one.
144 | $output = preg_replace("/$seppattern+/", $separator, $output);
145 |
146 | // Replace trailing separators around slashes.
147 | if ($separator !== '/') {
148 | $output = preg_replace("/\/+$seppattern\/+|$seppattern\/+|\/+$seppattern/", "/", $output);
149 | }
150 | else {
151 | // If the separator is a slash, we need to re-add the leading slash
152 | // dropped by the trim function.
153 | $output = '/' . $output;
154 | }
155 | }
156 |
157 | return $output;
158 | }
159 |
160 | /**
161 | * {@inheritdoc}
162 | */
163 | public function cleanString($string, array $options = array()) {
164 | if (empty($this->cleanStringCache)) {
165 | // Generate and cache variables used in this method.
166 | $config = $this->configFactory->get('pathauto.settings');
167 | $this->cleanStringCache = array(
168 | 'separator' => $config->get('separator'),
169 | 'strings' => array(),
170 | 'transliterate' => $config->get('transliterate'),
171 | 'punctuation' => array(),
172 | 'reduce_ascii' => (bool) $config->get('reduce_ascii'),
173 | 'ignore_words_regex' => FALSE,
174 | 'lowercase' => (bool) $config->get('case'),
175 | 'maxlength' => min($config->get('max_component_length'), $this->aliasStorageHelper->getAliasSchemaMaxLength()),
176 | );
177 |
178 | // Generate and cache the punctuation replacements for strtr().
179 | $punctuation = $this->getPunctuationCharacters();
180 | foreach ($punctuation as $name => $details) {
181 | $action = $config->get('punctuation.' . $name);
182 | switch ($action) {
183 | case PathautoGeneratorInterface::PUNCTUATION_REMOVE:
184 | $cache['punctuation'][$details['value']] = '';
185 | $this->cleanStringCache;
186 |
187 | case PathautoGeneratorInterface::PUNCTUATION_REPLACE:
188 | $this->cleanStringCache['punctuation'][$details['value']] = $this->cleanStringCache['separator'];
189 | break;
190 |
191 | case PathautoGeneratorInterface::PUNCTUATION_DO_NOTHING:
192 | // Literally do nothing.
193 | break;
194 | }
195 | }
196 |
197 | // Generate and cache the ignored words regular expression.
198 | $ignore_words = $config->get('ignore_words');
199 | $ignore_words_regex = preg_replace(array('/^[,\s]+|[,\s]+$/', '/[,\s]+/'), array('', '\b|\b'), $ignore_words);
200 | if ($ignore_words_regex) {
201 | $this->cleanStringCache['ignore_words_regex'] = '\b' . $ignore_words_regex . '\b';
202 | if (function_exists('mb_eregi_replace')) {
203 | mb_regex_encoding('UTF-8');
204 | $this->cleanStringCache['ignore_words_callback'] = 'mb_eregi_replace';
205 | }
206 | else {
207 | $this->cleanStringCache['ignore_words_callback'] = 'preg_replace';
208 | $this->cleanStringCache['ignore_words_regex'] = '/' . $this->cleanStringCache['ignore_words_regex'] . '/i';
209 | }
210 | }
211 | }
212 |
213 | // Empty strings do not need any processing.
214 | if ($string === '' || $string === NULL) {
215 | return '';
216 | }
217 |
218 | $langcode = NULL;
219 | if (!empty($options['language'])) {
220 | $langcode = $options['language']->getId();
221 | }
222 | elseif (!empty($options['langcode'])) {
223 | $langcode = $options['langcode'];
224 | }
225 |
226 | // Check if the string has already been processed, and if so return the
227 | // cached result.
228 | if (isset($this->cleanStringCache['strings'][$langcode][(string) $string])) {
229 | return $this->cleanStringCache['strings'][$langcode][(string) $string];
230 | }
231 |
232 | // Remove all HTML tags from the string.
233 | $output = Html::decodeEntities($string);
234 | $output = PlainTextOutput::renderFromHtml($output);
235 |
236 | // Optionally transliterate.
237 | if ($this->cleanStringCache['transliterate']) {
238 | // If the reduce strings to letters and numbers is enabled, don't bother
239 | // replacing unknown characters with a question mark. Use an empty string
240 | // instead.
241 | $output = $this->transliteration->transliterate($output, $langcode, $this->cleanStringCache['reduce_ascii'] ? '' : '?');
242 | }
243 |
244 | // Replace or drop punctuation based on user settings.
245 | $output = strtr($output, $this->cleanStringCache['punctuation']);
246 |
247 | // Reduce strings to letters and numbers.
248 | if ($this->cleanStringCache['reduce_ascii']) {
249 | $output = preg_replace('/[^a-zA-Z0-9\/]+/', $this->cleanStringCache['separator'], $output);
250 | }
251 |
252 | // Get rid of words that are on the ignore list.
253 | if ($this->cleanStringCache['ignore_words_regex']) {
254 | $words_removed = $this->cleanStringCache['ignore_words_callback']($this->cleanStringCache['ignore_words_regex'], '', $output);
255 | if (Unicode::strlen(trim($words_removed)) > 0) {
256 | $output = $words_removed;
257 | }
258 | }
259 |
260 | // Always replace whitespace with the separator.
261 | $output = preg_replace('/\s+/', $this->cleanStringCache['separator'], $output);
262 |
263 | // Trim duplicates and remove trailing and leading separators.
264 | $output = $this->getCleanSeparators($this->getCleanSeparators($output, $this->cleanStringCache['separator']));
265 |
266 | // Optionally convert to lower case.
267 | if ($this->cleanStringCache['lowercase']) {
268 | $output = Unicode::strtolower($output);
269 | }
270 |
271 | // Shorten to a logical place based on word boundaries.
272 | $output = Unicode::truncate($output, $this->cleanStringCache['maxlength'], TRUE);
273 |
274 | // Cache this result in the static array.
275 | $this->cleanStringCache['strings'][$langcode][(string) $string] = $output;
276 |
277 | return $output;
278 | }
279 |
280 |
281 | /**
282 | * {@inheritdoc}
283 | */
284 | public function getPunctuationCharacters() {
285 | if (empty($this->punctuationCharacters)) {
286 | $langcode = $this->languageManager->getCurrentLanguage()->getId();
287 |
288 | $cid = 'pathauto:punctuation:' . $langcode;
289 | if ($cache = $this->cacheBackend->get($cid)) {
290 | $this->punctuationCharacters = $cache->data;
291 | }
292 | else {
293 | $punctuation = array();
294 | $punctuation['double_quotes'] = array('value' => '"', 'name' => t('Double quotation marks'));
295 | $punctuation['quotes'] = array('value' => '\'', 'name' => t("Single quotation marks (apostrophe)"));
296 | $punctuation['backtick'] = array('value' => '`', 'name' => t('Back tick'));
297 | $punctuation['comma'] = array('value' => ',', 'name' => t('Comma'));
298 | $punctuation['period'] = array('value' => '.', 'name' => t('Period'));
299 | $punctuation['hyphen'] = array('value' => '-', 'name' => t('Hyphen'));
300 | $punctuation['underscore'] = array('value' => '_', 'name' => t('Underscore'));
301 | $punctuation['colon'] = array('value' => ':', 'name' => t('Colon'));
302 | $punctuation['semicolon'] = array('value' => ';', 'name' => t('Semicolon'));
303 | $punctuation['pipe'] = array('value' => '|', 'name' => t('Vertical bar (pipe)'));
304 | $punctuation['left_curly'] = array('value' => '{', 'name' => t('Left curly bracket'));
305 | $punctuation['left_square'] = array('value' => '[', 'name' => t('Left square bracket'));
306 | $punctuation['right_curly'] = array('value' => '}', 'name' => t('Right curly bracket'));
307 | $punctuation['right_square'] = array('value' => ']', 'name' => t('Right square bracket'));
308 | $punctuation['plus'] = array('value' => '+', 'name' => t('Plus sign'));
309 | $punctuation['equal'] = array('value' => '=', 'name' => t('Equal sign'));
310 | $punctuation['asterisk'] = array('value' => '*', 'name' => t('Asterisk'));
311 | $punctuation['ampersand'] = array('value' => '&', 'name' => t('Ampersand'));
312 | $punctuation['percent'] = array('value' => '%', 'name' => t('Percent sign'));
313 | $punctuation['caret'] = array('value' => '^', 'name' => t('Caret'));
314 | $punctuation['dollar'] = array('value' => '$', 'name' => t('Dollar sign'));
315 | $punctuation['hash'] = array('value' => '#', 'name' => t('Number sign (pound sign, hash)'));
316 | $punctuation['at'] = array('value' => '@', 'name' => t('At sign'));
317 | $punctuation['exclamation'] = array('value' => '!', 'name' => t('Exclamation mark'));
318 | $punctuation['tilde'] = array('value' => '~', 'name' => t('Tilde'));
319 | $punctuation['left_parenthesis'] = array('value' => '(', 'name' => t('Left parenthesis'));
320 | $punctuation['right_parenthesis'] = array('value' => ')', 'name' => t('Right parenthesis'));
321 | $punctuation['question_mark'] = array('value' => '?', 'name' => t('Question mark'));
322 | $punctuation['less_than'] = array('value' => '<', 'name' => t('Less-than sign'));
323 | $punctuation['greater_than'] = array('value' => '>', 'name' => t('Greater-than sign'));
324 | $punctuation['slash'] = array('value' => '/', 'name' => t('Slash'));
325 | $punctuation['back_slash'] = array('value' => '\\', 'name' => t('Backslash'));
326 |
327 | // Allow modules to alter the punctuation list and cache the result.
328 | $this->moduleHandler->alter('pathauto_punctuation_chars', $punctuation);
329 | $this->cacheBackend->set($cid, $punctuation);
330 | $this->punctuationCharacters = $punctuation;
331 | }
332 | }
333 |
334 | return $this->punctuationCharacters;
335 | }
336 |
337 | /**
338 | * {@inheritdoc}
339 | */
340 | public function cleanTokenValues(&$replacements, $data = array(), $options = array()) {
341 | foreach ($replacements as $token => $value) {
342 | // Only clean non-path tokens.
343 | if (!preg_match('/(path|alias|url|url-brief)\]$/', $token)) {
344 | $replacements[$token] = $this->cleanString($value, $options);
345 | }
346 | }
347 | }
348 |
349 | /**
350 | * {@inheritdoc}
351 | */
352 | public function resetCaches() {
353 | $this->cleanStringCache = array();
354 | }
355 |
356 | }
357 |
--------------------------------------------------------------------------------
/src/AliasCleanerInterface.php:
--------------------------------------------------------------------------------
1 | configFactory = $config_factory;
76 | $this->aliasStorage = $alias_storage;
77 | $this->database = $database;
78 | $this->messenger = $messenger;
79 | $this->stringTranslation = $string_translation;
80 | }
81 |
82 | /**
83 | * {@inheritdoc}
84 | */
85 | public function getAliasSchemaMaxLength() {
86 | return $this->aliasSchemaMaxLength;
87 | }
88 |
89 | /**
90 | * {@inheritdoc}
91 | */
92 | public function save(array $path, $existing_alias = NULL, $op = NULL) {
93 | $config = $this->configFactory->get('pathauto.settings');
94 |
95 | // Alert users if they are trying to create an alias that is the same as the
96 | // internal path.
97 | if ($path['source'] == $path['alias']) {
98 | $this->messenger->addMessage($this->t('Ignoring alias %alias because it is the same as the internal path.', array('%alias' => $path['alias'])));
99 | return NULL;
100 | }
101 |
102 | // Skip replacing the current alias with an identical alias.
103 | if (empty($existing_alias) || $existing_alias['alias'] != $path['alias']) {
104 | $path += array(
105 | 'pathauto' => TRUE,
106 | 'original' => $existing_alias,
107 | 'pid' => NULL,
108 | );
109 |
110 | // If there is already an alias, respect some update actions.
111 | if (!empty($existing_alias)) {
112 | switch ($config->get('update_action')) {
113 | case PathautoGeneratorInterface::UPDATE_ACTION_NO_NEW:
114 | // Do not create the alias.
115 | return NULL;
116 |
117 | case PathautoGeneratorInterface::UPDATE_ACTION_LEAVE:
118 | // Create a new alias instead of overwriting the existing by leaving
119 | // $path['pid'] empty.
120 | break;
121 |
122 | case PathautoGeneratorInterface::UPDATE_ACTION_DELETE:
123 | // The delete actions should overwrite the existing alias.
124 | $path['pid'] = $existing_alias['pid'];
125 | break;
126 | }
127 | }
128 |
129 | // Save the path array.
130 | $this->aliasStorage->save($path['source'], $path['alias'], $path['language'], $path['pid']);
131 |
132 | if (!empty($existing_alias['pid'])) {
133 | $this->messenger->addMessage($this->t(
134 | 'Created new alias %alias for %source, replacing %old_alias.',
135 | array(
136 | '%alias' => $path['alias'],
137 | '%source' => $path['source'],
138 | '%old_alias' => $existing_alias['alias'],
139 | )
140 | )
141 | );
142 | }
143 | else {
144 | $this->messenger->addMessage($this->t('Created new alias %alias for %source.', array(
145 | '%alias' => $path['alias'],
146 | '%source' => $path['source'],
147 | )));
148 | }
149 |
150 | return $path;
151 | }
152 | }
153 |
154 | /**
155 | * {@inheritdoc}
156 | */
157 | public function loadBySource($source, $language = LanguageInterface::LANGCODE_NOT_SPECIFIED) {
158 | // @todo convert this to be a query on alias storage.
159 | $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(
160 | ':source' => $source,
161 | ':language' => $language,
162 | ':language_none' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
163 | ))->fetchField();
164 | return $this->aliasStorage->load(array('pid' => $pid));
165 | }
166 |
167 | /**
168 | * {@inheritdoc}
169 | */
170 | public function exists($alias, $source, $language = LanguageInterface::LANGCODE_NOT_SPECIFIED) {
171 | 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(
172 | ':source' => $source,
173 | ':alias' => $alias,
174 | ':language' => $language,
175 | ':language_none' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
176 | ))->fetchField();
177 | }
178 |
179 | /**
180 | * {@inheritdoc}
181 | */
182 | public function deleteAll($source) {
183 | $pids = $this->loadBySourcePrefix($source);
184 | if ($pids) {
185 | $this->deleteMultiple($pids);
186 | }
187 | }
188 |
189 | /**
190 | * {@inheritdoc}
191 | */
192 | public function deleteEntityPathAll(EntityInterface $entity, $default_uri = NULL) {
193 | $this->deleteAll('/'. $entity->urlInfo()->getInternalPath());
194 | if (isset($default_uri) && $entity->urlInfo()->toString() != $default_uri) {
195 | $this->deleteAll($default_uri);
196 | }
197 | }
198 |
199 | /**
200 | * {@inheritdoc}
201 | */
202 | public function deleteMultiple($pids) {
203 | foreach ($pids as $pid) {
204 | $this->aliasStorage->delete(array('pid' => $pid));
205 | }
206 | }
207 |
208 | /**
209 | * {@inheritdoc}
210 | */
211 | public function loadBySourcePrefix($source) {
212 | return $this->database->query("SELECT pid FROM {url_alias} WHERE source = :source OR source LIKE :source_wildcard",
213 | [':source' => $source, ':source_wildcard' => $source . '/%'])
214 | ->fetchCol();
215 | }
216 |
217 | }
218 |
--------------------------------------------------------------------------------
/src/AliasStorageHelperInterface.php:
--------------------------------------------------------------------------------
1 | alterInfo('pathauto_alias_types');
33 | $this->setCacheBackend($cache_backend, 'pathauto_alias_types');
34 | }
35 |
36 | /**
37 | * Returns plugin definitions that support a given token type.
38 | *
39 | * @param string $type
40 | * The type of token plugin must support to be useful.
41 | *
42 | * @return array
43 | * Plugin definitions.
44 | */
45 | public function getPluginDefinitionByType($type) {
46 | $definitions = array_filter($this->getDefinitions(), function ($definition) use ($type) {
47 | if (!empty($definition['types']) && in_array($type, $definition['types'])) {
48 | return TRUE;
49 | }
50 | return FALSE;
51 | });
52 | return $definitions;
53 | }
54 |
55 | }
56 |
--------------------------------------------------------------------------------
/src/AliasUniquifier.php:
--------------------------------------------------------------------------------
1 | configFactory = $config_factory;
82 | $this->aliasStorageHelper = $alias_storage_helper;
83 | $this->moduleHandler = $module_handler;
84 | $this->urlMatcher = $url_matcher;
85 | $this->aliasManager = $alias_manager;
86 | }
87 |
88 | /**
89 | * {@inheritdoc}
90 | */
91 | public function uniquify(&$alias, $source, $langcode) {
92 | $config = $this->configFactory->get('pathauto.settings');
93 |
94 | if (!$this->isReserved($alias, $source, $langcode)) {
95 | return;
96 | }
97 |
98 | // If the alias already exists, generate a new, hopefully unique, variant.
99 | $maxlength = min($config->get('max_length'), $this->aliasStorageHelper->getAliasSchemaMaxlength());
100 | $separator = $config->get('separator');
101 | $original_alias = $alias;
102 |
103 | $i = 0;
104 | do {
105 | // Append an incrementing numeric suffix until we find a unique alias.
106 | $unique_suffix = $separator . $i;
107 | $alias = Unicode::truncate($original_alias, $maxlength - Unicode::strlen($unique_suffix), TRUE) . $unique_suffix;
108 | $i++;
109 | } while ($this->isReserved($alias, $source, $langcode));
110 | }
111 |
112 | /**
113 | * {@inheritdoc}
114 | */
115 | public function isReserved($alias, $source, $langcode = LanguageInterface::LANGCODE_NOT_SPECIFIED) {
116 | // Check if this alias already exists.
117 | if ($existing_source = $this->aliasManager->getPathByAlias($alias, $langcode)) {
118 | if ($existing_source != $alias) {
119 | // If it is an alias for the provided source, it is allowed to keep using
120 | // it. If not, then it is reserved.
121 | return $existing_source != $source;
122 | }
123 |
124 | }
125 |
126 | // Then check if there is a route with the same path.
127 | if ($this->isRoute($alias)) {
128 | return TRUE;
129 | }
130 | // Finally check if any other modules have reserved the alias.
131 | $args = array(
132 | $alias,
133 | $source,
134 | $langcode,
135 | );
136 | $implementations = $this->moduleHandler->getImplementations('pathauto_is_alias_reserved');
137 | foreach ($implementations as $module) {
138 |
139 | $result = $this->moduleHandler->invoke($module, 'pathauto_is_alias_reserved', $args);
140 |
141 | if (!empty($result)) {
142 | // As soon as the first module says that an alias is in fact reserved,
143 | // then there is no point in checking the rest of the modules.
144 | return TRUE;
145 | }
146 | }
147 |
148 | return FALSE;
149 | }
150 |
151 | /**
152 | * Verify if the given path is a valid route.
153 | *
154 | * @param string $path
155 | * A string containing a relative path.
156 | *
157 | * @return bool
158 | * TRUE if the path already exists.
159 | *
160 | * @throws \InvalidArgumentException
161 | */
162 | public function isRoute($path) {
163 | if (is_file(DRUPAL_ROOT . '/' . $path) || is_dir(DRUPAL_ROOT . '/' . $path)) {
164 | // Do not allow existing files or directories to get assigned an automatic
165 | // alias. Note that we do not need to use is_link() to check for symbolic
166 | // links since this returns TRUE for either is_file() or is_dir() already.
167 | return TRUE;
168 | }
169 |
170 | try {
171 | $route = $this->urlMatcher->match($path);
172 |
173 | if ($route['_route'] == $this->lastRouteName) {
174 | throw new \InvalidArgumentException('The given alias pattern (' . $path . ') always matches the route ' . $this->lastRouteName);
175 | }
176 |
177 | $this->lastRouteName = $route['_route'];
178 | return TRUE;
179 | }
180 | catch (ResourceNotFoundException $e) {
181 | $this->lastRouteName = NULL;
182 | return FALSE;
183 | }
184 | }
185 |
186 | }
187 |
--------------------------------------------------------------------------------
/src/AliasUniquifierInterface.php:
--------------------------------------------------------------------------------
1 | getSelectionConditions() as $id => $condition) {
139 | $criteria[$id] = $condition->getConfiguration();
140 | }
141 | $this->selection_criteria = $criteria;
142 |
143 | /** @var \Drupal\Core\Plugin\Context\ContextInterface[] $contexts */
144 | $contexts = $this->getContexts();
145 | foreach ($this->getAliasType()->getContexts() as $plugin_context_id => $plugin_context) {
146 | unset($contexts[$plugin_context_id]);
147 | }
148 | $this->context_definitions = [];
149 | foreach ($contexts as $context_id => $context) {
150 | $this->context_definitions[] = [
151 | 'id' => $context_id,
152 | 'label' => $context->getContextDefinition()->getLabel()
153 | ];
154 | }
155 |
156 | // Invalidate the static caches.
157 | \Drupal::service('pathauto.generator')->resetCaches();
158 | }
159 |
160 | /**
161 | * {@inheritdoc}
162 | */
163 | public static function postLoad(EntityStorageInterface $storage, array &$entities) {
164 | /** @var \Drupal\ctools\TypedDataResolver $resolver */
165 | $resolver = \Drupal::service('ctools.typed_data.resolver');
166 | /** @var \Drupal\pathauto\Entity\PathautoPattern $entity */
167 | foreach ($entities as $entity) {
168 | foreach ($entity->getContextDefinitions() as $definition) {
169 | $context = $resolver->convertTokenToContext($definition['id'], $entity->getContexts());
170 | $new_definition = new ContextDefinition(
171 | $context->getContextDefinition()->getDataType(),
172 | $definition['label'],
173 | $context->getContextDefinition()->isRequired(),
174 | $context->getContextDefinition()->isMultiple(),
175 | $context->getContextDefinition()->getDescription(),
176 | $context->getContextDefinition()->getDefaultValue()
177 | );
178 | $new_context = new Context($new_definition, $context->hasContextValue() ? $context->getContextValue() : NULL);
179 | $entity->addContext($definition['id'], $new_context);
180 | }
181 | }
182 | parent::postLoad($storage, $entities);
183 | }
184 |
185 | /**
186 | * {@inheritdoc}
187 | */
188 | public static function postDelete(EntityStorageInterface $storage, array $entities) {
189 | parent::postDelete($storage, $entities);
190 | // Invalidate the static caches.
191 | \Drupal::service('pathauto.generator')->resetCaches();
192 | }
193 |
194 | /**
195 | * {@inheritdoc}
196 | */
197 | public function calculateDependencies() {
198 | parent::calculateDependencies();
199 |
200 | $this->calculatePluginDependencies($this->getAliasType());
201 |
202 | foreach ($this->getSelectionConditions() as $instance) {
203 | $this->calculatePluginDependencies($instance);
204 | }
205 |
206 | return $this->getDependencies();
207 | }
208 |
209 | /**
210 | * {@inheritdoc}
211 | */
212 | public function getPattern() {
213 | return $this->pattern;
214 | }
215 |
216 | /**
217 | * {@inheritdoc}
218 | */
219 | public function setPattern($pattern) {
220 | $this->pattern = $pattern;
221 | return $this;
222 | }
223 |
224 | /**
225 | * {@inheritdoc}
226 | */
227 | public function getType() {
228 | return $this->type;
229 | }
230 |
231 | /**
232 | * {@inheritdoc}
233 | */
234 | public function getAliasType() {
235 | if (!$this->aliasTypeCollection) {
236 | $this->aliasTypeCollection = new DefaultSingleLazyPluginCollection(\Drupal::service('plugin.manager.alias_type'), $this->getType(), ['default' => $this->getPattern()]);
237 | }
238 | return $this->aliasTypeCollection->get($this->getType());
239 | }
240 |
241 | /**
242 | * {@inheritdoc}
243 | */
244 | public function getWeight() {
245 | return $this->weight;
246 | }
247 |
248 | /**
249 | * {@inheritdoc}
250 | */
251 | public function setWeight($weight) {
252 | $this->weight = $weight;
253 | return $this;
254 | }
255 |
256 | /**
257 | * {@inheritdoc}
258 | */
259 | public function hasContext($token) {
260 | $contexts = $this->getAliasType()->getContexts();
261 | $contexts += $this->contexts;
262 | return !empty($contexts[$token]);
263 | }
264 |
265 | /**
266 | * {@inheritdoc}
267 | */
268 | public function getContext($token) {
269 | $contexts = $this->getAliasType()->getContexts();
270 | $contexts += $this->contexts;
271 | return $contexts[$token];
272 | }
273 |
274 | /**
275 | * {@inheritdoc}
276 | */
277 | public function getContexts() {
278 | $contexts = $this->getAliasType()->getContexts();
279 | $contexts += $this->contexts;
280 | return $contexts;
281 | }
282 |
283 | /**
284 | * {@inheritdoc}
285 | */
286 | public function addContext($token, ContextInterface $context) {
287 | if (!$this->hasContext($token)) {
288 | $this->contexts[$token] = $context;
289 | }
290 | return $this;
291 | }
292 |
293 | /**
294 | * {@inheritdoc}
295 | */
296 | public function replaceContext($token, ContextInterface $context) {
297 | if ($this->hasContext($token)) {
298 | $this->contexts[$token] = $context;
299 | }
300 | return $this;
301 | }
302 |
303 | /**
304 | * {@inheritdoc}
305 | */
306 | public function removeContext($token) {
307 | if (isset($this->contexts[$token])) {
308 | unset($this->contexts[$token]);
309 | }
310 | return $this;
311 | }
312 |
313 | /**
314 | * {@inheritdoc}
315 | */
316 | public function getContextDefinitions() {
317 | return $this->context_definitions;
318 | }
319 |
320 | /**
321 | * {@inheritdoc}
322 | */
323 | public function getSelectionConditions() {
324 | if (!$this->selectionConditionCollection) {
325 | $this->selectionConditionCollection = new ConditionPluginCollection(\Drupal::service('plugin.manager.condition'), $this->get('selection_criteria'));
326 | }
327 | return $this->selectionConditionCollection;
328 | }
329 |
330 | /**
331 | * {@inheritdoc}
332 | */
333 | public function addSelectionCondition(array $configuration) {
334 | $configuration['uuid'] = $this->uuidGenerator()->generate();
335 | $this->getSelectionConditions()->addInstanceId($configuration['uuid'], $configuration);
336 | return $configuration['uuid'];
337 | }
338 |
339 | /**
340 | * {@inheritdoc}
341 | */
342 | public function getSelectionCondition($condition_id) {
343 | return $this->getSelectionConditions()->get($condition_id);
344 | }
345 |
346 | /**
347 | * {@inheritdoc}
348 | */
349 | public function removeSelectionCondition($condition_id) {
350 | $this->getSelectionConditions()->removeInstanceId($condition_id);
351 | return $this;
352 | }
353 |
354 | /**
355 | * {@inheritdoc}
356 | */
357 | public function getSelectionLogic() {
358 | return $this->selection_logic;
359 | }
360 |
361 | /**
362 | * {@inheritdoc}
363 | */
364 | public function applies($object) {
365 | if ($this->getAliasType()->applies($object)) {
366 | $definitions = $this->getAliasType()->getContextDefinitions();
367 | if (count($definitions) > 1) {
368 | throw new \Exception("Alias types do not support more than one context.");
369 | }
370 | $keys = array_keys($definitions);
371 | // Set the context object on our Alias plugin before retrieving contexts.
372 | $this->getAliasType()->setContextValue($keys[0], $object);
373 | /** @var \Drupal\Core\Plugin\Context\ContextInterface[] $base_contexts */
374 | $base_contexts = $this->getAliasType()->getContexts();
375 | $contexts = [];
376 | foreach ($base_contexts as $context_id => $base_context) {
377 | $contexts[$context_id] = new Context($base_context->getContextDefinition(), $object);
378 | }
379 | /** @var \Drupal\ctools\TypedDataResolver $resolver */
380 | $resolver = \Drupal::service('ctools.typed_data.resolver');
381 | foreach ($this->getContexts() as $token => $context) {
382 | $contexts[$token] = $resolver->convertTokenToContext($token, $contexts);
383 | }
384 | /** @var \Drupal\Core\Plugin\Context\ContextHandler $context_handler */
385 | $context_handler = \Drupal::service('context.handler');
386 | $conditions = $this->getSelectionConditions();
387 | foreach ($conditions as $condition) {
388 | if ($condition instanceof ContextAwarePluginInterface) {
389 | $context_handler->applyContextMapping($condition, $contexts);
390 | }
391 | $result = $condition->execute();
392 | if ($this->getSelectionLogic() == 'and' && !$result) {
393 | return FALSE;
394 | }
395 | elseif ($this->getSelectionLogic() == 'or' && $result) {
396 | return TRUE;
397 | }
398 | }
399 | return TRUE;
400 | }
401 | return FALSE;
402 | }
403 |
404 | }
405 |
--------------------------------------------------------------------------------
/src/Form/PathautoAdminDelete.php:
--------------------------------------------------------------------------------
1 | aliasTypeManager = $alias_type_manager;
35 | }
36 |
37 | /**
38 | * {@inheritdoc}
39 | */
40 | public static function create(ContainerInterface $container) {
41 | return new static(
42 | $container->get('plugin.manager.alias_type')
43 | );
44 | }
45 |
46 | /**
47 | * {@inheritdoc}
48 | */
49 | public function getFormId() {
50 | return 'pathauto_admin_delete';
51 | }
52 |
53 | /**
54 | * {@inheritdoc}
55 | */
56 | public function buildForm(array $form, FormStateInterface $form_state) {
57 | $form['delete'] = array(
58 | '#type' => 'fieldset',
59 | '#title' => t('Choose aliases to delete'),
60 | '#tree' => TRUE,
61 | );
62 |
63 | // First we do the "all" case.
64 | $total_count = db_query('SELECT count(1) FROM {url_alias}')->fetchField();
65 | $form['delete']['all_aliases'] = array(
66 | '#type' => 'checkbox',
67 | '#title' => t('All aliases'),
68 | '#default_value' => FALSE,
69 | '#description' => t('Delete all aliases. Number of aliases which will be deleted: %count.', array('%count' => $total_count)),
70 | );
71 |
72 | // Next, iterate over all alias types
73 | $definitions = $this->aliasTypeManager->getDefinitions();
74 |
75 | foreach ($definitions as $id => $definition) {
76 | /** @var \Drupal\pathauto\AliasTypeInterface $alias_type */
77 | $alias_type = $this->aliasTypeManager->createInstance($id);
78 | $count = db_query("SELECT count(1) FROM {url_alias} WHERE source LIKE :src", array(':src' => $alias_type->getSourcePrefix() . '%'))->fetchField();
79 | $form['delete']['plugins'][$id] = array(
80 | '#type' => 'checkbox',
81 | '#title' => (string) $definition['label'],
82 | '#default_value' => FALSE,
83 | '#description' => t('Delete aliases for all @label. Number of aliases which will be deleted: %count.', array('@label' => (string) $definition['label'], '%count' => $count)),
84 | );
85 | }
86 |
87 | // Warn them and give a button that shows we mean business.
88 | $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.') . '
');
89 | $form['buttons']['submit'] = array(
90 | '#type' => 'submit',
91 | '#value' => t('Delete aliases now!'),
92 | );
93 |
94 | return $form;
95 | }
96 |
97 | /**
98 | * {@inheritdoc}
99 | */
100 | public function submitForm(array &$form, FormStateInterface $form_state) {
101 | if ($form_state->getValue(['delete', 'all_aliases'])) {
102 | db_delete('url_alias')
103 | ->execute();
104 | drupal_set_message($this->t('All of your path aliases have been deleted.'));
105 | }
106 | foreach (array_keys(array_filter($form_state->getValue(['delete', 'plugins']))) as $id) {
107 | /** @var \Drupal\pathauto\AliasTypeInterface $alias_type */
108 | $alias_type = $this->aliasTypeManager->createInstance($id);
109 | db_delete('url_alias')
110 | ->condition('source', db_like($alias_type->getSourcePrefix()) . '%', 'LIKE')
111 | ->execute();
112 | drupal_set_message(t('All of your %label path aliases have been deleted.', array('%label' => $alias_type->getLabel())));
113 | }
114 | $form_state->setRedirect('pathauto.admin.delete');
115 | }
116 |
117 | }
118 |
--------------------------------------------------------------------------------
/src/Form/PathautoBulkUpdateForm.php:
--------------------------------------------------------------------------------
1 | aliasTypeManager = $alias_type_manager;
36 | }
37 |
38 | /**
39 | * {@inheritdoc}
40 | */
41 | public static function create(ContainerInterface $container) {
42 | return new static(
43 | $container->get('plugin.manager.alias_type')
44 | );
45 | }
46 |
47 | /**
48 | * {@inheritdoc}
49 | */
50 | public function getFormId() {
51 | return 'pathauto_bulk_update_form';
52 | }
53 |
54 | /**
55 | * {@inheritdoc}
56 | */
57 | public function buildForm(array $form, FormStateInterface $form_state) {
58 |
59 | $form = array();
60 |
61 | $form['#update_callbacks'] = array();
62 |
63 | $form['update'] = array(
64 | '#type' => 'checkboxes',
65 | '#title' => t('Select the types of un-aliased paths for which to generate URL aliases'),
66 | '#options' => array(),
67 | '#default_value' => array(),
68 | );
69 |
70 | $definitions = $this->aliasTypeManager->getDefinitions();
71 |
72 | foreach ($definitions as $id => $definition) {
73 | $alias_type = $this->aliasTypeManager->createInstance($id);
74 | if ($alias_type instanceof AliasTypeBatchUpdateInterface) {
75 | $form['update']['#options'][$id] = $alias_type->getLabel();
76 | }
77 | }
78 |
79 | $form['actions']['#type'] = 'actions';
80 | $form['actions']['submit'] = array(
81 | '#type' => 'submit',
82 | '#value' => t('Update'),
83 | );
84 |
85 | return $form;
86 | }
87 |
88 | /**
89 | * {@inheritdoc}
90 | */
91 | public function submitForm(array &$form, FormStateInterface $form_state) {
92 | $batch = array(
93 | 'title' => t('Bulk updating URL aliases'),
94 | 'operations' => array(
95 | array('Drupal\pathauto\Form\PathautoBulkUpdateForm::batchStart', array()),
96 | ),
97 | 'finished' => 'Drupal\pathauto\Form\PathautoBulkUpdateForm::batchFinished',
98 | );
99 |
100 | foreach ($form_state->getValue('update') as $id) {
101 | if (!empty($id)) {
102 | $batch['operations'][] = array('Drupal\pathauto\Form\PathautoBulkUpdateForm::batchProcess', array($id));
103 | }
104 | }
105 |
106 | batch_set($batch);
107 | }
108 |
109 | /**
110 | * Batch callback; count the current number of URL aliases for comparison later.
111 | */
112 | public static function batchStart(&$context) {
113 | $context['results']['count_before'] = db_select('url_alias')->countQuery()->execute()->fetchField();
114 | }
115 |
116 | /**
117 | * Common batch processing callback for all operations.
118 | *
119 | * Required to load our include the proper batch file.
120 | */
121 | public static function batchProcess($id, &$context) {
122 | /** @var \Drupal\pathauto\AliasTypeBatchUpdateInterface $alias_type */
123 | $alias_type = \Drupal::service('plugin.manager.alias_type')->createInstance($id);
124 | $alias_type->batchUpdate($context);
125 | }
126 |
127 | /**
128 | * Batch finished callback.
129 | */
130 | public static function batchFinished($success, $results, $operations) {
131 | if ($success) {
132 | // Count the current number of URL aliases after the batch is completed
133 | // and compare to the count before the batch started.
134 | $results['count_after'] = db_select('url_alias')->countQuery()->execute()->fetchField();
135 | $results['count_changed'] = max($results['count_after'] - $results['count_before'], 0);
136 | if ($results['count_changed']) {
137 | drupal_set_message(\Drupal::translation()->formatPlural($results['count_changed'], 'Generated 1 URL alias.', 'Generated @count URL aliases.'));
138 | }
139 | else {
140 | drupal_set_message(t('No new URL aliases to generate.'));
141 | }
142 | }
143 | else {
144 | $error_operation = reset($operations);
145 | 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))));
146 | }
147 | }
148 |
149 | }
150 |
--------------------------------------------------------------------------------
/src/Form/PathautoSettingsForm.php:
--------------------------------------------------------------------------------
1 | config('pathauto.settings');
51 |
52 | $form = array();
53 |
54 | $form['verbose'] = array(
55 | '#type' => 'checkbox',
56 | '#title' => t('Verbose'),
57 | '#default_value' => $config->get('verbose'),
58 | '#description' => t('Display alias changes (except during bulk updates).'),
59 | );
60 |
61 | $form['separator'] = array(
62 | '#type' => 'textfield',
63 | '#title' => t('Separator'),
64 | '#size' => 1,
65 | '#maxlength' => 1,
66 | '#default_value' => $config->get('separator'),
67 | '#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.'),
68 | );
69 |
70 | $form['case'] = array(
71 | '#type' => 'radios',
72 | '#title' => t('Character case'),
73 | '#default_value' => $config->get('case'),
74 | '#options' => array(
75 | self::CASE_LEAVE_ASIS => t('Leave case the same as source token values.'),
76 | self::CASE_LOWER => t('Change to lower case'),
77 | ),
78 | );
79 |
80 | $max_length = \Drupal::service('pathauto.alias_storage_helper')->getAliasSchemaMaxlength();
81 |
82 | $help_link = '';
83 | if (\Drupal::moduleHandler()->moduleExists('help')) {
84 | $help_link = ' ' . t('See Pathauto help for details.', [':pathauto-help' => Url::fromRoute('help.page', ['name' => 'pathauto'])->toString()]);
85 | }
86 |
87 | $form['max_length'] = array(
88 | '#type' => 'number',
89 | '#title' => t('Maximum alias length'),
90 | '#size' => 3,
91 | '#maxlength' => 3,
92 | '#default_value' => $config->get('max_length'),
93 | '#min' => 1,
94 | '#max' => $max_length,
95 | '#description' => t('Maximum length of aliases to generate. 100 is the recommended length. @max is the maximum possible length.', array('@max' => $max_length)) . $help_link,
96 | );
97 |
98 | $form['max_component_length'] = array(
99 | '#type' => 'number',
100 | '#title' => t('Maximum component length'),
101 | '#size' => 3,
102 | '#maxlength' => 3,
103 | '#default_value' => $config->get('max_component_length'),
104 | '#min' => 1,
105 | '#max' => $max_length,
106 | '#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.', ['@max' => $max_length]) . $help_link,
107 | );
108 |
109 | $description = t('What should Pathauto do when updating an existing content item which already has an alias?');
110 | if (\Drupal::moduleHandler()->moduleExists('redirect')) {
111 | $description .= ' ' . t('The Redirect module settings affect whether a redirect is created when an alias is deleted.', array(':url' => \Drupal::url('redirect.settings')));
112 | }
113 | else {
114 | $description .= ' ' . t('Considering installing the Redirect module to get redirects when your aliases change.', array(':url' => 'http://drupal.org/project/redirect'));
115 | }
116 |
117 | $form['update_action'] = array(
118 | '#type' => 'radios',
119 | '#title' => t('Update action'),
120 | '#default_value' => $config->get('update_action'),
121 | '#options' => array(
122 | PathautoGeneratorInterface::UPDATE_ACTION_NO_NEW => t('Do nothing. Leave the old alias intact.'),
123 | PathautoGeneratorInterface::UPDATE_ACTION_LEAVE => t('Create a new alias. Leave the existing alias functioning.'),
124 | PathautoGeneratorInterface::UPDATE_ACTION_DELETE => t('Create a new alias. Delete the old alias.'),
125 | ),
126 | '#description' => $description,
127 | );
128 |
129 | $form['transliterate'] = array(
130 | '#type' => 'checkbox',
131 | '#title' => t('Transliterate prior to creating alias'),
132 | '#default_value' => $config->get('transliterate'),
133 | '#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.'),
134 | );
135 |
136 | $form['reduce_ascii'] = array(
137 | '#type' => 'checkbox',
138 | '#title' => t('Reduce strings to letters and numbers'),
139 | '#default_value' => $config->get('reduce_ascii'),
140 | '#description' => t('Filters the new alias to only letters and numbers found in the ASCII-96 set.'),
141 | );
142 |
143 | $form['ignore_words'] = array(
144 | '#type' => 'textarea',
145 | '#title' => t('Strings to Remove'),
146 | '#default_value' => $config->get('ignore_words'),
147 | '#description' => t('Words to strip out of the URL alias, separated by commas. Do not use this to remove punctuation.'),
148 | '#wysiwyg' => FALSE,
149 | );
150 |
151 | $form['punctuation'] = array(
152 | '#type' => 'fieldset',
153 | '#title' => t('Punctuation'),
154 | '#collapsible' => TRUE,
155 | '#collapsed' => TRUE,
156 | '#tree' => TRUE,
157 | );
158 |
159 | $punctuation = \Drupal::service('pathauto.alias_cleaner')->getPunctuationCharacters();
160 |
161 | foreach ($punctuation as $name => $details) {
162 | // Use the value from config if it exists.
163 | if ($config->get('punctuation.' . $name) !== NULL) {
164 | $details['default'] = $config->get('punctuation.' . $name) !== NULL;
165 | }
166 | else {
167 | // Otherwise use the correct default.
168 | $details['default'] = $details['value'] == $config->get('separator') ? PathautoGeneratorInterface::PUNCTUATION_REPLACE : PathautoGeneratorInterface::PUNCTUATION_REMOVE;
169 | }
170 | $form['punctuation'][$name] = array(
171 | '#type' => 'select',
172 | '#title' => $details['name'] . ' (' . SafeMarkup::checkPlain($details['value']) . '
)',
173 | '#default_value' => $details['default'],
174 | '#options' => array(
175 | PathautoGeneratorInterface::PUNCTUATION_REMOVE => t('Remove'),
176 | PathautoGeneratorInterface::PUNCTUATION_REPLACE => t('Replace by separator'),
177 | PathautoGeneratorInterface::PUNCTUATION_DO_NOTHING => t('No action (do not replace)'),
178 | ),
179 | );
180 | }
181 |
182 | return parent::buildForm($form, $form_state);
183 | }
184 |
185 | /**
186 | * {@inheritdoc}
187 | */
188 | public function submitForm(array &$form, FormStateInterface $form_state) {
189 |
190 | $config = $this->config('pathauto.settings');
191 |
192 | $form_state->cleanValues();
193 | foreach ($form_state->getValues() as $key => $value) {
194 | $config->set($key, $value);
195 | }
196 |
197 | $config->save();
198 |
199 | parent::submitForm($form, $form_state);
200 | }
201 |
202 | }
203 |
--------------------------------------------------------------------------------
/src/Form/PatternEditForm.php:
--------------------------------------------------------------------------------
1 | get('plugin.manager.alias_type'),
58 | $container->get('entity_type.bundle.info'),
59 | $container->get('entity_type.manager'),
60 | $container->get('language_manager')
61 | );
62 | }
63 |
64 | /**
65 | * PatternEditForm constructor.
66 | *
67 | * @param \Drupal\pathauto\AliasTypeManager $manager
68 | * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
69 | * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
70 | * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
71 | */
72 | function __construct(AliasTypeManager $manager, EntityTypeBundleInfoInterface $entity_type_bundle_info, EntityTypeManagerInterface $entity_type_manager, LanguageManagerInterface $language_manager) {
73 | $this->manager = $manager;
74 | $this->entityTypeBundleInfo = $entity_type_bundle_info;
75 | $this->entityTypeManager = $entity_type_manager;
76 | $this->languageManager= $language_manager;
77 | }
78 |
79 | /**
80 | * {@inheritDoc}
81 | */
82 | public function buildForm(array $form, FormStateInterface $form_state) {
83 |
84 | $options = [];
85 | foreach ($this->manager->getDefinitions() as $plugin_id => $plugin_definition) {
86 | $options[$plugin_id] = $plugin_definition['label'];
87 | }
88 | $form['type'] = [
89 | '#type' => 'select',
90 | '#title' => $this->t('Pattern type'),
91 | '#default_value' => $this->entity->getType(),
92 | '#options' => $options,
93 | '#required' => TRUE,
94 | '#limit_validation_errors' => array(array('type')),
95 | '#submit' => array('::submitSelectType'),
96 | '#executes_submit_callback' => TRUE,
97 | '#ajax' => array(
98 | 'callback' => '::ajaxReplacePatternForm',
99 | 'wrapper' => 'pathauto-pattern',
100 | 'method' => 'replace',
101 | ),
102 | ];
103 |
104 | $form['pattern_container'] = [
105 | '#type' => 'container',
106 | '#prefix' => '',
107 | '#suffix' => '
',
108 | ];
109 |
110 | // if there is no type yet, stop here.
111 | if ($this->entity->getType()) {
112 |
113 | $alias_type = $this->entity->getAliasType();
114 |
115 | $form['pattern_container']['pattern'] = array(
116 | '#type' => 'textfield',
117 | '#title' => 'Path pattern',
118 | '#default_value' => $this->entity->getPattern(),
119 | '#size' => 65,
120 | '#maxlength' => 1280,
121 | '#element_validate' => array('token_element_validate'),
122 | '#after_build' => array('token_element_validate'),
123 | '#token_types' => $alias_type->getTokenTypes(),
124 | '#min_tokens' => 1,
125 | );
126 |
127 | // Show the token help relevant to this pattern type.
128 | $form['pattern_container']['token_help'] = array(
129 | '#theme' => 'token_tree_link',
130 | '#token_types' => $alias_type->getTokenTypes(),
131 | );
132 |
133 | // Expose bundle and language conditions.
134 | if ($alias_type->getDerivativeId() && $entity_type = $this->entityTypeManager->getDefinition($alias_type->getDerivativeId())) {
135 |
136 | $default_bundles = [];
137 | $default_languages = [];
138 | foreach ($this->entity->getSelectionConditions() as $condition_id => $condition) {
139 | if ($condition->getPluginId() == 'entity_bundle:' . $entity_type->id()) {
140 | $default_bundles = $condition->getConfiguration()['bundles'];
141 | }
142 | elseif ($condition->getPluginId() == 'language') {
143 | $default_languages = $condition->getConfiguration()['langcodes'];
144 | }
145 | }
146 |
147 | if ($entity_type->hasKey('bundle') && $bundles = $this->entityTypeBundleInfo->getBundleInfo($entity_type->id())) {
148 | $bundle_options = [];
149 | foreach ($bundles as $id => $info) {
150 | $bundle_options[$id] = $info['label'];
151 | }
152 | $form['pattern_container']['bundles'] = array(
153 | '#title' => $entity_type->getBundleLabel(),
154 | '#type' => 'checkboxes',
155 | '#options' => $bundle_options,
156 | '#default_value' => $default_bundles,
157 | '#description' => t('Check to which types this pattern should be applied. Leave empty to allow any.'),
158 | );
159 | }
160 |
161 | if ($this->languageManager->isMultilingual() && $entity_type->isTranslatable()) {
162 | $language_options = [];
163 | foreach ($this->languageManager->getLanguages() as $id => $language) {
164 | $language_options[$id] = $language->getName();
165 | }
166 | $form['pattern_container']['languages'] = array(
167 | '#title' => $this->t('Languages'),
168 | '#type' => 'checkboxes',
169 | '#options' => $language_options,
170 | '#default_value' => $default_languages,
171 | '#description' => t('Check to which languages this pattern should be applied. Leave empty to allow any.'),
172 | );
173 | }
174 | }
175 | }
176 |
177 | $form['label'] = array(
178 | '#type' => 'textfield',
179 | '#title' => $this->t('Label'),
180 | '#maxlength' => 255,
181 | '#default_value' => $this->entity->label(),
182 | '#required' => TRUE,
183 | );
184 |
185 | $form['id'] = array(
186 | '#type' => 'machine_name',
187 | '#title' => $this->t('ID'),
188 | '#maxlength' => 255,
189 | '#default_value' => $this->entity->id(),
190 | '#required' => TRUE,
191 | '#disabled' => !$this->entity->isNew(),
192 | '#machine_name' => array(
193 | 'exists' => 'Drupal\pathauto\Entity\PathautoPattern::load',
194 | ),
195 | );
196 |
197 | return parent::buildForm($form, $form_state);
198 | }
199 |
200 | /**
201 | * {@inheritDoc}
202 | */
203 | public function buildEntity(array $form, FormStateInterface $form_state) {
204 | /** @var \Drupal\pathauto\PathautoPatternInterface $entity */
205 | $entity = parent::buildEntity($form, $form_state);
206 |
207 | $default_weight = 0;
208 |
209 | $alias_type = $entity->getAliasType();
210 | if ($alias_type->getDerivativeId() && $this->entityTypeManager->hasDefinition($alias_type->getDerivativeId())) {
211 | $entity_type = $alias_type->getDerivativeId();
212 | // First, remove bundle and language conditions.
213 | foreach ($entity->getSelectionConditions() as $condition_id => $condition) {
214 |
215 | if ($condition->getPluginId() == 'entity_bundle:' . $entity_type || $condition->getPluginId() == 'language') {
216 | $entity->removeSelectionCondition($condition_id);
217 | }
218 | }
219 |
220 | if ($bundles = array_filter((array) $form_state->getValue('bundles'))) {
221 | $default_weight -= 5;
222 | $entity->addSelectionCondition(
223 | [
224 | 'id' => 'entity_bundle:' . $entity_type,
225 | 'bundles' => $bundles,
226 | 'negate' => FALSE,
227 | 'context_mapping' => [
228 | $entity_type => $entity_type,
229 | ]
230 | ]
231 | );
232 | }
233 |
234 | if ($languages = array_filter((array) $form_state->getValue('languages'))) {
235 | $default_weight -= 5;
236 | $language_mapping = $entity_type . ':' . $this->entityTypeManager->getDefinition($entity_type)->getKey('langcode') . ':language';
237 | $entity->addSelectionCondition(
238 | [
239 | 'id' => 'language',
240 | 'langcodes' => array_combine($languages, $languages),
241 | 'negate' => FALSE,
242 | 'context_mapping' => [
243 | 'language' => $language_mapping,
244 | ]
245 | ]
246 | );
247 | $new_definition = new ContextDefinition('language', 'Language');
248 | $new_context = new Context($new_definition);
249 | $entity->addContext($language_mapping, $new_context);
250 | }
251 |
252 | }
253 |
254 | $entity->setWeight($default_weight);
255 |
256 | return $entity;
257 | }
258 |
259 | /**
260 | * {@inheritDoc}
261 | */
262 | public function save(array $form, FormStateInterface $form_state) {
263 | parent::save($form, $form_state);
264 | drupal_set_message($this->t('Pattern @label saved.', ['@label' => $this->entity->label()]));
265 | $form_state->setRedirectUrl($this->entity->toUrl('collection'));
266 | }
267 |
268 | /**
269 | * Handles switching the type selector.
270 | */
271 | public function ajaxReplacePatternForm($form, FormStateInterface $form_state) {
272 | return $form['pattern_container'];
273 | }
274 |
275 | /**
276 | * Handles submit call when alias type is selected.
277 | */
278 | public function submitSelectType(array $form, FormStateInterface $form_state) {
279 | $this->entity = $this->buildEntity($form, $form_state);
280 | $form_state->setRebuild();
281 | }
282 |
283 | }
284 |
--------------------------------------------------------------------------------
/src/MessengerInterface.php:
--------------------------------------------------------------------------------
1 | configFactory = $config_factory;
118 | $this->moduleHandler = $module_handler;
119 | $this->token = $token;
120 | $this->aliasCleaner = $alias_cleaner;
121 | $this->aliasStorageHelper = $alias_storage_helper;
122 | $this->aliasUniquifier = $alias_uniquifier;
123 | $this->messenger = $messenger;
124 | $this->stringTranslation = $string_translation;
125 | $this->tokenEntityMapper = $token_entity_mappper;
126 | }
127 |
128 | /**
129 | * {@inheritdoc}
130 | */
131 | public function createEntityAlias(EntityInterface $entity, $op) {
132 | // Retrieve and apply the pattern for this content type.
133 | $pattern = $this->getPatternByEntity($entity);
134 | if (empty($pattern)) {
135 | // No pattern? Do nothing (otherwise we may blow away existing aliases...)
136 | return NULL;
137 | }
138 |
139 | $source = '/' . $entity->toUrl()->getInternalPath();
140 | $config = $this->configFactory->get('pathauto.settings');
141 | $langcode = $entity->language()->getId();
142 |
143 | // Build token data.
144 | $data = [
145 | $this->tokenEntityMapper->getTokenTypeForEntityType($entity->getEntityTypeId()) => $entity,
146 | ];
147 |
148 | // Allow other modules to alter the pattern.
149 | $context = array(
150 | 'module' => $entity->getEntityType()->getProvider(),
151 | 'op' => $op,
152 | 'source' => $source,
153 | 'data' => $data,
154 | 'bundle' => $entity->bundle(),
155 | 'language' => &$langcode,
156 | );
157 | // @todo Is still hook still useful?
158 | $this->moduleHandler->alter('pathauto_pattern', $pattern, $context);
159 |
160 | // Special handling when updating an item which is already aliased.
161 | $existing_alias = NULL;
162 | if ($op == 'update' || $op == 'bulkupdate') {
163 | if ($existing_alias = $this->aliasStorageHelper->loadBySource($source, $langcode)) {
164 | switch ($config->get('update_action')) {
165 | case PathautoGeneratorInterface::UPDATE_ACTION_NO_NEW:
166 | // If an alias already exists,
167 | // and the update action is set to do nothing,
168 | // then gosh-darn it, do nothing.
169 | return NULL;
170 | }
171 | }
172 | }
173 |
174 | // Replace any tokens in the pattern.
175 | // Uses callback option to clean replacements. No sanitization.
176 | // Pass empty BubbleableMetadata object to explicitly ignore cacheablity,
177 | // as the result is never rendered.
178 | $alias = $this->token->replace($pattern->getPattern(), $data, array(
179 | 'clear' => TRUE,
180 | 'callback' => array($this->aliasCleaner, 'cleanTokenValues'),
181 | 'langcode' => $langcode,
182 | 'pathauto' => TRUE,
183 | ), new BubbleableMetadata());
184 |
185 | // Check if the token replacement has not actually replaced any values. If
186 | // that is the case, then stop because we should not generate an alias.
187 | // @see token_scan()
188 | $pattern_tokens_removed = preg_replace('/\[[^\s\]:]*:[^\s\]]*\]/', '', $pattern->getPattern());
189 | if ($alias === $pattern_tokens_removed) {
190 | return NULL;
191 | }
192 |
193 | $alias = $this->aliasCleaner->cleanAlias($alias);
194 |
195 | // Allow other modules to alter the alias.
196 | $context['source'] = &$source;
197 | $context['pattern'] = $pattern;
198 | $this->moduleHandler->alter('pathauto_alias', $alias, $context);
199 |
200 | // If we have arrived at an empty string, discontinue.
201 | if (!Unicode::strlen($alias)) {
202 | return NULL;
203 | }
204 |
205 | // If the alias already exists, generate a new, hopefully unique, variant.
206 | $original_alias = $alias;
207 | $this->aliasUniquifier->uniquify($alias, $source, $langcode);
208 | if ($original_alias != $alias) {
209 | // Alert the user why this happened.
210 | $this->messenger->addMessage($this->t('The automatically generated alias %original_alias conflicted with an existing alias. Alias changed to %alias.', array(
211 | '%original_alias' => $original_alias,
212 | '%alias' => $alias,
213 | )), $op);
214 | }
215 |
216 | // Return the generated alias if requested.
217 | if ($op == 'return') {
218 | return $alias;
219 | }
220 |
221 | // Build the new path alias array and send it off to be created.
222 | $path = array(
223 | 'source' => $source,
224 | 'alias' => $alias,
225 | 'language' => $langcode,
226 | );
227 |
228 | return $this->aliasStorageHelper->save($path, $existing_alias, $op);
229 | }
230 |
231 | /**
232 | * Loads pathauto patterns for a given entity type ID
233 | *
234 | * @param string $entity_type_id
235 | * An entity type ID.
236 | *
237 | * @return \Drupal\pathauto\PathautoPatternInterface[]
238 | * A list of patterns, sorted by weight.
239 | */
240 | protected function getPatternByEntityType($entity_type_id) {
241 | if (!isset($this->patternsByEntityType[$entity_type_id])) {
242 | $ids = \Drupal::entityQuery('pathauto_pattern')
243 | ->condition('type', array_keys(\Drupal::service('plugin.manager.alias_type')
244 | ->getPluginDefinitionByType($this->tokenEntityMapper->getTokenTypeForEntityType($entity_type_id))))
245 | ->sort('weight')
246 | ->execute();
247 |
248 | $this->patternsByEntityType[$entity_type_id] = \Drupal::entityTypeManager()
249 | ->getStorage('pathauto_pattern')
250 | ->loadMultiple($ids);
251 | }
252 |
253 | return $this->patternsByEntityType[$entity_type_id];
254 | }
255 |
256 | /**
257 | * {@inheritdoc}
258 | */
259 | public function getPatternByEntity(EntityInterface $entity) {
260 | $langcode = $entity->language()->getId();
261 | if (!isset($this->patterns[$entity->getEntityTypeId()][$entity->id()][$langcode])) {
262 | foreach ($this->getPatternByEntityType($entity->getEntityTypeId()) as $pattern) {
263 | if ($pattern->applies($entity)) {
264 | $this->patterns[$entity->getEntityTypeId()][$entity->id()][$langcode] = $pattern;
265 | break;
266 | }
267 | }
268 | // If still not set.
269 | if (!isset($this->patterns[$entity->getEntityTypeId()][$entity->id()][$langcode])) {
270 | $this->patterns[$entity->getEntityTypeId()][$entity->id()][$langcode] = NULL;
271 | }
272 | }
273 | return $this->patterns[$entity->getEntityTypeId()][$entity->id()][$langcode];
274 | }
275 |
276 | /**
277 | * {@inheritdoc}
278 | */
279 | public function resetCaches() {
280 | $this->patterns = [];
281 | $this->patternsByEntityType = [];
282 | $this->aliasCleaner->resetCaches();
283 | }
284 |
285 | /**
286 | * {@inheritdoc}
287 | */
288 | public function updateEntityAlias(EntityInterface $entity, $op, array $options = array()) {
289 | // Skip if the entity does not have the path field.
290 | if (!($entity instanceof ContentEntityInterface) || !$entity->hasField('path')) {
291 | return NULL;
292 | }
293 |
294 | // Skip if pathauto processing is disabled.
295 | if ($entity->path->pathauto != PathautoState::CREATE && empty($options['force'])) {
296 | return NULL;
297 | }
298 |
299 | $options += array('language' => $entity->language()->getId());
300 | $type = $entity->getEntityTypeId();
301 |
302 | // Skip processing if the entity has no pattern.
303 | if (!$this->getPatternByEntity($entity)) {
304 | return NULL;
305 | }
306 |
307 | // Deal with taxonomy specific logic.
308 | // @todo Update and test forum related code.
309 | if ($type == 'taxonomy_term') {
310 |
311 | $config_forum = $this->configFactory->get('forum.settings');
312 | if ($entity->getVocabularyId() == $config_forum->get('vocabulary')) {
313 | $type = 'forum';
314 | }
315 | }
316 |
317 | $result = $this->createEntityAlias($entity, $op);
318 |
319 | // @todo Move this to a method on the pattern plugin.
320 | if ($type == 'taxonomy_term') {
321 | foreach ($this->loadTermChildren($entity->id()) as $subterm) {
322 | $this->updateEntityAlias($subterm, $op, $options);
323 | }
324 | }
325 |
326 | return $result;
327 | }
328 |
329 | /**
330 | * Finds all children of a term ID.
331 | *
332 | * @param int $tid
333 | * Term ID to retrieve parents for.
334 | *
335 | * @return \Drupal\taxonomy\TermInterface[]
336 | * An array of term objects that are the children of the term $tid.
337 | */
338 | protected function loadTermChildren($tid) {
339 | return \Drupal::entityManager()->getStorage('taxonomy_term')->loadChildren($tid);
340 | }
341 |
342 | }
343 |
--------------------------------------------------------------------------------
/src/PathautoGeneratorInterface.php:
--------------------------------------------------------------------------------
1 | setLabel(t('Pathauto state'))
26 | ->setDescription(t('Whether an automated alias should be created or not.'))
27 | ->setComputed(TRUE)
28 | ->setClass('\Drupal\pathauto\PathautoState');
29 | return $properties;
30 | }
31 |
32 | /**
33 | * {@inheritdoc}
34 | */
35 | public function postSave($update) {
36 | // Only allow the parent implementation to act if pathauto will not create
37 | // an alias.
38 | if ($this->pathauto == PathautoState::SKIP) {
39 | parent::postSave($update);
40 | }
41 | $this->get('pathauto')->persist();
42 | }
43 |
44 | /**
45 | * {@inheritdoc}
46 | */
47 | public function isEmpty() {
48 | // Make sure that the pathauto state flag does not get lost if just that is
49 | // changed.
50 | return !$this->alias && !$this->get('pathauto')->hasValue();
51 | }
52 |
53 | /**
54 | * {@inheritdoc}
55 | */
56 | public function applyDefaultValue($notify = TRUE) {
57 | parent::applyDefaultValue($notify);
58 | // Created fields default creating a new alias.
59 | $this->setValue(array('pathauto' => PathautoState::CREATE), $notify);
60 | return $this;
61 | }
62 |
63 |
64 | }
65 |
--------------------------------------------------------------------------------
/src/PathautoPatternInterface.php:
--------------------------------------------------------------------------------
1 | t('Label');
30 | $header['pattern'] = $this->t('Pattern');
31 | $header['type'] = $this->t('Pattern type');
32 | $header['conditions'] = $this->t('Conditions');
33 | return $header + parent::buildHeader();
34 | }
35 |
36 | /**
37 | * {@inheritdoc}
38 | */
39 | public function buildRow(EntityInterface $entity) {
40 | /* @var \Drupal\pathauto\PathautoPatternInterface $entity */
41 | $row['label'] = $entity->label();
42 | $row['patern']['#markup'] = $entity->getPattern();
43 | $row['type']['#markup'] = $entity->getAliasType()->getLabel();
44 | $row['conditions']['#theme'] = 'item_list';
45 | foreach ($entity->getSelectionConditions() as $condition) {
46 | $row['conditions']['#items'][] = $condition->summary();
47 | }
48 | return $row + parent::buildRow($entity);
49 | }
50 |
51 | }
52 |
--------------------------------------------------------------------------------
/src/PathautoState.php:
--------------------------------------------------------------------------------
1 | value === NULL) {
42 | $entity = $this->parent->getEntity();
43 |
44 | // @todo: Investigate why this happens.
45 | if ($entity->isNew()) {
46 | $this->value = static::CREATE;
47 | return $this->value;
48 | }
49 |
50 | // If no value has been set or loaded yet, try to load a value if this
51 | // entity has already been saved.
52 | $this->value = \Drupal::keyValue($this->getCollection())
53 | ->get($this->parent->getEntity()->id());
54 | // If it was not yet saved or no value was found, try to detect based on
55 | // an existing alias if the entity is not new.
56 | if ($this->value === NULL) {
57 | $entity_path = '/' . $entity->toUrl()->getInternalPath();
58 | $path = \Drupal::service('path.alias_manager')
59 | ->getAliasByPath(
60 | $entity_path, $entity->language()->getId()
61 | );
62 | $pathauto_alias = \Drupal::service('pathauto.generator')
63 | ->createEntityAlias($entity, 'return');
64 | if (($path != $entity_path && $path == $pathauto_alias)) {
65 | $this->value = static::CREATE;
66 | }
67 | else {
68 | $this->value = static::SKIP;
69 | }
70 | }
71 | }
72 | return $this->value;
73 | }
74 |
75 | /**
76 | * {@inheritdoc}
77 | */
78 | public function setValue($value, $notify = TRUE) {
79 | $this->value = $value;
80 | // Notify the parent of any changes.
81 | if ($notify && isset($this->parent)) {
82 | $this->parent->onChange($this->name);
83 | }
84 | }
85 |
86 | /**
87 | * Returns TRUE if a value was set.
88 | */
89 | public function hasValue() {
90 | return $this->value !== NULL;
91 | }
92 |
93 | /**
94 | * Persists the state.
95 | */
96 | public function persist() {
97 | \Drupal::keyValue($this->getCollection())->set(
98 | $this->parent->getEntity()
99 | ->id(), $this->value
100 | );
101 | }
102 |
103 | /**
104 | * Deletes the stored state.
105 | */
106 | public function purge() {
107 | \Drupal::keyValue($this->getCollection())
108 | ->delete($this->parent->getEntity()->id());
109 | }
110 |
111 | /**
112 | * Returns the key value collection that should be used for the given entity.
113 | * @return string
114 | */
115 | protected function getCollection() {
116 | return 'pathauto_state.' . $this->parent->getEntity()->getEntityTypeId();
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/src/PathautoWidget.php:
--------------------------------------------------------------------------------
1 | getEntity();
24 |
25 | // Taxonomy terms do not have an actual fieldset for path settings.
26 | // Merge in the defaults.
27 | // @todo Impossible to do this in widget, use another solution
28 | /*
29 | $form['path'] += array(
30 | '#type' => 'fieldset',
31 | '#title' => $this->t('URL path settings'),
32 | '#collapsible' => TRUE,
33 | '#collapsed' => empty($form['path']['alias']),
34 | '#group' => 'additional_settings',
35 | '#attributes' => array(
36 | 'class' => array('path-form'),
37 | ),
38 | '#access' => \Drupal::currentUser()->hasPermission('create url aliases') || \Drupal::currentUser()->hasPermission('administer url aliases'),
39 | '#weight' => 30,
40 | '#tree' => TRUE,
41 | '#element_validate' => array('path_form_element_validate'),
42 | );*/
43 |
44 |
45 |
46 | $pattern = \Drupal::service('pathauto.generator')->getPatternByEntity($entity);
47 | if (empty($pattern)) {
48 | return $element;
49 | }
50 |
51 | $element['pathauto'] = array(
52 | '#type' => 'checkbox',
53 | '#title' => $this->t('Generate automatic URL alias'),
54 | '#default_value' => $entity->path->pathauto,
55 | '#description' => $this->t('Uncheck this to create a custom alias below. Configure URL alias patterns.', array('@admin_link' => \Drupal::url('entity.pathauto_pattern.collection'))),
56 | '#weight' => -1,
57 | );
58 |
59 | // Add JavaScript that will disable the path textfield when the automatic
60 | // alias checkbox is checked.
61 | $element['alias']['#states']['!enabled']['input[name="path[pathauto]"]'] = array('checked' => TRUE);
62 |
63 |
64 | // Override path.module's vertical tabs summary.
65 | $element['alias']['#attached']['library'] = ['pathauto/widget'];
66 |
67 | if ($entity->path->pathauto == PathautoState::CREATE && !empty($entity->path->old_alias) && empty($entity->path->alias)) {
68 | $element['alias']['#default_value'] = $entity->path->old_alias;
69 | $entity->path->alias = $entity->path->old_alias;
70 | }
71 |
72 |
73 | // For Pathauto to remember the old alias and prevent the Path module from
74 | // deleting it when Pathauto wants to preserve it.
75 | if (!empty($entity->path->alias)) {
76 | $element['old_alias'] = array(
77 | '#type' => 'value',
78 | '#value' => $entity->path->alias,
79 | );
80 | }
81 |
82 | return $element;
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/Plugin/Action/UpdateAction.php:
--------------------------------------------------------------------------------
1 | path->pathauto = PathautoState::CREATE;
29 | \Drupal::service('pathauto.generator')->updateEntityAlias($entity, 'bulkupdate', array('message' => TRUE));
30 | }
31 |
32 | /**
33 | * {@inheritdoc}
34 | */
35 | public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
36 | $result = AccessResult::allowedIfHasPermission($account, 'create url aliases');
37 | return $return_as_object ? $result : $result->isAllowed();
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/Plugin/Deriver/EntityAliasTypeDeriver.php:
--------------------------------------------------------------------------------
1 | entityTypeManager = $entity_type_manager;
56 | $this->entityFieldManager = $entity_field_manager;
57 | $this->stringTranslation = $string_translation;
58 | $this->tokenEntityMapper = $token_entity_mapper;
59 | }
60 |
61 | /**
62 | * {@inheritdoc}
63 | */
64 | public static function create(ContainerInterface $container, $base_plugin_id) {
65 | return new static(
66 | $container->get('entity_type.manager'),
67 | $container->get('entity_field.manager'),
68 | $container->get('string_translation'),
69 | $container->get('token.entity_mapper')
70 | );
71 | }
72 |
73 | /**
74 | * {@inheritdoc}
75 | */
76 | public function getDerivativeDefinitions($base_plugin_definition) {
77 | foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) {
78 | // An entity type must have a canonical link template and support fields.
79 | if ($entity_type->hasLinkTemplate('canonical') && is_subclass_of($entity_type->getClass(), FieldableEntityInterface::class)) {
80 | $base_fields = $this->entityFieldManager->getBaseFieldDefinitions($entity_type_id);
81 | if (!isset($base_fields['path'])) {
82 | // The entity type does not have a path field and is therefore not
83 | // supported.
84 | // @todo: Add a UI to enable that base field on any content entity.
85 | continue;
86 | }
87 | $this->derivatives[$entity_type_id] = $base_plugin_definition;
88 | $this->derivatives[$entity_type_id]['label'] = $entity_type->getLabel();
89 | $this->derivatives[$entity_type_id]['types'] = [$this->tokenEntityMapper->getTokenTypeForEntityType($entity_type_id)];
90 | $this->derivatives[$entity_type_id]['provider'] = $entity_type->getProvider();
91 | $this->derivatives[$entity_type_id]['context'] = [
92 | $entity_type_id => new ContextDefinition("entity:$entity_type_id", $this->t('@label being aliased', ['@label' => $entity_type->getLabel()]))
93 | ];
94 | }
95 | }
96 | return $this->derivatives;
97 | }
98 |
99 | }
100 |
--------------------------------------------------------------------------------
/src/Plugin/pathauto/AliasType/EntityAliasTypeBase.php:
--------------------------------------------------------------------------------
1 | moduleHandler = $module_handler;
77 | $this->languageManager = $language_manager;
78 | $this->entityTypeManager = $entity_type_manager;
79 | }
80 |
81 | /**
82 | * {@inheritdoc}
83 | */
84 | public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
85 | return new static(
86 | $configuration,
87 | $plugin_id,
88 | $plugin_definition,
89 | $container->get('module_handler'),
90 | $container->get('language_manager'),
91 | $container->get('entity_type.manager')
92 | );
93 | }
94 |
95 | /**
96 | * {@inheritdoc}
97 | */
98 | public function getLabel() {
99 | $definition = $this->getPluginDefinition();
100 | // Cast the admin label to a string since it is an object.
101 | // @see \Drupal\Core\StringTranslation\TranslationWrapper
102 | return (string) $definition['label'];
103 | }
104 |
105 | /**
106 | * {@inheritdoc}
107 | */
108 | public function getTokenTypes() {
109 | $definition = $this->getPluginDefinition();
110 | return $definition['types'];
111 | }
112 |
113 | /**
114 | * {@inheritdoc}
115 | */
116 | public function batchUpdate(&$context) {
117 | if (!isset($context['sandbox']['current'])) {
118 | $context['sandbox']['count'] = 0;
119 | $context['sandbox']['current'] = 0;
120 | }
121 |
122 | $entity_type = $this->entityTypeManager->getDefinition($this->getEntityTypeId());
123 | $id_key = $entity_type->getKey('id');
124 |
125 | $query = db_select($entity_type->get('base_table'), 'base_table');
126 | $query->leftJoin('url_alias', 'ua', "CONCAT('" . $this->getSourcePrefix() . "' , base_table.$id_key) = ua.source");
127 | $query->addField('base_table', $id_key, 'id');
128 | $query->isNull('ua.source');
129 | $query->condition('base_table.' . $id_key, $context['sandbox']['current'], '>');
130 | $query->orderBy('base_table.' . $id_key);
131 | $query->addTag('pathauto_bulk_update');
132 | $query->addMetaData('entity', $this->getEntityTypeId());
133 |
134 | // Get the total amount of items to process.
135 | if (!isset($context['sandbox']['total'])) {
136 | $context['sandbox']['total'] = $query->countQuery()->execute()->fetchField();
137 |
138 | // If there are no entities to update, then stop immediately.
139 | if (!$context['sandbox']['total']) {
140 | $context['finished'] = 1;
141 | return;
142 | }
143 | }
144 |
145 | $query->range(0, 25);
146 | $ids = $query->execute()->fetchCol();
147 |
148 | $this->bulkUpdate($ids);
149 | $context['sandbox']['count'] += count($ids);
150 | $context['sandbox']['current'] = max($ids);
151 | $context['message'] = t('Updated alias for %label @id.', array('%label' => $entity_type->getLabel(), '@id' => end($ids)));
152 |
153 | if ($context['sandbox']['count'] != $context['sandbox']['total']) {
154 | $context['finished'] = $context['sandbox']['count'] / $context['sandbox']['total'];
155 | }
156 | }
157 |
158 | /**
159 | * Returns the entity type ID.
160 | *
161 | * @return string
162 | * The entity type ID.
163 | */
164 | protected function getEntityTypeId() {
165 | return $this->getDerivativeId();
166 | }
167 |
168 | /**
169 | * Update the URL aliases for multiple entities.
170 | *
171 | * @param array $ids
172 | * An array of entity IDs IDs.
173 | * @param array $options
174 | * An optional array of additional options.
175 | */
176 | protected function bulkUpdate(array $ids, array $options = array()) {
177 | $options += array('message' => FALSE);
178 |
179 | $entities = $this->entityTypeManager->getStorage($this->getEntityTypeId())->loadMultiple($ids);
180 | foreach ($entities as $entity) {
181 | // Update aliases for the entity's default language and its translations.
182 | foreach ($entity->getTranslationLanguages() as $langcode => $language) {
183 | $translated_entity = $entity->getTranslation($langcode);
184 | \Drupal::service('pathauto.generator')->updateEntityAlias($translated_entity, 'bulkupdate', $options);
185 | }
186 | }
187 |
188 | if (!empty($options['message'])) {
189 | drupal_set_message(\Drupal::translation()->formatPlural(count($ids), 'Updated 1 %label URL alias.', 'Updated @count %label URL aliases.'), array('%label' => $this->getLabel()));
190 | }
191 | }
192 |
193 | /**
194 | * {@inheritdoc}
195 | */
196 | public function calculateDependencies() {
197 | $dependencies = [];
198 | $dependencies['module'][] = $this->entityTypeManager->getDefinition($this->getEntityTypeId())->getProvider();
199 | return $dependencies;
200 | }
201 |
202 | /**
203 | * {@inheritdoc}
204 | */
205 | public function applies($object) {
206 | return $object instanceof FieldableEntityInterface && $object->getEntityTypeId() == $this->getEntityTypeId();
207 | }
208 |
209 | /**
210 | * {@inheritdoc}
211 | */
212 | public function getSourcePrefix() {
213 | if (empty($this->prefix)) {
214 | $entity_type = $this->entityTypeManager->getDefinition($this->getEntityTypeId());
215 | $path = $entity_type->getLinkTemplate('canonical');
216 | $this->prefix = substr($path, 0, strpos($path, '{'));
217 | }
218 | return $this->prefix;
219 | }
220 |
221 | }
222 |
--------------------------------------------------------------------------------
/src/Plugin/pathauto/AliasType/ForumAliasType.php:
--------------------------------------------------------------------------------
1 | configFactory->get('forum.settings');
46 | return $object->getVocabularyId() == $config_forum->get('vocabulary');
47 | }
48 | return FALSE;
49 | }
50 |
51 | }
52 |
--------------------------------------------------------------------------------
/src/Tests/PathautoBulkUpdateTest.php:
--------------------------------------------------------------------------------
1 | adminUser = $this->drupalCreateUser($permissions);
56 | $this->drupalLogin($this->adminUser);
57 |
58 | $this->createPattern('node', '/content/[node:title]');
59 | $this->createPattern('user', '/users/[user:name]');
60 | }
61 |
62 | function testBulkUpdate() {
63 | // Create some nodes.
64 | $this->nodes = array();
65 | for ($i = 1; $i <= 5; $i++) {
66 | $node = $this->drupalCreateNode();
67 | $this->nodes[$node->id()] = $node;
68 | }
69 |
70 | // Clear out all aliases.
71 | $this->deleteAllAliases();
72 |
73 | // Bulk create aliases.
74 | $edit = array(
75 | 'update[canonical_entities:node]' => TRUE,
76 | 'update[canonical_entities:user]' => TRUE,
77 | );
78 | $this->drupalPostForm('admin/config/search/path/update_bulk', $edit, t('Update'));
79 |
80 | // This has generated 6 aliases. 5 nodes and one user that we created. There
81 | // is also UID 1 but that user was created before the path field existed,
82 | // so he does not have a pathauto state.
83 | $this->assertText('Generated 6 URL aliases.');
84 |
85 | // Check that aliases have actually been created.
86 | foreach ($this->nodes as $node) {
87 | $this->assertEntityAliasExists($node);
88 | }
89 | $this->assertEntityAliasExists($this->adminUser);
90 |
91 | // Add a new node.
92 | $new_node = $this->drupalCreateNode(array('path' => array('alias' => '', 'pathauto' => PathautoState::SKIP)));
93 |
94 | // Run the update again which should not run against any nodes.
95 | $this->drupalPostForm('admin/config/search/path/update_bulk', $edit, t('Update'));
96 | $this->assertText('No new URL aliases to generate.');
97 |
98 | $this->assertNoEntityAliasExists($new_node);
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/src/Tests/PathautoLocaleTest.php:
--------------------------------------------------------------------------------
1 | drupalCreateContentType(array('type' => 'article', 'name' => 'Article'));
40 | }
41 |
42 | /**
43 | * Test that when an English node is updated, its old English alias is
44 | * updated and its newer French alias is left intact.
45 | */
46 | function testLanguageAliases() {
47 |
48 | $this->createPattern('node', '/content/[node:title]');
49 |
50 | // Add predefined French language.
51 | ConfigurableLanguage::createFromLangcode('fr')->save();
52 |
53 | $node = array(
54 | 'title' => 'English node',
55 | 'langcode' => 'en',
56 | 'path' => array(array(
57 | 'alias' => '/english-node',
58 | 'pathauto' => FALSE,
59 | )),
60 | );
61 | $node = $this->drupalCreateNode($node);
62 | $english_alias = \Drupal::service('path.alias_storage')->load(array('alias' => '/english-node', 'langcode' => 'en'));
63 | $this->assertTrue($english_alias, 'Alias created with proper language.');
64 |
65 | // Also save a French alias that should not be left alone, even though
66 | // it is the newer alias.
67 | $this->saveEntityAlias($node, '/french-node', 'fr');
68 |
69 | // Add an alias with the soon-to-be generated alias, causing the upcoming
70 | // alias update to generate a unique alias with the '-0' suffix.
71 | $this->saveAlias('/node/invalid', '/content/english-node', Language::LANGCODE_NOT_SPECIFIED);
72 |
73 | // Update the node, triggering a change in the English alias.
74 | $node->path->pathauto = PathautoState::CREATE;
75 | $node->save();
76 |
77 | // Check that the new English alias replaced the old one.
78 | $this->assertEntityAlias($node, '/content/english-node-0', 'en');
79 | $this->assertEntityAlias($node, '/french-node', 'fr');
80 | $this->assertAliasExists(array('pid' => $english_alias['pid'], 'alias' => '/content/english-node-0'));
81 |
82 | // Create a new node with the same title as before but without
83 | // specifying a language.
84 | $node = $this->drupalCreateNode(array('title' => 'English node', 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED));
85 |
86 | // Check that the new node had a unique alias generated with the '-1'
87 | // suffix.
88 | $this->assertEntityAlias($node, '/content/english-node-1', LanguageInterface::LANGCODE_NOT_SPECIFIED);
89 | }
90 |
91 | /**
92 | * Test that patterns work on multilingual content.
93 | */
94 | function testLanguagePatterns() {
95 | $this->drupalLogin($this->rootUser);
96 |
97 | // Add French language.
98 | $edit = array(
99 | 'predefined_langcode' => 'fr',
100 | );
101 | $this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add language'));
102 |
103 | // Enable content translation on articles.
104 | $this->drupalGet('admin/config/regional/content-language');
105 | $edit = array(
106 | 'entity_types[node]' => TRUE,
107 | 'settings[node][article][translatable]' => TRUE,
108 | 'settings[node][article][settings][language][language_alterable]' => TRUE,
109 | );
110 | $this->drupalPostForm(NULL, $edit, t('Save configuration'));
111 |
112 | // Create a pattern for English articles.
113 | $this->drupalGet('admin/config/search/path/patterns/add');
114 | $edit = array(
115 | 'type' => 'canonical_entities:node',
116 | );
117 | $this->drupalPostAjaxForm(NULL, $edit, 'type');
118 | $edit += array(
119 | 'pattern' => '/the-articles/[node:title]',
120 | 'label' => 'English articles',
121 | 'id' => 'english_articles',
122 | 'bundles[article]' => TRUE,
123 | 'languages[en]' => TRUE,
124 | );
125 | $this->drupalPostForm(NULL, $edit, 'Save');
126 | $this->assertText('Pattern English articles saved.');
127 |
128 | // Create a pattern for French articles.
129 | $this->drupalGet('admin/config/search/path/patterns/add');
130 | $edit = array(
131 | 'type' => 'canonical_entities:node',
132 | );
133 | $this->drupalPostAjaxForm(NULL, $edit, 'type');
134 | $edit += array(
135 | 'pattern' => '/les-articles/[node:title]',
136 | 'label' => 'French articles',
137 | 'id' => 'french_articles',
138 | 'bundles[article]' => TRUE,
139 | 'languages[fr]' => TRUE,
140 | );
141 | $this->drupalPostForm(NULL, $edit, 'Save');
142 | $this->assertText('Pattern French articles saved.');
143 |
144 | // Create a node and its translation. Assert aliases.
145 | $edit = array(
146 | 'title[0][value]' => 'English node',
147 | 'langcode[0][value]' => 'en',
148 | );
149 | $this->drupalPostForm('node/add/article', $edit, t('Save and publish'));
150 | $english_node = $this->drupalGetNodeByTitle('English node');
151 | $this->assertAlias('/node/' . $english_node->id(), '/the-articles/english-node', 'en');
152 |
153 | $this->drupalGet('node/' . $english_node->id() . '/translations');
154 | $this->clickLink(t('Add'));
155 | $edit = array(
156 | 'title[0][value]' => 'French node',
157 | );
158 | $this->drupalPostForm(NULL, $edit, t('Save and keep published (this translation)'));
159 | $this->rebuildContainer();
160 | $english_node = $this->drupalGetNodeByTitle('English node');
161 | $french_node = $english_node->getTranslation('fr');
162 | $this->assertAlias('/node/' . $french_node->id(), '/les-articles/french-node', 'fr');
163 |
164 | // Bulk delete and Bulk generate patterns. Assert aliases.
165 | $this->deleteAllAliases();
166 | // Bulk create aliases.
167 | $edit = array(
168 | 'update[canonical_entities:node]' => TRUE,
169 | );
170 | $this->drupalPostForm('admin/config/search/path/update_bulk', $edit, t('Update'));
171 | $this->assertText(t('Generated 2 URL aliases.'));
172 | $this->assertAlias('/node/' . $english_node->id(), '/the-articles/english-node', 'en');
173 | $this->assertAlias('/node/' . $french_node->id(), '/les-articles/french-node', 'fr');
174 | }
175 |
176 | }
177 |
--------------------------------------------------------------------------------
/src/Tests/PathautoMassDeleteTest.php:
--------------------------------------------------------------------------------
1 | adminUser = $this->drupalCreateUser($permissions);
69 | $this->drupalLogin($this->adminUser);
70 |
71 | $this->createPattern('node', '/content/[node:title]');
72 | $this->createPattern('user', '/users/[user:name]');
73 | $this->createPattern('taxonomy_term', '/[term:vocabulary]/[term:name]');
74 | }
75 |
76 | /**
77 | * Tests the deletion of all the aliases.
78 | */
79 | function testDeleteAll() {
80 | // 1. Test that deleting all the aliases, of any type, works.
81 | $this->generateAliases();
82 | $edit = array(
83 | 'delete[all_aliases]' => TRUE,
84 | );
85 | $this->drupalPostForm('admin/config/search/path/delete_bulk', $edit, t('Delete aliases now!'));
86 | $this->assertText(t('All of your path aliases have been deleted.'));
87 | $this->assertUrl(\Drupal::url('pathauto.admin.delete'));
88 |
89 | // Make sure that all of them are actually deleted.
90 | $aliases = db_select('url_alias', 'ua')->fields('ua', array())->execute()->fetchAll();
91 | $this->assertEqual($aliases, array(), "All the aliases have been deleted.");
92 |
93 | // 2. Test deleting only specific (entity type) aliases.
94 | $manager = $this->container->get('plugin.manager.alias_type');
95 | $pathauto_plugins = array('canonical_entities:node' => 'nodes', 'canonical_entities:taxonomy_term' => 'terms', 'canonical_entities:user' => 'accounts');
96 | foreach ($pathauto_plugins as $pathauto_plugin => $attribute) {
97 | $this->generateAliases();
98 | $edit = array(
99 | 'delete[plugins][' . $pathauto_plugin . ']' => TRUE,
100 | );
101 | $this->drupalPostForm('admin/config/search/path/delete_bulk', $edit, t('Delete aliases now!'));
102 | $alias_type = $manager->createInstance($pathauto_plugin);
103 | $this->assertRaw(t('All of your %label path aliases have been deleted.', array('%label' => $alias_type->getLabel())));
104 | // Check that the aliases were actually deleted.
105 | foreach ($this->{$attribute} as $entity) {
106 | $this->assertNoEntityAlias($entity);
107 | }
108 |
109 | // Check that the other aliases are not deleted.
110 | foreach ($pathauto_plugins as $_pathauto_plugin => $_attribute) {
111 | // Skip the aliases that should be deleted.
112 | if ($_pathauto_plugin == $pathauto_plugin) {
113 | continue;
114 | }
115 | foreach ($this->{$_attribute} as $entity) {
116 | $this->assertEntityAliasExists($entity);
117 | }
118 | }
119 | }
120 | }
121 |
122 | /**
123 | * Helper function to generate aliases.
124 | */
125 | function generateAliases() {
126 | // We generate a bunch of aliases for nodes, users and taxonomy terms. If
127 | // the entities are already created we just update them, otherwise we create
128 | // them.
129 | if (empty($this->nodes)) {
130 | for ($i = 1; $i <= 5; $i++) {
131 | $node = $this->drupalCreateNode();
132 | $this->nodes[$node->id()] = $node;
133 | }
134 | }
135 | else {
136 | foreach ($this->nodes as $node) {
137 | $node->save();
138 | }
139 | }
140 |
141 | if (empty($this->accounts)) {
142 | for ($i = 1; $i <= 5; $i++) {
143 | $account = $this->drupalCreateUser();
144 | $this->accounts[$account->id()] = $account;
145 | }
146 | }
147 | else {
148 | foreach ($this->accounts as $id => $account) {
149 | $account->save();
150 | }
151 | }
152 |
153 | if (empty($this->terms)) {
154 | $vocabulary = $this->addVocabulary(array('name' => 'test vocabulary', 'vid' => 'test_vocabulary'));
155 | for ($i = 1; $i <= 5; $i++) {
156 | $term = $this->addTerm($vocabulary);
157 | $this->terms[$term->id()] = $term;
158 | }
159 | }
160 | else {
161 | foreach ($this->terms as $term) {
162 | $term->save();
163 | }
164 | }
165 |
166 | // Check that we have aliases for the entities.
167 | foreach (array('nodes', 'accounts', 'terms') as $attribute) {
168 | foreach ($this->{$attribute} as $entity) {
169 | $this->assertEntityAliasExists($entity);
170 | }
171 | }
172 | }
173 |
174 | }
175 |
--------------------------------------------------------------------------------
/src/Tests/PathautoNodeWebTest.php:
--------------------------------------------------------------------------------
1 | drupalCreateContentType(array('type' => 'page', 'name' => 'Basic page'));
44 | $this->drupalCreateContentType(array('type' => 'article'));
45 |
46 | // Allow other modules to add additional permissions for the admin user.
47 | $permissions = array(
48 | 'administer pathauto',
49 | 'administer url aliases',
50 | 'create url aliases',
51 | 'administer nodes',
52 | 'bypass node access',
53 | 'access content overview',
54 | );
55 | $this->adminUser = $this->drupalCreateUser($permissions);
56 | $this->drupalLogin($this->adminUser);
57 |
58 | $this->createPattern('node', '/content/[node:title]');
59 |
60 | }
61 |
62 | /**
63 | * Tests editing nodes with different settings.
64 | */
65 | function testNodeEditing() {
66 | // Ensure that the Pathauto checkbox is checked by default on the node add form.
67 | $this->drupalGet('node/add/page');
68 | $this->assertFieldChecked('edit-path-0-pathauto');
69 |
70 | // Create a node by saving the node form.
71 | $title = ' Testing: node title [';
72 | $automatic_alias = '/content/testing-node-title';
73 | $this->drupalPostForm(NULL, array('title[0][value]' => $title), t('Save and publish'));
74 | $node = $this->drupalGetNodeByTitle($title);
75 |
76 | // Look for alias generated in the form.
77 | $this->drupalGet("node/{$node->id()}/edit");
78 | $this->assertFieldChecked('edit-path-0-pathauto');
79 | $this->assertFieldByName('path[0][alias]', $automatic_alias, 'Generated alias visible in the path alias field.');
80 |
81 | // Check whether the alias actually works.
82 | $this->drupalGet($automatic_alias);
83 | $this->assertText($title, 'Node accessible through automatic alias.');
84 |
85 | // Manually set the node's alias.
86 | $manual_alias = '/content/' . $node->id();
87 | $edit = array(
88 | 'path[0][pathauto]' => FALSE,
89 | 'path[0][alias]' => $manual_alias,
90 | );
91 | $this->drupalPostForm($node->urlInfo('edit-form'), $edit, t('Save and keep published'));
92 | $this->assertRaw(t('@type %title has been updated.', array('@type' => 'page', '%title' => $title)));
93 |
94 | // Check that the automatic alias checkbox is now unchecked by default.
95 | $this->drupalGet("node/{$node->id()}/edit");
96 | $this->assertNoFieldChecked('edit-path-0-pathauto');
97 | $this->assertFieldByName('path[0][alias]', $manual_alias);
98 |
99 | // Submit the node form with the default values.
100 | $this->drupalPostForm(NULL, array('path[0][pathauto]' => FALSE), t('Save and keep published'));
101 | $this->assertRaw(t('@type %title has been updated.', array('@type' => 'page', '%title' => $title)));
102 |
103 | // Test that the old (automatic) alias has been deleted and only accessible
104 | // through the new (manual) alias.
105 | $this->drupalGet($automatic_alias);
106 | $this->assertResponse(404, 'Node not accessible through automatic alias.');
107 | $this->drupalGet($manual_alias);
108 | $this->assertText($title, 'Node accessible through manual alias.');
109 |
110 | // Test that the manual alias is not kept for new nodes when the pathauto
111 | // checkbox is ticked.
112 | $title = 'Automatic Title';
113 | $edit = array(
114 | 'title[0][value]' => $title,
115 | 'path[0][pathauto]' => TRUE,
116 | 'path[0][alias]' => '/should-not-get-created',
117 | );
118 | $this->drupalPostForm('node/add/page', $edit, t('Save and publish'));
119 | $this->assertNoAliasExists(array('alias' => 'should-not-get-created'));
120 | $node = $this->drupalGetNodeByTitle($title);
121 | $this->assertEntityAlias($node, '/content/automatic-title');
122 |
123 | // Remove the pattern for nodes, the pathauto checkbox should not be
124 | // displayed.
125 | $ids = \Drupal::entityQuery('pathauto_pattern')
126 | ->condition('type', 'canonical_entities:node')
127 | ->execute();
128 | foreach (PathautoPattern::loadMultiple($ids) as $pattern) {
129 | $pattern->delete();
130 | }
131 |
132 | $this->drupalGet('node/add/article');
133 | $this->assertNoFieldById('edit-path-0-pathauto');
134 | $this->assertFieldByName('path[0][alias]', '');
135 |
136 | $edit = array();
137 | $edit['title'] = 'My test article';
138 | $this->drupalCreateNode($edit);
139 | //$this->drupalPostForm(NULL, $edit, t('Save and keep published'));
140 | $node = $this->drupalGetNodeByTitle($edit['title']);
141 |
142 | // Pathauto checkbox should still not exist.
143 | $this->drupalGet($node->urlInfo('edit-form'));
144 | $this->assertNoFieldById('edit-path-0-pathauto');
145 | $this->assertFieldByName('path[0][alias]', '');
146 | $this->assertNoEntityAlias($node);
147 | }
148 |
149 | /**
150 | * Test node operations.
151 | */
152 | function testNodeOperations() {
153 | $node1 = $this->drupalCreateNode(array('title' => 'node1'));
154 | $node2 = $this->drupalCreateNode(array('title' => 'node2'));
155 |
156 | // Delete all current URL aliases.
157 | $this->deleteAllAliases();
158 |
159 | $edit = array(
160 | 'action' => 'pathauto_update_alias_node',
161 | // @todo - here we expect the $node1 to be at 0 position, any better way?
162 | 'node_bulk_form[0]' => TRUE,
163 | );
164 | $this->drupalPostForm('admin/content', $edit, t('Apply'));
165 | $this->assertRaw(\Drupal::translation()->formatPlural(1, '%action was applied to @count item.', '%action was applied to @count items.', array(
166 | '%action' => 'Update URL-Alias',
167 | )));
168 |
169 | $this->assertEntityAlias($node1, '/content/' . $node1->getTitle());
170 | $this->assertEntityAlias($node2, '/node/' . $node2->id());
171 | }
172 |
173 | /**
174 | * @todo Merge this with existing node test methods?
175 | */
176 | public function testNodeState() {
177 | $nodeNoAliasUser = $this->drupalCreateUser(array('bypass node access'));
178 | $nodeAliasUser = $this->drupalCreateUser(array('bypass node access', 'create url aliases'));
179 |
180 | $node = $this->drupalCreateNode(array(
181 | 'title' => 'Node version one',
182 | 'type' => 'page',
183 | 'path' => array(
184 | 'pathauto' => PathautoState::SKIP,
185 | ),
186 | ));
187 |
188 | $this->assertNoEntityAlias($node);
189 |
190 | // Set a manual path alias for the node.
191 | $node->path->alias = '/test-alias';
192 | $node->save();
193 |
194 | // Ensure that the pathauto field was saved to the database.
195 | \Drupal::entityManager()->getStorage('node')->resetCache();
196 | $node = Node::load($node->id());
197 | $this->assertIdentical($node->path->pathauto, PathautoState::SKIP);
198 |
199 | // Ensure that the manual path alias was saved and an automatic alias was not generated.
200 | $this->assertEntityAlias($node, '/test-alias');
201 | $this->assertNoEntityAliasExists($node, '/content/node-version-one');
202 |
203 | // Save the node as a user who does not have access to path fieldset.
204 | $this->drupalLogin($nodeNoAliasUser);
205 | $this->drupalGet('node/' . $node->id() . '/edit');
206 | $this->assertNoFieldByName('path[0][pathauto]');
207 |
208 | $edit = array('title[0][value]' => 'Node version two');
209 | $this->drupalPostForm(NULL, $edit, 'Save');
210 | $this->assertText('Basic page Node version two has been updated.');
211 |
212 | $this->assertEntityAlias($node, '/test-alias');
213 | $this->assertNoEntityAliasExists($node, '/content/node-version-one');
214 | $this->assertNoEntityAliasExists($node, '/content/node-version-two');
215 |
216 | // Load the edit node page and check that the Pathauto checkbox is unchecked.
217 | $this->drupalLogin($nodeAliasUser);
218 | $this->drupalGet('node/' . $node->id() . '/edit');
219 | $this->assertNoFieldChecked('edit-path-0-pathauto');
220 |
221 | // Edit the manual alias and save the node.
222 | $edit = array(
223 | 'title[0][value]' => 'Node version three',
224 | 'path[0][alias]' => '/manually-edited-alias',
225 | );
226 | $this->drupalPostForm(NULL, $edit, 'Save');
227 | $this->assertText('Basic page Node version three has been updated.');
228 |
229 | $this->assertEntityAlias($node, '/manually-edited-alias');
230 | $this->assertNoEntityAliasExists($node, '/test-alias');
231 | $this->assertNoEntityAliasExists($node, '/content/node-version-one');
232 | $this->assertNoEntityAliasExists($node, '/content/node-version-two');
233 | $this->assertNoEntityAliasExists($node, '/content/node-version-three');
234 |
235 | // Programatically save the node with an automatic alias.
236 | \Drupal::entityManager()->getStorage('node')->resetCache();
237 | $node = Node::load($node->id());
238 | $node->path->pathauto = PathautoState::CREATE;
239 | $node->save();
240 |
241 | // Ensure that the pathauto field was saved to the database.
242 | \Drupal::entityManager()->getStorage('node')->resetCache();
243 | $node = Node::load($node->id());
244 | $this->assertIdentical($node->path->pathauto, PathautoState::CREATE);
245 |
246 | $this->assertEntityAlias($node, '/content/node-version-three');
247 | $this->assertNoEntityAliasExists($node, '/manually-edited-alias');
248 | $this->assertNoEntityAliasExists($node, '/test-alias');
249 | $this->assertNoEntityAliasExists($node, '/content/node-version-one');
250 | $this->assertNoEntityAliasExists($node, '/content/node-version-two');
251 |
252 | $node->delete();
253 | $this->assertNull(\Drupal::keyValue('pathauto_state.node')->get($node->id()), 'Pathauto state was deleted');
254 | }
255 |
256 | }
257 |
--------------------------------------------------------------------------------
/src/Tests/PathautoTaxonomyWebTest.php:
--------------------------------------------------------------------------------
1 | adminUser = $this->drupalCreateUser($permissions);
48 | $this->drupalLogin($this->adminUser);
49 |
50 | $this->createPattern('taxonomy_term', '/[term:vocabulary]/[term:name]');
51 | }
52 |
53 |
54 | /**
55 | * Basic functional testing of Pathauto with taxonomy terms.
56 | */
57 | function testTermEditing() {
58 | $this->drupalGet('admin/structure');
59 | $this->drupalGet('admin/structure/taxonomy');
60 |
61 | // Add vocabulary "tags".
62 | $vocabulary = $this->addVocabulary(array('name' => 'tags', 'vid' => 'tags'));
63 |
64 | // Create term for testing.
65 | $name = 'Testing: term name [';
66 | $automatic_alias = '/tags/testing-term-name';
67 | $this->drupalPostForm('admin/structure/taxonomy/manage/tags/add', array('name[0][value]' => $name), 'Save');
68 | $name = trim($name);
69 | $this->assertText("Created new term $name.");
70 | $term = $this->drupalGetTermByName($name);
71 |
72 | // Look for alias generated in the form.
73 | $this->drupalGet("taxonomy/term/{$term->id()}/edit");
74 | $this->assertFieldChecked('edit-path-0-pathauto');
75 | $this->assertFieldByName('path[0][alias]', $automatic_alias, 'Generated alias visible in the path alias field.');
76 |
77 | // Check whether the alias actually works.
78 | $this->drupalGet($automatic_alias);
79 | $this->assertText($name, 'Term accessible through automatic alias.');
80 |
81 | // Manually set the term's alias.
82 | $manual_alias = '/tags/' . $term->id();
83 | $edit = array(
84 | 'path[0][pathauto]' => FALSE,
85 | 'path[0][alias]' => $manual_alias,
86 | );
87 | $this->drupalPostForm("taxonomy/term/{$term->id()}/edit", $edit, t('Save'));
88 | $this->assertText("Updated term $name.");
89 |
90 | // Check that the automatic alias checkbox is now unchecked by default.
91 | $this->drupalGet("taxonomy/term/{$term->id()}/edit");
92 | $this->assertNoFieldChecked('edit-path-0-pathauto');
93 | $this->assertFieldByName('path[0][alias]', $manual_alias);
94 |
95 | // Submit the term form with the default values.
96 | $this->drupalPostForm(NULL, array('path[0][pathauto]' => FALSE), t('Save'));
97 | $this->assertText("Updated term $name.");
98 |
99 | // Test that the old (automatic) alias has been deleted and only accessible
100 | // through the new (manual) alias.
101 | $this->drupalGet($automatic_alias);
102 | $this->assertResponse(404, 'Term not accessible through automatic alias.');
103 | $this->drupalGet($manual_alias);
104 | $this->assertText($name, 'Term accessible through manual alias.');
105 | }
106 |
107 |
108 | }
109 |
--------------------------------------------------------------------------------
/src/Tests/PathautoTestHelperTrait.php:
--------------------------------------------------------------------------------
1 | Unicode::strtolower($this->randomMachineName()),
39 | 'type' => 'canonical_entities:' . $entity_type_id,
40 | 'pattern' => $pattern,
41 | 'weight' => $weight,
42 | ]);
43 | $pattern->save();
44 | return $pattern;
45 | }
46 |
47 | /**
48 | * Add a bundle condition to a pathauto pattern.
49 | *
50 | * @param \Drupal\pathauto\PathautoPatternInterface $pattern
51 | * The pattern.
52 | * @param string $entity_type
53 | * The entity type ID.
54 | * @param string $bundle
55 | * The bundle
56 | */
57 | protected function addBundleCondition(PathautoPatternInterface $pattern, $entity_type, $bundle) {
58 | $pattern->addSelectionCondition(
59 | [
60 | 'id' => 'entity_bundle:' . $entity_type,
61 | 'bundles' => [
62 | $bundle => $bundle,
63 | ],
64 | 'negate' => FALSE,
65 | 'context_mapping' => [
66 | $entity_type => $entity_type,
67 | ]
68 | ]
69 | );
70 | }
71 |
72 | public function assertToken($type, $object, $token, $expected) {
73 | $bubbleable_metadata = new BubbleableMetadata();
74 | $tokens = \Drupal::token()->generate($type, array($token => $token), array($type => $object), [], $bubbleable_metadata);
75 | $tokens += array($token => '');
76 | $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)));
77 | }
78 |
79 | public function saveAlias($source, $alias, $langcode = Language::LANGCODE_NOT_SPECIFIED) {
80 | \Drupal::service('path.alias_storage')->delete(array('source' => $source, 'language', 'langcode' => $langcode));
81 | return \Drupal::service('path.alias_storage')->save($source, $alias, $langcode);
82 | }
83 |
84 | public function saveEntityAlias(EntityInterface $entity, $alias, $langcode = NULL) {
85 | // By default, use the entity language.
86 | if (!$langcode) {
87 | $langcode = $entity->language()->getId();
88 | }
89 | return $this->saveAlias('/' . $entity->urlInfo()->getInternalPath(), $alias, $langcode);
90 | }
91 |
92 | public function assertEntityAlias(EntityInterface $entity, $expected_alias, $langcode = NULL) {
93 | // By default, use the entity language.
94 | if (!$langcode) {
95 | $langcode = $entity->language()->getId();
96 | }
97 | $this->assertAlias('/' . $entity->urlInfo()->getInternalPath(), $expected_alias, $langcode);
98 | }
99 |
100 | public function assertEntityAliasExists(EntityInterface $entity) {
101 | return $this->assertAliasExists(array('source' => '/' . $entity->urlInfo()->getInternalPath()));
102 | }
103 |
104 | public function assertNoEntityAlias(EntityInterface $entity, $langcode = NULL) {
105 | // By default, use the entity language.
106 | if (!$langcode) {
107 | $langcode = $entity->language()->getId();
108 | }
109 | $this->assertEntityAlias($entity, '/' . $entity->urlInfo()->getInternalPath(), $langcode);
110 | }
111 |
112 | public function assertNoEntityAliasExists(EntityInterface $entity, $alias = NULL) {
113 | $path = array('source' => '/' . $entity->urlInfo()->getInternalPath());
114 | if (!empty($alias)) {
115 | $path['alias'] = $alias;
116 | }
117 | $this->assertNoAliasExists($path);
118 | }
119 |
120 | public function assertAlias($source, $expected_alias, $langcode = Language::LANGCODE_NOT_SPECIFIED) {
121 | $alias = array('alias' => $source);
122 | foreach (db_select('url_alias')->fields('url_alias')->condition('source', $source)->execute() as $row) {
123 | $alias = (array) $row;
124 | if ($row->alias == $expected_alias) {
125 | break;
126 | }
127 | }
128 | $this->assertIdentical($alias['alias'], $expected_alias, t("Alias for %source with language '@language' is correct.",
129 | array('%source' => $source, '@language' => $langcode)));
130 | }
131 |
132 | public function assertAliasExists($conditions) {
133 | $path = \Drupal::service('path.alias_storage')->load($conditions);
134 | $this->assertTrue($path, t('Alias with conditions @conditions found.', array('@conditions' => var_export($conditions, TRUE))));
135 | return $path;
136 | }
137 |
138 | public function assertNoAliasExists($conditions) {
139 | $alias = \Drupal::service('path.alias_storage')->load($conditions);
140 | $this->assertFalse($alias, t('Alias with conditions @conditions not found.', array('@conditions' => var_export($conditions, TRUE))));
141 | }
142 |
143 | public function deleteAllAliases() {
144 | db_delete('url_alias')->execute();
145 | \Drupal::service('path.alias_manager')->cacheClear();
146 | }
147 |
148 | /**
149 | * @param array $values
150 | * @return \Drupal\taxonomy\VocabularyInterface
151 | */
152 | public function addVocabulary(array $values = array()) {
153 | $name = Unicode::strtolower($this->randomMachineName(5));
154 | $values += array(
155 | 'name' => $name,
156 | 'vid' => $name,
157 | );
158 | $vocabulary = entity_create('taxonomy_vocabulary', $values);
159 | $vocabulary->save();
160 |
161 | return $vocabulary;
162 | }
163 |
164 | public function addTerm(VocabularyInterface $vocabulary, array $values = array()) {
165 | $values += array(
166 | 'name' => Unicode::strtolower($this->randomMachineName(5)),
167 | 'vid' => $vocabulary->id(),
168 | );
169 |
170 | $term = entity_create('taxonomy_term', $values);
171 | $term->save();
172 | return $term;
173 | }
174 |
175 | public function assertEntityPattern($entity_type, $bundle, $langcode = Language::LANGCODE_NOT_SPECIFIED, $expected) {
176 |
177 | $values = [
178 | 'langcode' => $langcode,
179 | \Drupal::entityTypeManager()->getDefinition($entity_type)->getKey('bundle') => $bundle,
180 | ];
181 | $entity = \Drupal::entityTypeManager()->getStorage($entity_type)->create($values);
182 |
183 | $pattern = \Drupal::service('pathauto.generator')->getPatternByEntity($entity);
184 | $this->assertIdentical($expected, $pattern->getPattern());
185 | }
186 |
187 | public function drupalGetTermByName($name, $reset = FALSE) {
188 | if ($reset) {
189 | // @todo - implement cache reset.
190 | }
191 | $terms = \Drupal::entityManager()->getStorage('taxonomy_term')->loadByProperties(array('name' => $name));
192 | return !empty($terms) ? reset($terms) : FALSE;
193 | }
194 | }
195 |
--------------------------------------------------------------------------------
/src/Tests/PathautoTokenTest.php:
--------------------------------------------------------------------------------
1 | installConfig(array('pathauto'));
30 |
31 | $array = array(
32 | 'test first arg',
33 | 'The Array / value',
34 | );
35 |
36 | $tokens = array(
37 | 'join-path' => 'test-first-arg/array-value',
38 | );
39 | $data['array'] = $array;
40 | $replacements = $this->assertTokens('array', $data, $tokens);
41 |
42 | // Ensure that the cleanTokenValues() method does not alter this token value.
43 | /* @var \Drupal\pathauto\AliasCleanerInterface $alias_cleaner */
44 | $alias_cleaner = \Drupal::service('pathauto.alias_cleaner');
45 | $alias_cleaner->cleanTokenValues($replacements, $data, array());
46 | $this->assertEqual($replacements['[array:join-path]'], 'test-first-arg/array-value');
47 | }
48 |
49 | /**
50 | * Function copied from TokenTestHelper::assertTokens().
51 | */
52 | public function assertTokens($type, array $data, array $tokens, array $options = array()) {
53 | $input = $this->mapTokenNames($type, array_keys($tokens));
54 | $bubbleable_metadata = new BubbleableMetadata();
55 | $replacements = \Drupal::token()->generate($type, $input, $data, $options, $bubbleable_metadata);
56 | foreach ($tokens as $name => $expected) {
57 | $token = $input[$name];
58 | if (!isset($expected)) {
59 | $this->assertTrue(!isset($values[$token]), t("Token value for @token was not generated.", array('@type' => $type, '@token' => $token)));
60 | }
61 | elseif (!isset($replacements[$token])) {
62 | $this->fail(t("Token value for @token was not generated.", array('@type' => $type, '@token' => $token)));
63 | }
64 | elseif (!empty($options['regex'])) {
65 | $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)));
66 | }
67 | else {
68 | $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)));
69 | }
70 | }
71 |
72 | return $replacements;
73 | }
74 |
75 | public function mapTokenNames($type, array $tokens = array()) {
76 | $return = array();
77 | foreach ($tokens as $token) {
78 | $return[$token] = "[$type:$token]";
79 | }
80 | return $return;
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/Tests/PathautoUiTest.php:
--------------------------------------------------------------------------------
1 | drupalCreateContentType(array('type' => 'page', 'name' => 'Basic page'));
42 | $this->drupalCreateContentType(array('type' => 'article'));
43 |
44 | // Allow other modules to add additional permissions for the admin user.
45 | $permissions = array(
46 | 'administer pathauto',
47 | 'administer url aliases',
48 | 'create url aliases',
49 | 'administer nodes',
50 | 'bypass node access',
51 | 'access content overview',
52 | );
53 | $this->adminUser = $this->drupalCreateUser($permissions);
54 | $this->drupalLogin($this->adminUser);
55 | }
56 |
57 | function testSettingsValidation() {
58 | $edit = array();
59 | $edit['max_length'] = 'abc';
60 | $edit['max_component_length'] = 'abc';
61 | $this->drupalPostForm('admin/config/search/path/settings', $edit, 'Save configuration');
62 | /*$this->assertText('The field Maximum alias length is not a valid number.');
63 | $this->assertText('The field Maximum component length is not a valid number.');*/
64 | $this->assertNoText('The configuration options have been saved.');
65 |
66 | $edit['max_length'] = '0';
67 | $edit['max_component_length'] = '0';
68 | $this->drupalPostForm('admin/config/search/path/settings', $edit, 'Save configuration');
69 | /*$this->assertText('The field Maximum alias length cannot be less than 1.');
70 | $this->assertText('The field Maximum component length cannot be less than 1.');*/
71 | $this->assertNoText('The configuration options have been saved.');
72 |
73 | $edit['max_length'] = '999';
74 | $edit['max_component_length'] = '999';
75 | $this->drupalPostForm('admin/config/search/path/settings', $edit, 'Save configuration');
76 | /*$this->assertText('The field Maximum alias length cannot be greater than 255.');
77 | $this->assertText('The field Maximum component length cannot be greater than 255.');*/
78 | $this->assertNoText('The configuration options have been saved.');
79 |
80 | $edit['max_length'] = '50';
81 | $edit['max_component_length'] = '50';
82 | $this->drupalPostForm('admin/config/search/path/settings', $edit, 'Save configuration');
83 | $this->assertText('The configuration options have been saved.');
84 | }
85 |
86 | function testPatternsValidation() {
87 | // Try to save an invalid pattern.
88 | $this->drupalGet('admin/config/search/path/patterns/add');
89 | $edit = array(
90 | 'type' => 'canonical_entities:node',
91 | );
92 | $this->drupalPostAjaxForm(NULL, $edit, 'type');
93 | $edit += array(
94 | 'pattern' => '[node:title]/[user:name]/[term:name]',
95 | 'bundles[page]' => TRUE,
96 | 'label' => 'Page pattern',
97 | 'id' => 'page_pattern',
98 | );
99 | $this->drupalPostForm(NULL, $edit, 'Save');
100 | $this->assertText('The Path pattern is using the following invalid tokens: [user:name], [term:name].');
101 | $this->assertNoText('The configuration options have been saved.');
102 |
103 | // Fix the pattern, then check that it gets saved successfully.
104 | $edit['pattern'] = '[node:title]';
105 | $this->drupalPostForm(NULL, $edit, 'Save');
106 | $this->assertText('Pattern Page pattern saved.');
107 | }
108 |
109 | }
110 |
--------------------------------------------------------------------------------
/src/Tests/PathautoUserWebTest.php:
--------------------------------------------------------------------------------
1 | adminUser = $this->drupalCreateUser($permissions);
50 | $this->drupalLogin($this->adminUser);
51 |
52 | $this->createPattern('user', '/users/[user:name]');
53 | }
54 |
55 |
56 | /**
57 | * Basic functional testing of Pathauto with users.
58 | */
59 | function testUserEditing() {
60 | // There should be no Pathauto checkbox on user forms.
61 | $this->drupalGet('user/' . $this->adminUser->id() . '/edit');
62 | $this->assertNoFieldById('path[0][pathauto]');
63 | }
64 |
65 | /**
66 | * Test user operations.
67 | */
68 | function testUserOperations() {
69 | $account = $this->drupalCreateUser();
70 |
71 | // Delete all current URL aliases.
72 | $this->deleteAllAliases();
73 |
74 | // Find the position of just created account in the user_admin_people view.
75 | $view = Views::getView('user_admin_people');
76 | $view->initDisplay();
77 | $view->preview('page_1');
78 |
79 |
80 | foreach ($view->result as $key => $row) {
81 | if ($view->field['name']->getValue($row) == $account->getUsername()) {
82 | break;
83 | }
84 | }
85 |
86 | $edit = array(
87 | 'action' => 'pathauto_update_alias_user',
88 | "user_bulk_form[$key]" => TRUE,
89 | );
90 | $this->drupalPostForm('admin/people', $edit, t('Apply'));
91 | $this->assertRaw(\Drupal::translation()->formatPlural(1, '%action was applied to @count item.', '%action was applied to @count items.', array(
92 | '%action' => 'Update URL-Alias',
93 | )));
94 |
95 | $this->assertEntityAlias($account, '/users/' . Unicode::strtolower($account->getUsername()));
96 | $this->assertEntityAlias($this->adminUser, '/user/' . $this->adminUser->id());
97 | }
98 |
99 | }
100 |
--------------------------------------------------------------------------------
/src/VerboseMessenger.php:
--------------------------------------------------------------------------------
1 | configFactory = $config_factory;
44 | $this->account = $account;
45 | }
46 |
47 | /**
48 | * {@inheritdoc}
49 | */
50 | public function addMessage($message, $op = NULL) {
51 |
52 | if (!isset($this->isVerbose)) {
53 | $config = $this->configFactory->get('pathauto.settings');
54 | $this->isVerbose = $config->get('verbose') && $this->account->hasPermission('notify of path changes');
55 | }
56 |
57 | if (!$this->isVerbose || (isset($op) && in_array($op, array('bulkupdate', 'return')))) {
58 | return FALSE;
59 | }
60 |
61 | if ($message) {
62 | drupal_set_message($message);
63 | }
64 |
65 | return TRUE;
66 | }
67 |
68 | }
69 |
--------------------------------------------------------------------------------
/tests/src/VerboseMessengerTest.php:
--------------------------------------------------------------------------------
1 | getConfigFactoryStub(array('pathauto.settings' => array('verbose' => TRUE)));
31 | $account = $this->getMock('\Drupal\Core\Session\AccountInterface');
32 | $account->expects($this->once())
33 | ->method('hasPermission')
34 | ->withAnyParameters()
35 | ->willReturn(TRUE);
36 |
37 | $this->messenger = new VerboseMessenger($config_factory, $account);
38 | }
39 |
40 | /**
41 | * Tests add messages.
42 | * @covers ::addMessage
43 | */
44 | public function testAddMessage() {
45 | $this->assertTrue($this->messenger->addMessage("Test message"), "The message was added");
46 | }
47 |
48 | /**
49 | * @covers ::addMessage
50 | */
51 | public function testDoNotAddMessageWhileBulkupdate() {
52 | $this->assertFalse($this->messenger->addMessage("Test message", "bulkupdate"), "The message was NOT added");
53 | }
54 | }
55 |
56 | }
57 | namespace {
58 | // @todo Delete after https://drupal.org/node/1858196 is in.
59 | if (!function_exists('drupal_set_message')) {
60 | function drupal_set_message() {
61 | }
62 | }
63 | }
64 |
65 |
--------------------------------------------------------------------------------