diff --git a/.travis-before-script.sh b/.travis-before-script.sh new file mode 100644 index 0000000..a361750 --- /dev/null +++ b/.travis-before-script.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +set -e $DRUPAL_TI_DEBUG + +# Ensure the right Drupal version is installed. +# Note: This function is re-entrant. +drupal_ti_ensure_drupal + +# Add needed dependencies. +cd "$DRUPAL_TI_DRUPAL_DIR" + +# These variables come from environments/drupal-*.sh +mkdir -p "$DRUPAL_TI_MODULES_PATH" +cd "$DRUPAL_TI_MODULES_PATH" + +# Download token 8.x-1.x and ctools 8.x-3.x +git clone --depth 1 --branch 8.x-1.x http://git.drupal.org/project/token.git +git clone --depth 1 --branch 8.x-3.x http://git.drupal.org/project/ctools.git diff --git a/.travis.yml b/.travis.yml index 78b774f..112aea1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,96 +1,115 @@ +# @file +# .travis.yml - Drupal for Travis CI Integration +# +# Template provided by https://github.com/LionsAd/drupal_ti. +# +# Based for simpletest upon: +# https://github.com/sonnym/travis-ci-drupal-module-example + language: php -cache: - bundler: true - directories: - - $HOME/tmp/drush - - $HOME/.bundle - apt: true + +sudo: false php: - - 5.4 - 5.5 + - 5.6 + - 7 + - hhvm + +matrix: + fast_finish: true + allow_failures: + - php: 7 + - php: hhvm env: - - PATH=$PATH:/home/travis/.composer/vendor/bin + global: + # add composer's global bin directory to the path + # see: https://github.com/drush-ops/drush#install---composer + - PATH="$PATH:$HOME/.composer/vendor/bin" + + # Configuration variables. + - DRUPAL_TI_MODULE_NAME="pathauto" + - DRUPAL_TI_SIMPLETEST_GROUP="pathauto" + + # Define runners and environment vars to include before and after the + # main runners / environment vars. + #- DRUPAL_TI_SCRIPT_DIR_BEFORE="./.drupal_ti/before" + #- DRUPAL_TI_SCRIPT_DIR_AFTER="./drupal_ti/after" + + # The environment to use, supported are: drupal-7, drupal-8 + - DRUPAL_TI_ENVIRONMENT="drupal-8" + + # Drupal specific variables. + - DRUPAL_TI_DB="drupal_travis_db" + - DRUPAL_TI_DB_URL="mysql://root:@127.0.0.1/drupal_travis_db" + # Note: Do not add a trailing slash here. + - DRUPAL_TI_WEBSERVER_URL="http://127.0.0.1" + - DRUPAL_TI_WEBSERVER_PORT="8080" + + # Simpletest specific commandline arguments, the DRUPAL_TI_SIMPLETEST_GROUP is appended at the end. + - DRUPAL_TI_SIMPLETEST_ARGS="--verbose --color --concurrency 4 --url $DRUPAL_TI_WEBSERVER_URL:$DRUPAL_TI_WEBSERVER_PORT" + + # === Behat specific variables. + # This is relative to $TRAVIS_BUILD_DIR + - DRUPAL_TI_BEHAT_DIR="./tests/behat" + # These arguments are passed to the bin/behat command. + - DRUPAL_TI_BEHAT_ARGS="" + # Specify the filename of the behat.yml with the $DRUPAL_TI_DRUPAL_DIR variables. + - DRUPAL_TI_BEHAT_YML="behat.yml.dist" + # This is used to setup Xvfb. + - DRUPAL_TI_BEHAT_SCREENSIZE_COLOR="1280x1024x16" + # The version of seleniumthat should be used. + - DRUPAL_TI_BEHAT_SELENIUM_VERSION="2.44" + # Set DRUPAL_TI_BEHAT_DRIVER to "selenium" to use "firefox" or "chrome" here. + - DRUPAL_TI_BEHAT_DRIVER="phantomjs" + - DRUPAL_TI_BEHAT_BROWSER="firefox" + + # PHPUnit specific commandline arguments. + - DRUPAL_TI_PHPUNIT_ARGS="--verbose --debug" + # Specifying the phpunit-core src/ directory is useful when e.g. a vendor/ + # directory is present in the module directory, which phpunit would then + # try to find tests in. This option is relative to $TRAVIS_BUILD_DIR. + #- DRUPAL_TI_PHPUNIT_CORE_SRC_DIRECTORY="./tests/src" + + # Code coverage via coveralls.io + - DRUPAL_TI_COVERAGE="satooshi/php-coveralls:0.6.*" + # This needs to match your .coveralls.yml file. + - DRUPAL_TI_COVERAGE_FILE="build/logs/clover.xml" + + # Debug options + #- DRUPAL_TI_DEBUG="-x -v" + # Set to "all" to output all files, set to e.g. "xvfb selenium" or "selenium", + # etc. to only output those channels. + #- DRUPAL_TI_DEBUG_FILE_OUTPUT="selenium xvfb webserver" + + matrix: + # [[[ SELECT ANY OR MORE OPTIONS ]]] + #- DRUPAL_TI_RUNNERS="phpunit" + #- DRUPAL_TI_RUNNERS="simpletest" + #- DRUPAL_TI_RUNNERS="behat" + - DRUPAL_TI_RUNNERS="phpunit-core simpletest" -# This will create the database mysql: - database: drupal + database: drupal_travis_db username: root encoding: utf8 -# To be able to run a webbrowser -# If we need anything more powerful -# than e.g. phantomjs before_install: - - "export DISPLAY=:99.0" - - "sh -e /etc/init.d/xvfb start" + - composer self-update + - composer global require "lionsad/drupal_ti:1.*" + - drupal-ti before_install install: - # Grab Drush - - composer global require drush/drush:dev-master --prefer-source - - cd /home/travis/.composer/vendor/drush/drush && cd - - # Make sure we don't fail when checking out projects - - echo -e "Host github.com\n\tStrictHostKeyChecking no\n" >> ~/.ssh/config - # LAMP package installation (mysql is already started) - - sudo apt-get update - - sudo apt-get install apache2 libapache2-mod-fastcgi - # enable php-fpm, travis does not support any other method with php and apache - - sudo cp ~/.phpenv/versions/$(phpenv version-name)/etc/php-fpm.conf.default ~/.phpenv/versions/$(phpenv version-name)/etc/php-fpm.conf - - sudo a2enmod rewrite actions fastcgi alias - - echo "cgi.fix_pathinfo = 1" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini - - ~/.phpenv/versions/$(phpenv version-name)/sbin/php-fpm - # Make sure the apache root is in our wanted directory - - echo "$(curl -fsSL https://gist.githubusercontent.com/nickveenhof/11386315/raw/b8abaf9304fe12b5cc7752d39c29c1edae8ac2e6/gistfile1.txt)" | sed -e "s,PATH,$TRAVIS_BUILD_DIR/../drupal,g" | sudo tee /etc/apache2/sites-available/default > /dev/null - # Set sendmail so drush doesn't throw an error during site install. - - echo "sendmail_path='true'" >> `php --ini | grep "Loaded Configuration" | awk '{print $4}'` - # Forward the errors to the syslog so we can print them - - echo "error_log=syslog" >> `php --ini | grep "Loaded Configuration" | awk '{print $4}'` - # Get latest drupal 8 core - - cd $TRAVIS_BUILD_DIR/.. - - git clone --depth 1 --branch 8.0.x http://git.drupal.org/project/drupal.git - - cd drupal/modules - - git clone --depth 1 --branch 8.x-1.x http://git.drupal.org/project/token.git - # Restart apache and test it - - sudo service apache2 restart - - curl -v "http://localhost" - # Re-enable when trying to get CodeSniffer doesn't return a 403 anymore. - #- composer global require drupal/coder:\>7 + - drupal-ti install before_script: - - cd $TRAVIS_BUILD_DIR/../drupal - # Update drupal core - - git pull origin 8.0.x - # Install the site - - drush -v site-install minimal --db-url=mysql://root:@localhost/drupal --yes - - drush en --yes simpletest - - drush cr - - phpenv rehash + # Install token 8.x-1.x + - drupal-ti --include .travis-before-script.sh + - drupal-ti before_script script: - # go to our Drupal module directory - - mkdir $TRAVIS_BUILD_DIR/../drupal/modules/pathauto - - cp -R $TRAVIS_BUILD_DIR/* $TRAVIS_BUILD_DIR/../drupal/modules/pathauto/ - # go to our Drupal main directory - - cd $TRAVIS_BUILD_DIR/../drupal - - ls -la $TRAVIS_BUILD_DIR/../drupal/sites/default - # Run the tests - - php core/scripts/run-tests.sh --verbose --color --concurrency 4 --php `which php` --url http://localhost "pathauto" | tee /tmp/test.txt; TEST_EXIT=${PIPESTATUS[0]}; echo $TEST_EXIT - # Check if we had fails in the run-tests.sh script - # Exit with the inverted value, because if there are no fails found, it will exit with 1 and for us that\ - # is a good thing so invert it to 0. Travis has some issues with the exclamation mark in front so we have to fiddle a - # bit. - # Also make the grep case insensitive and fail on run-tests.sh regular fails as well on fatal errors. - - TEST_OUTPUT=$(! egrep -i "([0-9]+ fails)|(PHP Fatal error)|([0-9]+ exceptions)" /tmp/test.txt > /dev/null)$? - - echo $TEST_OUTPUT - - cd $TRAVIS_BUILD_DIR/../drupal/core - - ./vendor/bin/phpunit --verbose --debug ../modules/pathauto/; TEST_PHPUNIT=$?; echo $TEST_PHPUNIT - # if the TEST_EXIT status is 0 AND the TEST_OUTPUT status is also 0 it means we succeeded, in all other cases we - # failed. - # Re-enable when trying to get CodeSniffer doesn't return a 403 anymore. - #- /home/travis/.composer/vendor/bin/phpcs --standard=/home/travis/.composer/vendor/drupal/coder/coder_sniffer/Drupal --extensions=php,inc,test,module,install --ignore=css/ $TRAVIS_BUILD_DIR/../drupal/modules/search_api - - php -i | grep 'php.ini' - - sudo cat /var/log/apache2/error.log - # Disabled as it exits with 1 - sudo cat /var/log/syslog | grep 'php' - # Exit the build - - if [ $TEST_EXIT -eq 0 ] && [ $TEST_OUTPUT -eq 0 ] && [ $TEST_PHPUNIT -eq 0 ]; then exit 0; else exit 1; fi + - drupal-ti script + +after_script: + - drupal-ti after_script diff --git a/API.txt b/API.txt deleted file mode 100644 index 0e845ba..0000000 --- a/API.txt +++ /dev/null @@ -1,149 +0,0 @@ -This document explains how to provide "Pathauto integration" in a -module. You need this if you would like to provide additional tokens -or if your module has paths and you wish to have them automatically -aliased. The simplest integration is just to provide tokens so we -cover that first. More advanced integration requires an -implementation of hook_pathauto to provide a settings form. - -It may be helpful to review some examples of integration from the -pathauto_node.inc, pathauto_taxonomy.inc, and pathauto_user.inc files. - - -================== -1 - Providing additional tokens -================== - -If all you want is to enable tokens for your module you will simply -need to implement two functions: - - hook_token_values - hook_token_list - -See the token.module and it's API.txt for more information about this -process. - -If the token is intended to generate a path expected to contain slashes, -the token name must end in 'path', 'path-raw' or 'alias'. This indicates to -Pathauto that the slashes should not be removed from the replacement value. - -When an object is created (whether it is a node or a user or a -taxonomy term) the data that Pathauto hands to the token_values in the -$object is in a specific format. This is the format that most people -write code to handle. However, during edits and bulk updates the data -may be in a totally different format. So, if you are writing a -hook_token_values implementation to add special tokens, be sure to -test creation, edit, and bulk update cases to make sure your code will -handle it. - -================== -2 - Settings hook - To create aliases for your module -================== -You must implement hook_pathauto($op), where $op is always (at this -time) 'settings'. Return an object (NOT an array) containing the -following members, which will be used by pathauto to build a group -of settings for your module and define the variables for saving your -settings: - -module - The name of your module (e.g., 'node') -groupheader - The translated label for the settings group (e.g., - t('Content path settings') -patterndescr - The translated label for the default pattern (e.g., - t('Default path pattern (applies to all content types with blank patterns below)') -patterndefault - A translated default pattern (e.g., t('[cat]/[title].html')) -token_type - The token type (e.g. 'node', 'user') that can be used. -patternitems - For modules which need to express multiple patterns - (for example, the node module supports a separate pattern for each - content type), an array whose keys consist of identifiers for each - pattern (e.g., the content type name) and values consist of the - translated label for the pattern -bulkname - For modules which support a bulk update operation, the - translated label for the action (e.g., t('Bulk update content paths')) -bulkdescr - For modules which support a bulk update operation, a - translated, more thorough description of what the operation will do - (e.g., t('Generate aliases for all existing content items which do not already have aliases.')) - - -================== -2 - $alias = \Drupal::service('pathauto.manager')->createAlias($module, $op, $placeholders, $src, $type=NULL) -================== - -At the appropriate time (usually when a new item is being created for -which a generated alias is desired), call \Drupal::service('pathauto.manager')->createAlias() to -generate and create the alias. See the user, taxonomy, and nodeapi hook -implementations in pathauto.module for examples. - -$module - The name of your module (e.g., 'node') -$op - Operation being performed on the item ('insert', 'update', or - 'bulkupdate') -$placeholders - An array whose keys consist of the translated placeholders - which appear in patterns and values are the "clean" values to be - substituted into the pattern. Call \Drupal::service('pathauto.manager')->cleanString() on any - values which you do not know to be purely alphanumeric, to substitute - any non-alphanumerics with the user's designated separator. Note that - if the pattern has multiple slash-separated components (e.g., [term:path]), - \Drupal::service('pathauto.manager')->cleanString() should be called for each component, not the - complete string. - Example: $placeholders[t('[title]')] = \Drupal::service('pathauto.manager')->cleanString($node->title); -$src - The "real" URI of the content to be aliased (e.g., "node/$node->nid") -$type - For modules which provided patternitems in hook_autopath(), - the relevant identifier for the specific item to be aliased (e.g., - $node->type) - -\Drupal::service('pathauto.manager')->createAlias() returns the alias that was created. - - -================== -3 - Bulk update function -================== - -If a module supports bulk updating of aliases, it must provide a -function of this form, to be called by pathauto when the corresponding -checkbox is selected and the settings page submitted: - -function _pathauto_bulkupdate() - -The function should iterate over the content items controlled by the -module, calling \Drupal::service('pathauto.manager')->createAlias() for each one. It is -recommended that the function report on its success (e.g., with a -count of created aliases) via drupal_set_message(). - - -================== -4 - Bulk delete hook_path_alias_types() -================== - -For modules that create new types of pages that can be aliased with pathauto, a -hook implementation is needed to allow the user to delete them all at once. - -function hook_path_alias_types() - -This hook returns an array whose keys match the beginning of the source paths -(e.g.: "node/", "user/", etc.) and whose values describe the type of page (e.g.: -"content", "users"). Like all displayed strings, these descriptionsshould be -localized with t(). Use % to match interior pieces of a path; "user/%/track". This -is a database wildcard, so be careful. - - -================== -Modules that extend node and/or taxonomy -================== - -NOTE: this is basically not true any more. If you feel you need this file an issue. - -Many contributed Drupal modules extend the core node and taxonomy -modules. To extend pathauto patterns to support their extensions, they -may implement the pathauto_node and pathauto_taxonomy hooks. - -To do so, implement the function _pathauto_node (or _taxonomy), -accepting the arguments $op and $node (or $term). Two operations are -supported: - -$op = 'placeholders' - return an array keyed on placeholder strings -(e.g., t('[eventyyyy]')) valued with descriptions (e.g. t('The year the -event starts.')). -$op = 'values' - return an array keyed on placeholder strings, valued -with the "clean" actual value for the passed node or category (e.g., -\Drupal::service('pathauto.manager')->cleanString(date('M', $eventstart))); - -See contrib/pathauto_node_event.inc for an example of extending node -patterns. diff --git a/INSTALL.txt b/INSTALL.txt deleted file mode 100644 index 586fdfe..0000000 --- a/INSTALL.txt +++ /dev/null @@ -1,48 +0,0 @@ -**Installation: - -Pathauto is an extension to the path module, which must be enabled. - -Pathauto also relies on the Token module, which must be downloaded and -enabled separately. - -1. Unpack the Pathauto folder and contents in the appropriate modules -directory of your Drupal installation. This is probably - sites/all/modules/ -2. Enable the Pathauto module in the administration tools. -3. If you're not using Drupal's default administrative account, make -sure "administer pathauto" is enabled through access control administration. -4. Visit the Pathauto settings page and make appropriate configurations - For 5.x: Administer > Site configuration > Pathauto - For 6.x: Administer > Site building > URL alias > Automated alias settings - -**Transliteration support: -If you desire transliteration support in the creation of URLs (e.g. the -replacement of Á with A) then you will need to install the Transliteration -module, which can be found at http://drupal.org/project/transliteration - -Once you've installed and enabled the module, simply go to -admin/config/search/path/settings and check the "Transliterate prior to -creating alias" box and path aliases should now be transliterated automagically. - -**Upgrading from previous versions: -If you are upgrading from Pathauto 5.x-1.x to 5.x-2.x (or 6.x-2.x) then you -will probably need to change your patterns. - -For content patterns: - [user] is now [author-name] - [cat] is now [term] - -There may be other changes as well. Please review the pattern examples on - Administration > Site Configuration > Pathauto - -If you upgraded from Pathauto 5.x-1.x directly without enabling Token -first then you will need to - 1) download/install the Token module - 2) disable the Pathauto module - 3) re-enable the Pathauto module - -Upgrade to 6.x: -Note that the settings page has moved so that it is more logically grouped with -other URL alias related items under - Administer > Site building > URL alias > Automated alias settings - diff --git a/README.md b/README.md index 800e177..56d449c 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,6 @@ #Pathauto [![Build Status](https://travis-ci.org/md-systems/pathauto.svg?branch=8.x-1.x)](https://travis-ci.org/md-systems/pathauto) -Please read this file and also the INSTALL.txt. -They contain answers to many common questions. -If you are developing for this module, the API.txt may be interesting. -If you are upgrading, check the CHANGELOG.txt for major changes. +If you are developing for this module, have a look at pathauto.api.php. ##Description @@ -12,10 +9,11 @@ automatically generate aliases based on appropriate criteria, with a central settings path for site administrators. Implementations are provided for core entity types: content, taxonomy terms, -and users (including blogs and tracker pages). +and users (including blogs and forum pages). Pathauto also provides a way to delete large numbers of aliases. This feature -is available at Administer > Site building > URL aliases > Delete aliases +is available at Administer > Configuration > Search and metadata > URL aliases > +Delete aliases. ##Benefits @@ -26,10 +24,6 @@ automatically using keywords based directly on the page content in the URL, relevant search engine hits for your page can be significantly enhanced. -##Installation AND Upgrades - -See the INSTALL.txt file. - ##Notices Pathauto just adds URL aliases to content, users, and taxonomy terms. @@ -50,18 +44,12 @@ For external links, you might want to consider the Path Redirect or Global Redirect modules, which allow you to set forwarding either per item or across the site to your aliased URLs. -URLs (not) Getting Replaced With Aliases: -Please bear in mind that only URLs passed through Drupal's l() or url() -functions will be replaced with their aliases during page output. If a module -or your template contains hardcoded links, such as 'href="node/$node->nid"' -those won't get replaced with their corresponding aliases. Use the -Drupal API instead: - -* 'href="'. url("node/$node->nid") .'"' or -* l("Your link title", "node/$node->nid") - -See http://api.drupal.org/api/HEAD/function/url and -http://api.drupal.org/api/HEAD/function/l for more information. +###URLs (not) Getting Replaced With Aliases: +Please bear in mind that only URLs passed through Drupal's Drupal's URL and +Link APIs will be replaced with their aliases during page output. If +a module or your template contains hardcoded links, such as +'href="node/$node->nid"', those won't get replaced with their corresponding +aliases. ## Disabling Pathauto for a specific content type (or taxonomy) @@ -69,20 +57,6 @@ When the pattern for a content type is left blank, the default pattern will be used. But if the default pattern is also blank, Pathauto will be disabled for that content type. -## Bulk Updates Must be Run Multiple Times - -As of 5.x-2.x Pathauto now performs bulk updates in a manner which is more -likely to succeed on large sites. The drawback is that it needs to be run -multiple times. If you want to reduce the number of times that you need to -run Pathauto you can increase the "Maximum number of objects to alias in a -bulk update:" setting under General Settings. - -##WYSIWYG Conflicts - FCKEditor, TinyMCE, etc. - -If you use a WYSIWYG editor, please disable it for the Pathauto admin page. -Failure to do so may cause errors about "preg_replace" problems due to the

-tag being added to the "strings to replace". See http://drupal.org/node/175772 - ##Credits: The original module combined the functionality of Mike Ryan's autopath with @@ -90,7 +64,8 @@ Tommy Sundstrom's path_automatic. Significant enhancements were contributed by jdmquin @ www.bcdems.net. -Matt England added the tracker support. +Matt England added the tracker support (tracker support has been removed in +recent changes). Other suggestions and patches contributed by the Drupal community. diff --git a/TODO.txt b/TODO.txt deleted file mode 100644 index 710a830..0000000 --- a/TODO.txt +++ /dev/null @@ -1,11 +0,0 @@ -- Creating a new node with a manually set alias and not unchecking - the checkbox results in two aliases, one saved by pathauto and one by - PathItem. A possible clean way to do this might be to replace the PathItem - class with a ustom one and completely replace the logic there. Or fix core to - make sure that it sets ->pid after saving a new alias. - -- pathauto_path_alias_types() => Plugins - -- INSTALL.txt: Update - -- Update hook_help(): https://drupal.org/node/2250345 diff --git a/composer.json b/composer.json index 0b1952a..4d174bb 100644 --- a/composer.json +++ b/composer.json @@ -2,5 +2,17 @@ "name": "drupal/pathauto", "description": "Provides a generic set of views plugins a mechanism for modules to automatically generate aliases for the content they manage.", "type": "drupal-module", - "license": "GPL-2.0+" + "license": "GPL-2.0+", + + "repositories": [ + { + "type": "composer", + "url": "https://packagist.drupal-composer.org" + } + ], + + "require": { + "drupal/token": "8.1.x-dev", + "drupal/ctools": "8.3.x-dev" + } } diff --git a/config/install/pathauto.pattern.yml b/config/install/pathauto.pattern.yml deleted file mode 100644 index 008417f..0000000 --- a/config/install/pathauto.pattern.yml +++ /dev/null @@ -1,12 +0,0 @@ -patterns: - node: - default: 'content/[node:title]' - - taxonomy_term: - default: '[term:vocabulary]/[term:name]' - - forum: - default: '[term:vocabulary]/[term:name]' - - user: - default: 'users/[user:name]' diff --git a/config/install/pathauto.settings.yml b/config/install/pathauto.settings.yml index bdae42e..2a331f2 100644 --- a/config/install/pathauto.settings.yml +++ b/config/install/pathauto.settings.yml @@ -7,6 +7,6 @@ max_component_length: 100 transliterate : FALSE reduce_ascii : FALSE ignore_words : ', in, is,that, the , this, with, ' -case : 1 +case : TRUE ignore_words : 'a, an, as, at, before, but, by, for, from, is, in, into, like, of, off, on, onto, per, since, than, the, this, that, to, up, via, with' update_action : 2 diff --git a/config/schema/pathauto.schema.yml b/config/schema/pathauto.schema.yml index f3f2bc7..0148949 100644 --- a/config/schema/pathauto.schema.yml +++ b/config/schema/pathauto.schema.yml @@ -1,28 +1,5 @@ -pathauto.pattern: - type: mapping - mapping: - patterns: - type: sequence - sequence: - type: mapping - mapping: - default: - type: string - bundles: - type: sequence - sequence: - type: mapping - mapping: - default: - type: string - languages: - type: sequence - sequence: - type: string - - pathauto.settings: - type: mapping + type: config_object mapping: punctuation: type: sequence diff --git a/config/schema/pathauto_pattern.schema.yml b/config/schema/pathauto_pattern.schema.yml new file mode 100644 index 0000000..1cadb39 --- /dev/null +++ b/config/schema/pathauto_pattern.schema.yml @@ -0,0 +1,36 @@ +pathauto.pattern.*: + type: config_entity + label: 'Pathauto pattern config' + mapping: + id: + type: string + label: 'ID' + label: + type: label + label: 'Label' + uuid: + type: string + type: + type: string + label: 'Pattern type' + pattern: + type: string + label: 'Pattern' + selection_criteria: + type: sequence + label: 'Selection criteria' + sequence: + type: condition.plugin.[id] + label: 'Selection condition' + selection_logic: + type: string + label: 'Selection logic' + weight: + type: integer + label: 'Weight' + context_definitions: + type: sequence + label: 'Context definitions' + sequence: + - type: ctools.relationship + label: 'Context' diff --git a/pathauto.api.php b/pathauto.api.php index 26ec374..4af9023 100644 --- a/pathauto.api.php +++ b/pathauto.api.php @@ -1,18 +1,64 @@ get('entity.definitions.installed'); + foreach ($collection->getAll() as $key => $definitions) { + if (!is_array($definitions) || empty($definitions['path'])) { + continue; + } + + // Retrieve and change path base field definition. + $path_definition = $definitions['path']; + if (($options = $path_definition->getDisplayOptions('form')) && $options['type'] = 'pathauto') { + $options['type'] = 'path'; + $path_definition->setDisplayOptions('form', $options); + // Save the new value. + $collection->set($key, $definitions); + } + + } + + foreach (EntityFormDisplay::loadMultiple() as $form_display) { + if ($component = $form_display->getComponent('path')) { + if (isset($component['type']) && $component['type'] == 'pathauto') { + $component['type'] = 'path'; + $form_display->setComponent('path', $component); + $form_display->save(); + } + } + } +} + +/** + * Converts patterns from configuration objects to configuration entities. + */ +function pathauto_update_8100(&$sandbox) { + if (!\Drupal::service('module_handler')->moduleExists('ctools')) { + throw new UpdateException('Install Chaos tools suite (https://www.drupal.org/project/ctools) before running this database update.'); + } + + $messages = array(); + $entity_manager = \Drupal::service('entity.manager'); + $entity_type_manager = \Drupal::service('entity_type.manager'); + $language_manager = \Drupal::service('language_manager'); + $entity_types = $entity_manager->getDefinitions(); + + // 1. Load all patterns. + $config = \Drupal::service('config.factory')->getEditable('pathauto.pattern'); + $patterns = $config->get('patterns'); + + // 2. Create a configuration entity per pattern. + foreach ($patterns as $entity_type => $entity_patterns) { + if (!array_key_exists($entity_type, $entity_types)) { + // We found an unknown entity type. Report it. + $messages[] = t('Entity of type @type was not processed. It defines the following patterns: @patterns', array( + '@type' => $entity_type, + '@patterns' => print_r($entity_patterns, TRUE), + )); + continue; + } + $entity_label = $entity_types[$entity_type]->getLabel(); + + if (isset($entity_patterns['default'])) { + // This is a pattern for an entity type, such as "node". + $pattern = PathautoPattern::create([ + 'id' => $entity_type, + 'label' => $entity_label, + 'type' => 'canonical_entities:' . $entity_type, + 'pattern' => $entity_patterns['default'], + 'weight' => 0, + ]); + $pattern->save(); + } + + // Loop over bundles and create patterns if they have a value. + // Bundle keys may have a language suffix for language-dependant patterns. + if (isset($entity_patterns['bundles'])) { + $bundle_info = $entity_manager->getBundleInfo($entity_type); + foreach ($entity_patterns['bundles'] as $bundle => $bundle_patterns) { + if (empty($bundle_patterns['default'])) { + // This bundle does not define a pattern. Move on to the next one. + continue; + } + + if (isset($bundle_info[$bundle])) { + // This is a pattern for a bundle, such as "node_article". + $pattern = PathautoPattern::create([ + 'id' => $entity_type . '_' . $bundle, + 'label' => $entity_label . ' ' . $bundle_info[$bundle]['label'], + 'type' => 'canonical_entities:' . $entity_type, + 'pattern' => $bundle_patterns['default'], + 'weight' => -5, + ]); + + // Add the bundle condition. + $pattern->addSelectionCondition([ + 'id' => 'entity_bundle:' . $entity_type, + 'bundles' => array($bundle), + 'negate' => FALSE, + 'context_mapping' => [ $entity_type => $entity_type ], + ]); + + $pattern->save(); + } + else { + // This is either a language dependent pattern such as "article_es" or + // an unknown bundle or langcode. Let's figure it out. + $matches = NULL; + $langcode = NULL; + preg_match('/^(.*)_([a-z-]*)$/', $bundle, $matches); + if (count($matches) == 3) { + list(, $extracted_bundle, $langcode) = $matches; + $language = $language_manager->getLanguage($langcode); + } + // Validate bundle, langcode and language. + if (!isset($bundle_info[$extracted_bundle]) || ($langcode == NULL) || ($language == NULL)) { + $messages[] = t('Unrecognized entity bundle @entity:@bundle was not processed. It defines the following patterns: @patterns', array( + '@entity' => $entity_type, + '@bundle' => $bundle, + '@patterns' => print_r($entity_patterns, TRUE), + )); + continue; + } + + // This is a pattern for a bundle and a language, such as "node_article_es". + $pattern = PathautoPattern::create([ + 'id' => $entity_type . '_' . $extracted_bundle . '_' . str_replace('-', '_', $langcode), + 'label' => $entity_label . ' ' . $bundle_info[$extracted_bundle]['label'] . ' ' . $language->getName(), + 'type' => 'canonical_entities:' . $entity_type, + 'pattern' => $bundle_patterns['default'], + 'weight' => -10, + ]); + + // Add the bundle condition. + $pattern->addSelectionCondition([ + 'id' => 'entity_bundle:' . $entity_type, + 'bundles' => array($extracted_bundle => $extracted_bundle), + 'negate' => FALSE, + 'context_mapping' => [ $entity_type => $entity_type ], + ]); + + // Add the language condition. + $language_mapping = $entity_type . ':' . $entity_type_manager->getDefinition($entity_type)->getKey('langcode') . ':language'; + $pattern->addSelectionCondition([ + 'id' => 'language', + 'langcodes' => [ $langcode => $langcode ], + 'negate' => FALSE, + 'context_mapping' => [ + 'language' => $language_mapping, + ] + ]); + + // Add the context relationship for this language. + $new_definition = new ContextDefinition('language', 'Language'); + $new_context = new Context($new_definition); + $pattern->addContext($language_mapping, $new_context); + + $pattern->save(); + } + } + } + } + + // 3. Delete the old configuration object that stores patterns. + $config->delete(); + + // 4. Print out messages. + if (!empty($messages)) { + return implode('
', $messages); + } +} diff --git a/pathauto.js b/pathauto.js index be49cd6..52dcf12 100644 --- a/pathauto.js +++ b/pathauto.js @@ -3,13 +3,13 @@ Drupal.behaviors.pathFieldsetSummaries = { attach: function (context) { $('fieldset.path-form', context).drupalSetSummary(function (context) { - var path = $('.form-item-path-alias input').val(); - var automatic = $('.form-item-path-pathauto input').attr('checked'); + var path = $('.form-item-path-alias input', context).val(); + var automatic = $('.form-item-path-pathauto input', context).attr('checked'); if (automatic) { return Drupal.t('Automatic alias'); } - if (path) { + else if (path) { return Drupal.t('Alias: @alias', { '@alias': path }); } else { diff --git a/pathauto.links.action.yml b/pathauto.links.action.yml new file mode 100644 index 0000000..d68b94a --- /dev/null +++ b/pathauto.links.action.yml @@ -0,0 +1,6 @@ +entity.pathauto_pattern.add_form: + route_name: 'entity.pathauto_pattern.add_form' + title: 'Add Pathauto pattern' + appears_on: + - entity.pathauto_pattern.collection + diff --git a/pathauto.links.task.yml b/pathauto.links.task.yml index 2ce9963..d6fa0bc 100644 --- a/pathauto.links.task.yml +++ b/pathauto.links.task.yml @@ -1,5 +1,5 @@ pathauto.patterns.form: - route_name: pathauto.patterns.form + route_name: entity.pathauto_pattern.collection base_route: path.admin_overview title: 'Patterns' weight: 10 diff --git a/pathauto.module b/pathauto.module index 7484750..cc1d24e 100644 --- a/pathauto.module +++ b/pathauto.module @@ -16,15 +16,12 @@ * @ingroup pathauto */ - use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Field\BaseFieldDefinition; -use Drupal\Core\Language\Language; use Drupal\Core\Routing\RouteMatchInterface; -use Drupal\node\Entity\Node; -use Drupal\user\Entity\User; +use Drupal\pathauto\PathautoState; /** * The default ignore word list. @@ -36,8 +33,6 @@ define('PATHAUTO_IGNORE_WORDS', 'a, an, as, at, before, but, by, for, from, is, */ function pathauto_hook_info() { $hooks = array( - 'pathauto', - 'path_alias_types', 'pathauto_pattern_alter', 'pathauto_alias_alter', 'pathauto_is_alias_reserved', @@ -45,27 +40,6 @@ function pathauto_hook_info() { return array_fill_keys($hooks, array('group' => 'pathauto')); } - -/** - * Implements hook_module_implements_alter(). - * - * Adds pathauto support for core modules. - */ -function pathauto_module_implements_alter(&$implementations, $hook) { - if (in_array($hook, array('pathauto'))) { - $modules = array('node', 'taxonomy', 'user', 'forum'); - foreach ($modules as $module) { - if (\Drupal::moduleHandler()->moduleExists($module)) { - $implementations[$module] = TRUE; - } - } - // Move pathauto.module to get included first since it is responsible for - // other modules. - unset($implementations['pathauto']); - $implementations = array_merge(array('pathauto' => 'pathauto'), $implementations); - } -} - /** * Implements hook_help(). */ @@ -87,92 +61,16 @@ function pathauto_help($route_name, RouteMatchInterface $route_match) { } } -/** - * Delete multiple URL aliases. - * - * Intent of this is to abstract a potential path_delete_multiple() function - * for Drupal 7 or 8. - * - * @param $pids - * An array of path IDs to delete. - */ -function pathauto_path_delete_multiple($pids) { - foreach ($pids as $pid) { - \Drupal::service('path.alias_storage')->delete(array('pid' => $pid)); - } -} - -/** - * Delete an URL alias and any of its sub-paths. - * - * Given a source like 'node/1' this function will delete any alias that have - * that specific source or any sources that match 'node/1/%'. - * - * @param $source - * An string with a source URL path. - */ -function pathauto_path_delete_all($source) { - $sql = "SELECT pid FROM {url_alias} WHERE source = :source OR source LIKE :source_wildcard"; - $pids = db_query($sql, array(':source' => $source, ':source_wildcard' => $source . '/%'))->fetchCol(); - if ($pids) { - pathauto_path_delete_multiple($pids); - } -} - -/** - * Delete an entity URL alias and any of its sub-paths. - * - * This function also checks to see if the default entity URI is different from - * the current entity URI and will delete any of the default aliases. - * - * @param EntityInterface $entity - * An entity object. - * @param string $default_uri - * The optional default uri path for the entity. - */ -function pathauto_entity_path_delete_all(EntityInterface $entity, $default_uri = NULL) { - pathauto_path_delete_all($entity->getSystemPath()); - if (isset($default_uri) && $entity->getSystemPath() != $default_uri) { - pathauto_path_delete_all($default_uri); - } -} - - -/** - * Implements hook_entity_bundle_rename(). - */ -function pathauto_entity_bundle_rename($entity_type_id, $bundle_old, $bundle_new) { - $config = \Drupal::configFactory()->getEditable('pathauto.pattern'); - $bundle_settings = $config->get('patterns.' . $entity_type_id . '.bundles'); - - if (isset($bundle_settings[$bundle_old])) { - $bundle_settings[$bundle_new] = $bundle_settings[$bundle_old]; - unset($bundle_settings[$bundle_old]); - $config->set('patterns.' . $entity_type_id . '.bundles', $bundle_settings); - $config->save(); - } -} - -/** - * Implements hook__entity_bundle_delete(). - */ -function pathauto_entity_bundle_delete($entity_type, $bundle) { - $config = \Drupal::configFactory()->getEditable('pathauto.pattern'); - $config->clear('patterns.' . $entity_type . '.bundles.' . $bundle); - $config->save(); -} - - /** * Implements hook_entity_presave(). */ function pathauto_entity_presave($entity) { - if (!($entity instanceof ContentEntityInterface) || $entity->hasField('path')) { + if (!($entity instanceof ContentEntityInterface) || !$entity->hasField('path')) { return; } + // About to be saved (before insert/update) - if (!empty($entity->path->pathauto) && isset($entity->path->old_alias) - && $entity->path->alias == '' && $entity->path->old_alias != '') { + if ($entity->path->pathauto == PathautoState::SKIP && $entity->path->old_alias != '') { /* * There was an old alias, but when pathauto_perform_alias was checked * the javascript disabled the textbox which led to an empty value being @@ -181,28 +79,20 @@ function pathauto_entity_presave($entity) { */ $entity->path->alias = $entity->path->old_alias; } - - // Help prevent errors with progromatically creating entities by defining - // path['alias'] as an empty string. - // @see http://drupal.org/node/1328180 - // @see http://drupal.org/node/1576552 - if (isset($entity->path->pathauto) && !isset($entity->path->alias)) { - $entity->path->alias = ''; - } } /** * Implements hook_entity_insert(). */ function pathauto_entity_insert(EntityInterface $entity) { - \Drupal::service('pathauto.manager')->updateAlias($entity, 'insert'); + \Drupal::service('pathauto.generator')->updateEntityAlias($entity, 'insert'); } /** * Implements hook_entity_update(). */ function pathauto_entity_update(EntityInterface $entity) { - \Drupal::service('pathauto.manager')->updateAlias($entity, 'update'); + \Drupal::service('pathauto.generator')->updateEntityAlias($entity, 'update'); } @@ -210,78 +100,11 @@ function pathauto_entity_update(EntityInterface $entity) { * Implements hook_entity_update(). */ function pathauto_entity_delete(EntityInterface $entity) { - pathauto_entity_path_delete_all($entity); -} - -/** - * Update the URL aliases for multiple nodes. - * - * @param array $nids - * An array of node IDs. - * @param string $op - * Operation being performed on the nodes ('insert', 'update' or - * 'bulkupdate'). - * @param array $options - * An optional array of additional options. - */ -function pathauto_node_update_alias_multiple(array $nids, $op, array $options = array()) { - $options += array('message' => FALSE); - - $nodes = Node::loadMultiple($nids); - foreach ($nodes as $node) { - \Drupal::service('pathauto.manager')->updateAlias($node, $op, $options); - } - - if (!empty($options['message'])) { - drupal_set_message(\Drupal::translation()->formatPlural(count($nids), 'Updated URL alias for 1 node.', 'Updated URL aliases for @count nodes.')); - } -} - -/** - * Update the URL aliases for multiple taxonomy terms. - * - * @param array $tids - * An array of term IDs. - * @param string $op - * Operation being performed on the nodes ('insert', 'update' or - * 'bulkupdate'). - * @param array $options - * An optional array of additional options. - */ -function pathauto_taxonomy_term_update_alias_multiple(array $tids, $op, array $options = array()) { - $options += array('message' => FALSE); - - $terms = entity_load_multiple('taxonomy_term', $tids); - foreach ($terms as $term) { - \Drupal::service('pathauto.manager')->updateAlias($term, $op, $options); - } - - if (!empty($options['message'])) { - drupal_set_message(\Drupal::translation()->formatPlural(count($tids), 'Updated URL alias for 1 term.', 'Updated URL aliases for @count terms.')); - } -} - -/** - * Update the URL aliases for multiple user accounts. - * - * @param array $uids - * An array of user account IDs. - * @param string $op - * Operation being performed on the accounts ('insert', 'update' or - * 'bulkupdate'). - * @param array $options - * An optional array of additional options. - */ -function pathauto_user_update_alias_multiple(array $uids, $op, array $options = array()) { - $options += array('message' => FALSE); - - $accounts = User::loadMultiple($uids); - foreach ($accounts as $account) { - \Drupal::service('pathauto.manager')->updateAlias($account, $op, $options); + if ($entity->hasLinkTemplate('canonical')) { + \Drupal::service('pathauto.alias_storage_helper')->deleteEntityPathAll($entity); } - - if (!empty($options['message'])) { - drupal_set_message(\Drupal::translation()->formatPlural(count($uids), 'Updated URL alias for 1 user account.', 'Updated URL aliases for @count user accounts.')); + if ($entity instanceof ContentEntityInterface && $entity->hasField('path')) { + $entity->path->first()->get('pathauto')->purge(); } } @@ -293,18 +116,12 @@ function pathauto_field_info_alter(&$info) { } /** - * Implements hook_entity_base_field_info_alter(). + * Implements hook_field_widget_info_alter(). */ -function pathauto_entity_base_field_info_alter(&$fields, EntityTypeInterface $entity_type) { - if (isset($fields['path']) && $fields['path']->getType() == 'path') { - $fields['path']->setDisplayOptions('form', array( - 'type' => 'pathauto', - 'weight' => 30, - )); - } +function pathauto_field_widget_info_alter(&$widgets) { + $widgets['path']['class'] = 'Drupal\pathauto\PathautoWidget'; } - /** * Implements hook_entity_base_field_info(). */ @@ -316,8 +133,9 @@ function pathauto_entity_base_field_info(EntityTypeInterface $entity_type) { ->setCustomStorage(TRUE) ->setLabel(t('URL alias')) ->setTranslatable(TRUE) + ->setComputed(TRUE) ->setDisplayOptions('form', array( - 'type' => 'pathauto', + 'type' => 'path', 'weight' => 30, )) ->setDisplayConfigurable('form', TRUE); @@ -325,3 +143,14 @@ function pathauto_entity_base_field_info(EntityTypeInterface $entity_type) { return $fields; } } + +/** + * Implements hook_entity_base_field_info_alter(). + */ +function pathauto_entity_base_field_info_alter(&$fields, EntityTypeInterface $entity_type) { + if (isset($fields['path'])) { + // Path fields need to be computed so that the pathauto state can be + // accessed even if there is no alias being set. + $fields['path']->setComputed(TRUE); + } +} diff --git a/pathauto.pathauto.inc b/pathauto.pathauto.inc deleted file mode 100644 index 00861ef..0000000 --- a/pathauto.pathauto.inc +++ /dev/null @@ -1,329 +0,0 @@ -moduleExists('taxonomy')) { - $objects['taxonomy/term/'] = t('Taxonomy terms'); - } - if (\Drupal::moduleHandler()->moduleExists('forum')) { - $objects['forum/'] = t('Forums'); - } - return $objects; -} - -/** - * Implements hook_pathauto(). - * - * This function is empty so that the other core module implementations can be - * defined in this file. This is because in pathauto_module_implements_alter() - * we add pathauto to be included first. The module system then peforms a - * check on any subsequent run if this function still exists. If this does not - * exist, than this file will not get included and the core implementations - * will never get run. - * - * @see pathauto_module_implements_alter() - */ -function pathauto_pathauto() { - // Empty hook; see the above comment. -} - -/** - * Implements hook_pathauto(). - */ -function node_pathauto($op) { - switch ($op) { - case 'settings': - $settings = array(); - $settings['module'] = 'node'; - $settings['token_type'] = 'node'; - $settings['groupheader'] = t('Content paths'); - $settings['patterndescr'] = t('Default path pattern (applies to all content types with blank patterns below)'); - $settings['patterndefault'] = 'content/[node:title]'; - $settings['batch_update_callback'] = 'node_pathauto_bulk_update_batch_process'; - $settings['batch_file'] = drupal_get_path('module', 'pathauto') . '/pathauto.pathauto.inc'; - - $languages = array(); - if (\Drupal::moduleHandler()->moduleExists('locale')) { - $languages = array(Language::LANGCODE_NOT_SPECIFIED => t('language neutral')) + \Drupal::languageManager()->getLanguages('name'); - } - - foreach (node_type_get_names() as $node_type => $node_name) { - if (count($languages) && \Drupal::moduleHandler()->moduleExists('content_translation') && \Drupal::service('content_translation.manager')->isEnabled('node', $node_type)) { - $settings['patternitems'][$node_type] = t('Default path pattern for @node_type (applies to all @node_type content types with blank patterns below)', array('@node_type' => $node_name)); - foreach ($languages as $lang_code => $lang_name) { - $settings['patternitems'][$node_type . '_' . $lang_code] = t('Pattern for all @language @node_type paths', array('@node_type' => $node_name, '@language' => $lang_name)); - } - } - else { - $settings['patternitems'][$node_type] = t('Pattern for all @node_type paths', array('@node_type' => $node_name)); - } - } - return (object) $settings; - - default: - break; - } -} - -/** - * Batch processing callback; Generate aliases for nodes. - */ -function node_pathauto_bulk_update_batch_process(&$context) { - if (!isset($context['sandbox']['current'])) { - $context['sandbox']['count'] = 0; - $context['sandbox']['current'] = 0; - } - - $query = db_select('node', 'n'); - $query->leftJoin('url_alias', 'ua', "CONCAT('node/', n.nid) = ua.source"); - $query->addField('n', 'nid'); - $query->isNull('ua.source'); - $query->condition('n.nid', $context['sandbox']['current'], '>'); - $query->orderBy('n.nid'); - $query->addTag('pathauto_bulk_update'); - $query->addMetaData('entity', 'node'); - - // Get the total amount of items to process. - if (!isset($context['sandbox']['total'])) { - $context['sandbox']['total'] = $query->countQuery()->execute()->fetchField(); - - // If there are no nodes to update, the stop immediately. - if (!$context['sandbox']['total']) { - $context['finished'] = 1; - return; - } - } - - $query->range(0, 25); - $nids = $query->execute()->fetchCol(); - - pathauto_node_update_alias_multiple($nids, 'bulkupdate'); - $context['sandbox']['count'] += count($nids); - $context['sandbox']['current'] = max($nids); - $context['message'] = t('Updated alias for node @nid.', array('@nid' => end($nids))); - - if ($context['sandbox']['count'] != $context['sandbox']['total']) { - $context['finished'] = $context['sandbox']['count'] / $context['sandbox']['total']; - } -} - -/** - * Implements hook_pathauto(). - */ -function taxonomy_pathauto($op) { - switch ($op) { - case 'settings': - $settings = array(); - $settings['module'] = 'taxonomy_term'; - $settings['token_type'] = 'term'; - $settings['groupheader'] = t('Taxonomy term paths'); - $settings['patterndescr'] = t('Default path pattern (applies to all vocabularies with blank patterns below)'); - $settings['patterndefault'] = '[term:vocabulary]/[term:name]'; - $settings['batch_update_callback'] = 'taxonomy_pathauto_bulk_update_batch_process'; - $settings['batch_file'] = drupal_get_path('module', 'pathauto') . '/pathauto.pathauto.inc'; - - $vocabularies = entity_load_multiple('taxonomy_vocabulary'); - if (count($vocabularies)) { - $settings['patternitems'] = array(); - foreach ($vocabularies as $vid => $vocabulary) { - /*if ($vid == variable_get('forum_nav_vocabulary', '')) { - // Skip the forum vocabulary. - continue; - }*/ - - $settings['patternitems'][$vocabulary->id()] = t('Pattern for all %vocab-name paths', array('%vocab-name' => $vocabulary->label())); - } - } - return (object) $settings; - - default: - break; - } -} - -/** - * Batch processing callback; Generate aliases for taxonomy terms. - */ -function taxonomy_pathauto_bulk_update_batch_process(&$context) { - if (!isset($context['sandbox']['current'])) { - $context['sandbox']['count'] = 0; - $context['sandbox']['current'] = 0; - } - - $query = db_select('taxonomy_term_data', 'td'); - $query->leftJoin('url_alias', 'ua', "CONCAT('taxonomy/term/', td.tid) = ua.source"); - $query->addField('td', 'tid'); - $query->isNull('ua.source'); - $query->condition('td.tid', $context['sandbox']['current'], '>'); - // Exclude the forums terms. - if ($forum_vid = 'forums') { - $query->condition('td.vid', $forum_vid, '<>'); - } - $query->orderBy('td.tid'); - $query->addTag('pathauto_bulk_update'); - $query->addMetaData('entity', 'taxonomy_term'); - - // Get the total amount of items to process. - if (!isset($context['sandbox']['total'])) { - $context['sandbox']['total'] = $query->countQuery()->execute()->fetchField(); - - // If there are no nodes to update, the stop immediately. - if (!$context['sandbox']['total']) { - $context['finished'] = 1; - return; - } - } - - $query->range(0, 25); - $tids = $query->execute()->fetchCol(); - - pathauto_taxonomy_term_update_alias_multiple($tids, 'bulkupdate'); - $context['sandbox']['count'] += count($tids); - $context['sandbox']['current'] = max($tids); - $context['message'] = t('Updated alias for term @tid.', array('@tid' => end($tids))); - - if ($context['sandbox']['count'] != $context['sandbox']['total']) { - $context['finished'] = $context['sandbox']['count'] / $context['sandbox']['total']; - } -} - -/** - * Implements hook_pathauto() for forum module. - */ -function forum_pathauto($op) { - switch ($op) { - case 'settings': - $settings = array(); - $settings['module'] = 'forum'; - $settings['token_type'] = 'term'; - $settings['groupheader'] = t('Forum paths'); - $settings['patterndescr'] = t('Pattern for forums and forum containers'); - $settings['patterndefault'] = '[term:vocabulary]/[term:name]'; - $settings['batch_update_callback'] = 'forum_pathauto_bulk_update_batch_process'; - $settings['batch_file'] = drupal_get_path('module', 'pathauto') . '/pathauto.pathauto.inc'; - return (object) $settings; - - default: - break; - } -} - -/** - * Batch processing callback; Generate aliases for forums. - */ -function forum_pathauto_bulk_update_batch_process(&$context) { - if (!isset($context['sandbox']['current'])) { - $context['sandbox']['count'] = 0; - $context['sandbox']['current'] = 0; - } - - $query = db_select('taxonomy_term_data', 'td'); - $query->leftJoin('url_alias', 'ua', "CONCAT('forum/', td.tid) = ua.source"); - $query->addField('td', 'tid'); - $query->isNull('ua.source'); - $query->condition('td.tid', $context['sandbox']['current'], '>'); - $query->condition('td.vid', 'forums'); - $query->orderBy('td.tid'); - $query->addTag('pathauto_bulk_update'); - $query->addMetaData('entity', 'taxonomy_term'); - - // Get the total amount of items to process. - if (!isset($context['sandbox']['total'])) { - $context['sandbox']['total'] = $query->countQuery()->execute()->fetchField(); - - // If there are no nodes to update, the stop immediately. - if (!$context['sandbox']['total']) { - $context['finished'] = 1; - return; - } - } - - $query->range(0, 25); - $tids = $query->execute()->fetchCol(); - - pathauto_taxonomy_term_update_alias_multiple($tids, 'bulkupdate'); - $context['sandbox']['count'] += count($tids); - $context['sandbox']['current'] = max($tids); - $context['message'] = t('Updated alias for forum @tid.', array('@tid' => end($tids))); - - if ($context['sandbox']['count'] != $context['sandbox']['total']) { - $context['finished'] = $context['sandbox']['count'] / $context['sandbox']['total']; - } -} - -/** - * Implements hook_pathauto(). - */ -function user_pathauto($op) { - switch ($op) { - case 'settings': - $settings = array(); - $settings['module'] = 'user'; - $settings['token_type'] = 'user'; - $settings['groupheader'] = t('User paths'); - $settings['patterndescr'] = t('Pattern for user account page paths'); - $settings['patterndefault'] = 'users/[user:name]'; - $settings['batch_update_callback'] = 'user_pathauto_bulk_update_batch_process'; - $settings['batch_file'] = drupal_get_path('module', 'pathauto') . '/pathauto.pathauto.inc'; - return (object) $settings; - - default: - break; - } -} - -/** - * Batch processing callback; Generate aliases for users. - */ -function user_pathauto_bulk_update_batch_process(&$context) { - if (!isset($context['sandbox']['current'])) { - $context['sandbox']['count'] = 0; - $context['sandbox']['current'] = 0; - } - - $query = db_select('users', 'u'); - $query->leftJoin('url_alias', 'ua', "CONCAT('user/', u.uid) = ua.source"); - $query->addField('u', 'uid'); - $query->isNull('ua.source'); - $query->condition('u.uid', $context['sandbox']['current'], '>'); - $query->orderBy('u.uid'); - $query->addTag('pathauto_bulk_update'); - $query->addMetaData('entity', 'user'); - - // Get the total amount of items to process. - if (!isset($context['sandbox']['total'])) { - $context['sandbox']['total'] = $query->countQuery()->execute()->fetchField(); - - // If there are no nodes to update, the stop immediately. - if (!$context['sandbox']['total']) { - $context['finished'] = 1; - return; - } - } - - $query->range(0, 25); - $uids = $query->execute()->fetchCol(); - - pathauto_user_update_alias_multiple($uids, 'bulkupdate', array()); - $context['sandbox']['count'] += count($uids); - $context['sandbox']['current'] = max($uids); - $context['message'] = t('Updated alias for user @uid.', array('@uid' => end($uids))); - - if ($context['sandbox']['count'] != $context['sandbox']['total']) { - $context['finished'] = $context['sandbox']['count'] / $context['sandbox']['total']; - } -} diff --git a/pathauto.routing.yml b/pathauto.routing.yml index 92c1efe..0902381 100644 --- a/pathauto.routing.yml +++ b/pathauto.routing.yml @@ -1,13 +1,22 @@ -pathauto.patterns.form: - path: 'admin/config/search/path/patterns' +entity.pathauto_pattern.collection: + path: '/admin/config/search/path/patterns' defaults: - _form: '\Drupal\pathauto\Form\PathautoPatternsForm' + _entity_list: 'pathauto_pattern' _title: 'Patterns' requirements: _permission: 'administer pathauto' +entity.pathauto_pattern.add_form: + path: '/admin/config/search/path/patterns/add' + defaults: + _entity_form: 'pathauto_pattern.default' + _title: 'Add Pathauto pattern' + tempstore_id: 'pathauto.pattern' + requirements: + _permission: 'administer pathauto' + pathauto.settings.form: - path: 'admin/config/search/path/settings' + path: '/admin/config/search/path/settings' defaults: _form: '\Drupal\pathauto\Form\PathautoSettingsForm' _title: 'Settings' @@ -15,7 +24,7 @@ pathauto.settings.form: _permission: 'administer pathauto' pathauto.bulk.update.form: - path: 'admin/config/search/path/update_bulk' + path: '/admin/config/search/path/update_bulk' defaults: _form: '\Drupal\pathauto\Form\PathautoBulkUpdateForm' _title: 'Bulk generate' @@ -23,9 +32,9 @@ pathauto.bulk.update.form: _permission: 'administer url aliases' pathauto.admin.delete: - path: 'admin/config/search/path/delete_bulk' + path: '/admin/config/search/path/delete_bulk' defaults: _form: '\Drupal\pathauto\Form\PathautoAdminDelete' _title: 'Delete aliases' requirements: - _permission: 'administer url aliases' \ No newline at end of file + _permission: 'administer url aliases' diff --git a/pathauto.services.yml b/pathauto.services.yml index 5e244db..748de19 100644 --- a/pathauto.services.yml +++ b/pathauto.services.yml @@ -1,16 +1,16 @@ services: - pathauto.manager: - class: Drupal\pathauto\PathautoManager - arguments: ['@config.factory', '@language_manager', '@cache.default', '@module_handler', '@token', '@pathauto.alias_cleaner', '@pathauto.alias_storage_helper', '@pathauto.alias_uniquifier', ,'@pathauto.verbose_messenger', '@string_translation'] + pathauto.generator: + class: Drupal\pathauto\PathautoGenerator + arguments: ['@config.factory', '@module_handler', '@token', '@pathauto.alias_cleaner', '@pathauto.alias_storage_helper', '@pathauto.alias_uniquifier', '@pathauto.verbose_messenger', '@string_translation', '@token.entity_mapper'] pathauto.alias_cleaner: class: Drupal\pathauto\AliasCleaner - arguments: ['@config.factory', '@pathauto.alias_storage_helper'] + arguments: ['@config.factory', '@pathauto.alias_storage_helper', '@language_manager', '@cache.discovery', '@transliteration', '@module_handler'] pathauto.alias_storage_helper: class: Drupal\pathauto\AliasStorageHelper arguments: ['@config.factory', '@path.alias_storage', '@database','@pathauto.verbose_messenger', '@string_translation'] pathauto.alias_uniquifier: class: Drupal\pathauto\AliasUniquifier - arguments: ['@config.factory', '@pathauto.alias_storage_helper','@module_handler', '@router.no_access_checks'] + arguments: ['@config.factory', '@pathauto.alias_storage_helper','@module_handler', '@router.no_access_checks', '@path.alias_manager'] pathauto.verbose_messenger: class: Drupal\pathauto\VerboseMessenger arguments: ['@config.factory', '@current_user'] diff --git a/pathauto.tokens.inc b/pathauto.tokens.inc index 5660c67..32a2b09 100644 --- a/pathauto.tokens.inc +++ b/pathauto.tokens.inc @@ -6,6 +6,7 @@ */ use Drupal\Core\Render\Element; +use Drupal\Core\Render\BubbleableMetadata; /** * Implements hook_token_info(). @@ -24,7 +25,7 @@ function pathauto_token_info() { /** * Implements hook_tokens(). */ -function pathauto_tokens($type, $tokens, array $data = array(), array $options = array()) { +function pathauto_tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) { $replacements = array(); if ($type == 'array' && !empty($data['array'])) { @@ -37,7 +38,7 @@ function pathauto_tokens($type, $tokens, array $data = array(), array $options = $values = array(); foreach (token_element_children($array) as $key) { $value = is_array($array[$key]) ? render($array[$key]) : (string) $array[$key]; - $value = \Drupal::service('pathauto.manager')->cleanString($value, $options); + $value = \Drupal::service('pathauto.alias_cleaner')->cleanString($value, $options); $values[] = $value; } $replacements[$original] = implode('/', $values); diff --git a/src/AliasCleaner.php b/src/AliasCleaner.php index b1ab5cf..c5df05c 100644 --- a/src/AliasCleaner.php +++ b/src/AliasCleaner.php @@ -7,8 +7,14 @@ namespace Drupal\pathauto; +use Drupal\Component\Render\PlainTextOutput; +use Drupal\Component\Transliteration\TransliterationInterface; +use Drupal\Component\Utility\Html; use Drupal\Component\Utility\Unicode; +use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\Language\LanguageManagerInterface; /** * Provides an alias cleaner. @@ -30,11 +36,41 @@ class AliasCleaner implements AliasCleanerInterface { protected $aliasStorageHelper; /** - * Alias max length. + * Language manager. * - * @var int + * @var \Drupal\Core\Language\LanguageManagerInterface */ - protected $aliasMaxLength; + protected $languageManager; + + /** + * Cache backend. + * + * @var \Drupal\Core\Cache\CacheBackendInterface + */ + protected $cacheBackend; + + /** + * Calculated settings cache. + * + * @todo Split this up into separate properties. + * + * @var array + */ + protected $cleanStringCache = array(); + + /** + * Transliteration service. + * + * @var \Drupal\Component\Transliteration\TransliterationInterface + */ + protected $transliteration; + + /** + * The module handler. + * + * @var \Drupal\Core\Extension\ModuleHandlerInterface + */ + protected $moduleHandler; /** * Creates a new AliasCleaner. @@ -43,20 +79,30 @@ class AliasCleaner implements AliasCleanerInterface { * The config factory. * @param \Drupal\pathauto\AliasStorageHelperInterface $alias_storage_helper * The alias storage helper. + * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager + * The language manager. + * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend + * The cache backend. + * @param \Drupal\Component\Transliteration\TransliterationInterface $transliteration + * The transliteration service. + * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler + * The module handler. */ - public function __construct(ConfigFactoryInterface $config_factory, AliasStorageHelperInterface $alias_storage_helper) { + public function __construct(ConfigFactoryInterface $config_factory, AliasStorageHelperInterface $alias_storage_helper, LanguageManagerInterface $language_manager, CacheBackendInterface $cache_backend, TransliterationInterface $transliteration, ModuleHandlerInterface $module_handler) { $this->configFactory = $config_factory; $this->aliasStorageHelper = $alias_storage_helper; + $this->languageManager = $language_manager; + $this->cacheBackend = $cache_backend; + $this->transliteration = $transliteration; + $this->moduleHandler = $module_handler; } /** * {@inheritdoc} */ public function cleanAlias($alias) { - if (!isset($this->aliasMaxLength)) { - $config = $this->configFactory->get('pathauto.settings'); - $this->aliasMaxLength = min($config->get('max_length'), $this->aliasStorageHelper->getAliasSchemaMaxLength()); - } + $config = $this->configFactory->get('pathauto.settings'); + $alias_max_length = min($config->get('max_length'), $this->aliasStorageHelper->getAliasSchemaMaxLength()); $output = $alias; @@ -70,7 +116,7 @@ public function cleanAlias($alias) { $output = $this->getCleanSeparators($output, '/'); // Shorten to a logical place based on word boundaries. - $output = Unicode::truncate($output, $this->aliasMaxLength, TRUE); + $output = Unicode::truncate($output, $alias_max_length, TRUE); return $output; } @@ -101,9 +147,210 @@ public function getCleanSeparators($string, $separator = NULL) { if ($separator !== '/') { $output = preg_replace("/\/+$seppattern\/+|$seppattern\/+|\/+$seppattern/", "/", $output); } + else { + // If the separator is a slash, we need to re-add the leading slash + // dropped by the trim function. + $output = '/' . $output; + } } return $output; } + /** + * {@inheritdoc} + */ + public function cleanString($string, array $options = array()) { + if (empty($this->cleanStringCache)) { + // Generate and cache variables used in this method. + $config = $this->configFactory->get('pathauto.settings'); + $this->cleanStringCache = array( + 'separator' => $config->get('separator'), + 'strings' => array(), + 'transliterate' => $config->get('transliterate'), + 'punctuation' => array(), + 'reduce_ascii' => (bool) $config->get('reduce_ascii'), + 'ignore_words_regex' => FALSE, + 'lowercase' => (bool) $config->get('case'), + 'maxlength' => min($config->get('max_component_length'), $this->aliasStorageHelper->getAliasSchemaMaxLength()), + ); + + // Generate and cache the punctuation replacements for strtr(). + $punctuation = $this->getPunctuationCharacters(); + foreach ($punctuation as $name => $details) { + $action = $config->get('punctuation.' . $name); + switch ($action) { + case PathautoGeneratorInterface::PUNCTUATION_REMOVE: + $cache['punctuation'][$details['value']] = ''; + $this->cleanStringCache; + + case PathautoGeneratorInterface::PUNCTUATION_REPLACE: + $this->cleanStringCache['punctuation'][$details['value']] = $this->cleanStringCache['separator']; + break; + + case PathautoGeneratorInterface::PUNCTUATION_DO_NOTHING: + // Literally do nothing. + break; + } + } + + // Generate and cache the ignored words regular expression. + $ignore_words = $config->get('ignore_words'); + $ignore_words_regex = preg_replace(array('/^[,\s]+|[,\s]+$/', '/[,\s]+/'), array('', '\b|\b'), $ignore_words); + if ($ignore_words_regex) { + $this->cleanStringCache['ignore_words_regex'] = '\b' . $ignore_words_regex . '\b'; + if (function_exists('mb_eregi_replace')) { + mb_regex_encoding('UTF-8'); + $this->cleanStringCache['ignore_words_callback'] = 'mb_eregi_replace'; + } + else { + $this->cleanStringCache['ignore_words_callback'] = 'preg_replace'; + $this->cleanStringCache['ignore_words_regex'] = '/' . $this->cleanStringCache['ignore_words_regex'] . '/i'; + } + } + } + + // Empty strings do not need any processing. + if ($string === '' || $string === NULL) { + return ''; + } + + $langcode = NULL; + if (!empty($options['language'])) { + $langcode = $options['language']->getId(); + } + elseif (!empty($options['langcode'])) { + $langcode = $options['langcode']; + } + + // Check if the string has already been processed, and if so return the + // cached result. + if (isset($this->cleanStringCache['strings'][$langcode][(string) $string])) { + return $this->cleanStringCache['strings'][$langcode][(string) $string]; + } + + // Remove all HTML tags from the string. + $output = Html::decodeEntities($string); + $output = PlainTextOutput::renderFromHtml($output); + + // Optionally transliterate. + if ($this->cleanStringCache['transliterate']) { + // If the reduce strings to letters and numbers is enabled, don't bother + // replacing unknown characters with a question mark. Use an empty string + // instead. + $output = $this->transliteration->transliterate($output, $langcode, $this->cleanStringCache['reduce_ascii'] ? '' : '?'); + } + + // Replace or drop punctuation based on user settings. + $output = strtr($output, $this->cleanStringCache['punctuation']); + + // Reduce strings to letters and numbers. + if ($this->cleanStringCache['reduce_ascii']) { + $output = preg_replace('/[^a-zA-Z0-9\/]+/', $this->cleanStringCache['separator'], $output); + } + + // Get rid of words that are on the ignore list. + if ($this->cleanStringCache['ignore_words_regex']) { + $words_removed = $this->cleanStringCache['ignore_words_callback']($this->cleanStringCache['ignore_words_regex'], '', $output); + if (Unicode::strlen(trim($words_removed)) > 0) { + $output = $words_removed; + } + } + + // Always replace whitespace with the separator. + $output = preg_replace('/\s+/', $this->cleanStringCache['separator'], $output); + + // Trim duplicates and remove trailing and leading separators. + $output = $this->getCleanSeparators($this->getCleanSeparators($output, $this->cleanStringCache['separator'])); + + // Optionally convert to lower case. + if ($this->cleanStringCache['lowercase']) { + $output = Unicode::strtolower($output); + } + + // Shorten to a logical place based on word boundaries. + $output = Unicode::truncate($output, $this->cleanStringCache['maxlength'], TRUE); + + // Cache this result in the static array. + $this->cleanStringCache['strings'][$langcode][(string) $string] = $output; + + return $output; + } + + + /** + * {@inheritdoc} + */ + public function getPunctuationCharacters() { + if (empty($this->punctuationCharacters)) { + $langcode = $this->languageManager->getCurrentLanguage()->getId(); + + $cid = 'pathauto:punctuation:' . $langcode; + if ($cache = $this->cacheBackend->get($cid)) { + $this->punctuationCharacters = $cache->data; + } + else { + $punctuation = array(); + $punctuation['double_quotes'] = array('value' => '"', 'name' => t('Double quotation marks')); + $punctuation['quotes'] = array('value' => '\'', 'name' => t("Single quotation marks (apostrophe)")); + $punctuation['backtick'] = array('value' => '`', 'name' => t('Back tick')); + $punctuation['comma'] = array('value' => ',', 'name' => t('Comma')); + $punctuation['period'] = array('value' => '.', 'name' => t('Period')); + $punctuation['hyphen'] = array('value' => '-', 'name' => t('Hyphen')); + $punctuation['underscore'] = array('value' => '_', 'name' => t('Underscore')); + $punctuation['colon'] = array('value' => ':', 'name' => t('Colon')); + $punctuation['semicolon'] = array('value' => ';', 'name' => t('Semicolon')); + $punctuation['pipe'] = array('value' => '|', 'name' => t('Vertical bar (pipe)')); + $punctuation['left_curly'] = array('value' => '{', 'name' => t('Left curly bracket')); + $punctuation['left_square'] = array('value' => '[', 'name' => t('Left square bracket')); + $punctuation['right_curly'] = array('value' => '}', 'name' => t('Right curly bracket')); + $punctuation['right_square'] = array('value' => ']', 'name' => t('Right square bracket')); + $punctuation['plus'] = array('value' => '+', 'name' => t('Plus sign')); + $punctuation['equal'] = array('value' => '=', 'name' => t('Equal sign')); + $punctuation['asterisk'] = array('value' => '*', 'name' => t('Asterisk')); + $punctuation['ampersand'] = array('value' => '&', 'name' => t('Ampersand')); + $punctuation['percent'] = array('value' => '%', 'name' => t('Percent sign')); + $punctuation['caret'] = array('value' => '^', 'name' => t('Caret')); + $punctuation['dollar'] = array('value' => '$', 'name' => t('Dollar sign')); + $punctuation['hash'] = array('value' => '#', 'name' => t('Number sign (pound sign, hash)')); + $punctuation['at'] = array('value' => '@', 'name' => t('At sign')); + $punctuation['exclamation'] = array('value' => '!', 'name' => t('Exclamation mark')); + $punctuation['tilde'] = array('value' => '~', 'name' => t('Tilde')); + $punctuation['left_parenthesis'] = array('value' => '(', 'name' => t('Left parenthesis')); + $punctuation['right_parenthesis'] = array('value' => ')', 'name' => t('Right parenthesis')); + $punctuation['question_mark'] = array('value' => '?', 'name' => t('Question mark')); + $punctuation['less_than'] = array('value' => '<', 'name' => t('Less-than sign')); + $punctuation['greater_than'] = array('value' => '>', 'name' => t('Greater-than sign')); + $punctuation['slash'] = array('value' => '/', 'name' => t('Slash')); + $punctuation['back_slash'] = array('value' => '\\', 'name' => t('Backslash')); + + // Allow modules to alter the punctuation list and cache the result. + $this->moduleHandler->alter('pathauto_punctuation_chars', $punctuation); + $this->cacheBackend->set($cid, $punctuation); + $this->punctuationCharacters = $punctuation; + } + } + + return $this->punctuationCharacters; + } + + /** + * {@inheritdoc} + */ + public function cleanTokenValues(&$replacements, $data = array(), $options = array()) { + foreach ($replacements as $token => $value) { + // Only clean non-path tokens. + if (!preg_match('/(path|alias|url|url-brief)\]$/', $token)) { + $replacements[$token] = $this->cleanString($value, $options); + } + } + } + + /** + * {@inheritdoc} + */ + public function resetCaches() { + $this->cleanStringCache = array(); + } + } diff --git a/src/AliasCleanerInterface.php b/src/AliasCleanerInterface.php index 3ffed9a..7c90c1e 100644 --- a/src/AliasCleanerInterface.php +++ b/src/AliasCleanerInterface.php @@ -42,4 +42,65 @@ public function cleanAlias($alias); * @see pathauto_clean_alias() */ public function getCleanSeparators($string, $separator = NULL); + + /** + * Clean up a string segment to be used in an URL alias. + * + * Performs the following possible alterations: + * - Remove all HTML tags. + * - Process the string through the transliteration module. + * - Replace or remove punctuation with the separator character. + * - Remove back-slashes. + * - Replace non-ascii and non-numeric characters with the separator. + * - Remove common words. + * - Replace whitespace with the separator character. + * - Trim duplicate, leading, and trailing separators. + * - Convert to lower-case. + * - Shorten to a desired length and logical position based on word boundaries. + * + * This function should *not* be called on URL alias or path strings + * because it is assumed that they are already clean. + * + * @param string $string + * A string to clean. + * @param array $options + * (optional) A keyed array of settings and flags to control the Pathauto + * clean string replacement process. Supported options are: + * - langcode: A language code to be used when translating strings. + * + * @return string + * The cleaned string. + */ + public function cleanString($string, array $options = array()); + + /** + * Return an array of arrays for punctuation values. + * + * Returns an array of arrays for punctuation values keyed by a name, including + * the value and a textual description. + * Can and should be expanded to include "all" non text punctuation values. + * + * @return array + * An array of arrays for punctuation values keyed by a name, including the + * value and a textual description. + */ + public function getPunctuationCharacters(); + + /** + * Clean tokens so they are URL friendly. + * + * @param array $replacements + * An array of token replacements + * that need to be "cleaned" for use in the URL. + * @param array $data + * An array of objects used to generate the replacements. + * @param array $options + * An array of options used to generate the replacements. + */ + public function cleanTokenValues(&$replacements, $data = array(), $options = array()); + + /** + * Resets internal caches. + */ + public function resetCaches(); } diff --git a/src/AliasStorageHelper.php b/src/AliasStorageHelper.php index 2c4f1f1..593c067 100644 --- a/src/AliasStorageHelper.php +++ b/src/AliasStorageHelper.php @@ -9,6 +9,7 @@ use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Database\Connection; +use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Language\LanguageInterface; use Drupal\Core\Path\AliasStorageInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; @@ -109,16 +110,16 @@ public function save(array $path, $existing_alias = NULL, $op = NULL) { // If there is already an alias, respect some update actions. if (!empty($existing_alias)) { switch ($config->get('update_action')) { - case PathautoManagerInterface::UPDATE_ACTION_NO_NEW: + case PathautoGeneratorInterface::UPDATE_ACTION_NO_NEW: // Do not create the alias. return NULL; - case PathautoManagerInterface::UPDATE_ACTION_LEAVE: + case PathautoGeneratorInterface::UPDATE_ACTION_LEAVE: // Create a new alias instead of overwriting the existing by leaving // $path['pid'] empty. break; - case PathautoManagerInterface::UPDATE_ACTION_DELETE: + case PathautoGeneratorInterface::UPDATE_ACTION_DELETE: // The delete actions should overwrite the existing alias. $path['pid'] = $existing_alias['pid']; break; @@ -175,4 +176,42 @@ public function exists($alias, $source, $language = LanguageInterface::LANGCODE_ ))->fetchField(); } + /** + * {@inheritdoc} + */ + public function deleteAll($source) { + $pids = $this->loadBySourcePrefix($source); + if ($pids) { + $this->deleteMultiple($pids); + } + } + + /** + * {@inheritdoc} + */ + public function deleteEntityPathAll(EntityInterface $entity, $default_uri = NULL) { + $this->deleteAll('/'. $entity->urlInfo()->getInternalPath()); + if (isset($default_uri) && $entity->urlInfo()->toString() != $default_uri) { + $this->deleteAll($default_uri); + } + } + + /** + * {@inheritdoc} + */ + public function deleteMultiple($pids) { + foreach ($pids as $pid) { + $this->aliasStorage->delete(array('pid' => $pid)); + } + } + + /** + * {@inheritdoc} + */ + public function loadBySourcePrefix($source) { + return $this->database->query("SELECT pid FROM {url_alias} WHERE source = :source OR source LIKE :source_wildcard", + [':source' => $source, ':source_wildcard' => $source . '/%']) + ->fetchCol(); + } + } diff --git a/src/AliasStorageHelperInterface.php b/src/AliasStorageHelperInterface.php index 3ad7460..e4d9c6d 100644 --- a/src/AliasStorageHelperInterface.php +++ b/src/AliasStorageHelperInterface.php @@ -6,6 +6,7 @@ */ namespace Drupal\pathauto; +use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Language\LanguageInterface; /** @@ -71,4 +72,48 @@ public function loadBySource($source, $language = LanguageInterface::LANGCODE_NO */ public function exists($alias, $source, $language = LanguageInterface::LANGCODE_NOT_SPECIFIED); + /** + * Delete all aliases by source url. + * + * Can use wildcard patterns, e.g. + * + * @param string $source + * The URL alias source. + */ + public function deleteAll($source); + + /** + * Delete an entity URL alias and any of its sub-paths. + * + * This function also checks to see if the default entity URI is different + * from the current entity URI and will delete any of the default aliases. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * An entity object. + * @param string $default_uri + * The optional default uri path for the entity. + */ + public function deleteEntityPathAll(EntityInterface $entity, $default_uri = NULL); + + /** + * Delete multiple URL aliases. + * + * Intent of this is to abstract a potential path_delete_multiple() function + * for Drupal 7 or 8. + * + * @param integer[] $pids + * An array of path IDs to delete. + */ + public function deleteMultiple($pids); + + /** + * Fetches an existing URL alias given a path prefix. + * + * @param string $source + * An internal Drupal path prefix. + * + * @return integer[] + * An array of PIDs. + */ + public function loadBySourcePrefix($source); } diff --git a/src/AliasTypeBatchUpdateInterface.php b/src/AliasTypeBatchUpdateInterface.php new file mode 100644 index 0000000..7d64d96 --- /dev/null +++ b/src/AliasTypeBatchUpdateInterface.php @@ -0,0 +1,22 @@ +setCacheBackend($cache_backend, 'pathauto_alias_types'); } + /** + * Returns plugin definitions that support a given token type. + * + * @param string $type + * The type of token plugin must support to be useful. + * + * @return array + * Plugin definitions. + */ + public function getPluginDefinitionByType($type) { + $definitions = array_filter($this->getDefinitions(), function ($definition) use ($type) { + if (!empty($definition['types']) && in_array($type, $definition['types'])) { + return TRUE; + } + return FALSE; + }); + return $definitions; + } + } diff --git a/src/AliasUniquifier.php b/src/AliasUniquifier.php index 09eaac7..c430f0a 100644 --- a/src/AliasUniquifier.php +++ b/src/AliasUniquifier.php @@ -11,6 +11,8 @@ use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Language\LanguageInterface; +use Drupal\Core\Path\AliasManagerInterface; +use Drupal\Core\Path\AliasStorageInterface; use Symfony\Component\Routing\Exception\ResourceNotFoundException; use Symfony\Component\Routing\Matcher\UrlMatcherInterface; @@ -47,6 +49,22 @@ class AliasUniquifier implements AliasUniquifierInterface { */ protected $urlMatcher; + /** + * The alias manager. + * + * @var \Drupal\Core\Path\AliasManagerInterface + */ + protected $aliasManager; + + /** + * Stores the last matching route name. + * + * Used to prevent a loop if the same route matches a given pattern. + * + * @var + */ + protected $lastRouteName; + /** * Creates a new AliasUniquifier. * @@ -59,11 +77,12 @@ class AliasUniquifier implements AliasUniquifierInterface { * @param \Symfony\Component\Routing\Matcher\UrlMatcherInterface $url_matcher * The url matcher service. */ - public function __construct(ConfigFactoryInterface $config_factory, AliasStorageHelperInterface $alias_storage_helper, ModuleHandlerInterface $module_handler, UrlMatcherInterface $url_matcher) { + public function __construct(ConfigFactoryInterface $config_factory, AliasStorageHelperInterface $alias_storage_helper, ModuleHandlerInterface $module_handler, UrlMatcherInterface $url_matcher, AliasManagerInterface $alias_manager) { $this->configFactory = $config_factory; $this->aliasStorageHelper = $alias_storage_helper; $this->moduleHandler = $module_handler; $this->urlMatcher = $url_matcher; + $this->aliasManager = $alias_manager; } /** @@ -85,7 +104,7 @@ public function uniquify(&$alias, $source, $langcode) { do { // Append an incrementing numeric suffix until we find a unique alias. $unique_suffix = $separator . $i; - $alias = Unicode::truncate($original_alias, $maxlength - Unicode::strlen($unique_suffix, TRUE)) . $unique_suffix; + $alias = Unicode::truncate($original_alias, $maxlength - Unicode::strlen($unique_suffix), TRUE) . $unique_suffix; $i++; } while ($this->isReserved($alias, $source, $langcode)); } @@ -94,10 +113,16 @@ public function uniquify(&$alias, $source, $langcode) { * {@inheritdoc} */ public function isReserved($alias, $source, $langcode = LanguageInterface::LANGCODE_NOT_SPECIFIED) { - // First check whether the alias exists for another source. - if ($this->aliasStorageHelper->exists($alias, $source, $langcode)) { - return TRUE; + // Check if this alias already exists. + if ($existing_source = $this->aliasManager->getPathByAlias($alias, $langcode)) { + if ($existing_source != $alias) { + // If it is an alias for the provided source, it is allowed to keep using + // it. If not, then it is reserved. + return $existing_source != $source; + } + } + // Then check if there is a route with the same path. if ($this->isRoute($alias)) { return TRUE; @@ -126,13 +151,13 @@ public function isReserved($alias, $source, $langcode = LanguageInterface::LANGC /** * Verify if the given path is a valid route. * - * Taken from menu_execute_active_handler(). - * * @param string $path * A string containing a relative path. * * @return bool * TRUE if the path already exists. + * + * @throws \InvalidArgumentException */ public function isRoute($path) { if (is_file(DRUPAL_ROOT . '/' . $path) || is_dir(DRUPAL_ROOT . '/' . $path)) { @@ -143,10 +168,17 @@ public function isRoute($path) { } try { - $this->urlMatcher->match('/' . $path); + $route = $this->urlMatcher->match($path); + + if ($route['_route'] == $this->lastRouteName) { + throw new \InvalidArgumentException('The given alias pattern (' . $path . ') always matches the route ' . $this->lastRouteName); + } + + $this->lastRouteName = $route['_route']; return TRUE; } catch (ResourceNotFoundException $e) { + $this->lastRouteName = NULL; return FALSE; } } diff --git a/src/Entity/PathautoPattern.php b/src/Entity/PathautoPattern.php new file mode 100644 index 0000000..f7619e8 --- /dev/null +++ b/src/Entity/PathautoPattern.php @@ -0,0 +1,404 @@ +getSelectionConditions() as $id => $condition) { + $criteria[$id] = $condition->getConfiguration(); + } + $this->selection_criteria = $criteria; + + /** @var \Drupal\Core\Plugin\Context\ContextInterface[] $contexts */ + $contexts = $this->getContexts(); + foreach ($this->getAliasType()->getContexts() as $plugin_context_id => $plugin_context) { + unset($contexts[$plugin_context_id]); + } + $this->context_definitions = []; + foreach ($contexts as $context_id => $context) { + $this->context_definitions[] = [ + 'id' => $context_id, + 'label' => $context->getContextDefinition()->getLabel() + ]; + } + + // Invalidate the static caches. + \Drupal::service('pathauto.generator')->resetCaches(); + } + + /** + * {@inheritdoc} + */ + public static function postLoad(EntityStorageInterface $storage, array &$entities) { + /** @var \Drupal\ctools\TypedDataResolver $resolver */ + $resolver = \Drupal::service('ctools.typed_data.resolver'); + /** @var \Drupal\pathauto\Entity\PathautoPattern $entity */ + foreach ($entities as $entity) { + foreach ($entity->getContextDefinitions() as $definition) { + $context = $resolver->convertTokenToContext($definition['id'], $entity->getContexts()); + $new_definition = new ContextDefinition( + $context->getContextDefinition()->getDataType(), + $definition['label'], + $context->getContextDefinition()->isRequired(), + $context->getContextDefinition()->isMultiple(), + $context->getContextDefinition()->getDescription(), + $context->getContextDefinition()->getDefaultValue() + ); + $new_context = new Context($new_definition, $context->hasContextValue() ? $context->getContextValue() : NULL); + $entity->addContext($definition['id'], $new_context); + } + } + parent::postLoad($storage, $entities); + } + + /** + * {@inheritdoc} + */ + public static function postDelete(EntityStorageInterface $storage, array $entities) { + parent::postDelete($storage, $entities); + // Invalidate the static caches. + \Drupal::service('pathauto.generator')->resetCaches(); + } + + /** + * {@inheritdoc} + */ + public function calculateDependencies() { + parent::calculateDependencies(); + + $this->calculatePluginDependencies($this->getAliasType()); + + foreach ($this->getSelectionConditions() as $instance) { + $this->calculatePluginDependencies($instance); + } + + return $this->getDependencies(); + } + + /** + * {@inheritdoc} + */ + public function getPattern() { + return $this->pattern; + } + + /** + * {@inheritdoc} + */ + public function setPattern($pattern) { + $this->pattern = $pattern; + return $this; + } + + /** + * {@inheritdoc} + */ + public function getType() { + return $this->type; + } + + /** + * {@inheritdoc} + */ + public function getAliasType() { + if (!$this->aliasTypeCollection) { + $this->aliasTypeCollection = new DefaultSingleLazyPluginCollection(\Drupal::service('plugin.manager.alias_type'), $this->getType(), ['default' => $this->getPattern()]); + } + return $this->aliasTypeCollection->get($this->getType()); + } + + /** + * {@inheritdoc} + */ + public function getWeight() { + return $this->weight; + } + + /** + * {@inheritdoc} + */ + public function setWeight($weight) { + $this->weight = $weight; + return $this; + } + + /** + * {@inheritdoc} + */ + public function hasContext($token) { + $contexts = $this->getAliasType()->getContexts(); + $contexts += $this->contexts; + return !empty($contexts[$token]); + } + + /** + * {@inheritdoc} + */ + public function getContext($token) { + $contexts = $this->getAliasType()->getContexts(); + $contexts += $this->contexts; + return $contexts[$token]; + } + + /** + * {@inheritdoc} + */ + public function getContexts() { + $contexts = $this->getAliasType()->getContexts(); + $contexts += $this->contexts; + return $contexts; + } + + /** + * {@inheritdoc} + */ + public function addContext($token, ContextInterface $context) { + if (!$this->hasContext($token)) { + $this->contexts[$token] = $context; + } + return $this; + } + + /** + * {@inheritdoc} + */ + public function replaceContext($token, ContextInterface $context) { + if ($this->hasContext($token)) { + $this->contexts[$token] = $context; + } + return $this; + } + + /** + * {@inheritdoc} + */ + public function removeContext($token) { + if (isset($this->contexts[$token])) { + unset($this->contexts[$token]); + } + return $this; + } + + /** + * {@inheritdoc} + */ + public function getContextDefinitions() { + return $this->context_definitions; + } + + /** + * {@inheritdoc} + */ + public function getSelectionConditions() { + if (!$this->selectionConditionCollection) { + $this->selectionConditionCollection = new ConditionPluginCollection(\Drupal::service('plugin.manager.condition'), $this->get('selection_criteria')); + } + return $this->selectionConditionCollection; + } + + /** + * {@inheritdoc} + */ + public function addSelectionCondition(array $configuration) { + $configuration['uuid'] = $this->uuidGenerator()->generate(); + $this->getSelectionConditions()->addInstanceId($configuration['uuid'], $configuration); + return $configuration['uuid']; + } + + /** + * {@inheritdoc} + */ + public function getSelectionCondition($condition_id) { + return $this->getSelectionConditions()->get($condition_id); + } + + /** + * {@inheritdoc} + */ + public function removeSelectionCondition($condition_id) { + $this->getSelectionConditions()->removeInstanceId($condition_id); + return $this; + } + + /** + * {@inheritdoc} + */ + public function getSelectionLogic() { + return $this->selection_logic; + } + + /** + * {@inheritdoc} + */ + public function applies($object) { + if ($this->getAliasType()->applies($object)) { + $definitions = $this->getAliasType()->getContextDefinitions(); + if (count($definitions) > 1) { + throw new \Exception("Alias types do not support more than one context."); + } + $keys = array_keys($definitions); + // Set the context object on our Alias plugin before retrieving contexts. + $this->getAliasType()->setContextValue($keys[0], $object); + /** @var \Drupal\Core\Plugin\Context\ContextInterface[] $base_contexts */ + $base_contexts = $this->getAliasType()->getContexts(); + $contexts = []; + foreach ($base_contexts as $context_id => $base_context) { + $contexts[$context_id] = new Context($base_context->getContextDefinition(), $object); + } + /** @var \Drupal\ctools\TypedDataResolver $resolver */ + $resolver = \Drupal::service('ctools.typed_data.resolver'); + foreach ($this->getContexts() as $token => $context) { + $contexts[$token] = $resolver->convertTokenToContext($token, $contexts); + } + /** @var \Drupal\Core\Plugin\Context\ContextHandler $context_handler */ + $context_handler = \Drupal::service('context.handler'); + $conditions = $this->getSelectionConditions(); + foreach ($conditions as $condition) { + if ($condition instanceof ContextAwarePluginInterface) { + $context_handler->applyContextMapping($condition, $contexts); + } + $result = $condition->execute(); + if ($this->getSelectionLogic() == 'and' && !$result) { + return FALSE; + } + elseif ($this->getSelectionLogic() == 'or' && $result) { + return TRUE; + } + } + return TRUE; + } + return FALSE; + } + +} diff --git a/src/Form/PathautoAdminDelete.php b/src/Form/PathautoAdminDelete.php index 0df7747..77003d0 100644 --- a/src/Form/PathautoAdminDelete.php +++ b/src/Form/PathautoAdminDelete.php @@ -9,12 +9,40 @@ use Drupal\Core\Form\FormBase; use Drupal\Core\Form\FormStateInterface; +use Drupal\pathauto\AliasTypeManager; +use Symfony\Component\DependencyInjection\ContainerInterface; /** - * Configure file system settings for this site. + * Alias mass delete form. */ class PathautoAdminDelete extends FormBase { + /** + * The alias type manager. + * + * @var \Drupal\pathauto\AliasTypeManager + */ + protected $aliasTypeManager; + + /** + * Constructs a PathautoAdminDelete object. + * + * @param \Drupal\pathauto\AliasTypeManager $alias_type_manager + * The alias type manager. + */ + public function __construct(AliasTypeManager $alias_type_manager) { + $this->aliasTypeManager = $alias_type_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('plugin.manager.alias_type') + ); + } + /** * {@inheritdoc} */ @@ -26,14 +54,10 @@ public function getFormId() { * {@inheritdoc} */ public function buildForm(array $form, FormStateInterface $form_state) { - - $form = array(); - $form['delete'] = array( '#type' => 'fieldset', '#title' => t('Choose aliases to delete'), - '#collapsible' => FALSE, - '#collapsed' => FALSE, + '#tree' => TRUE, ); // First we do the "all" case. @@ -45,20 +69,18 @@ public function buildForm(array $form, FormStateInterface $form_state) { '#description' => t('Delete all aliases. Number of aliases which will be deleted: %count.', array('%count' => $total_count)), ); - // Next, iterate over an array of objects/alias types - // which can be deleted and provide checkboxes. - $args = func_get_args(); - // Remove $hook from the arguments. - unset($args[0]); - $objects = \Drupal::moduleHandler()->invokeAll('path_alias_types', $args); + // Next, iterate over all alias types + $definitions = $this->aliasTypeManager->getDefinitions(); - foreach ($objects as $internal_name => $label) { - $count = db_query("SELECT count(1) FROM {url_alias} WHERE source LIKE :src", array(':src' => "$internal_name%"))->fetchField(); - $form['delete'][$internal_name] = array( + foreach ($definitions as $id => $definition) { + /** @var \Drupal\pathauto\AliasTypeInterface $alias_type */ + $alias_type = $this->aliasTypeManager->createInstance($id); + $count = db_query("SELECT count(1) FROM {url_alias} WHERE source LIKE :src", array(':src' => $alias_type->getSourcePrefix() . '%'))->fetchField(); + $form['delete']['plugins'][$id] = array( '#type' => 'checkbox', - '#title' => $label, // This label is sent through t() in the hard coded function where it is defined. + '#title' => (string) $definition['label'], '#default_value' => FALSE, - '#description' => t('Delete aliases for all @label. Number of aliases which will be deleted: %count.', array('@label' => $label, '%count' => $count)), + '#description' => t('Delete aliases for all @label. Number of aliases which will be deleted: %count.', array('@label' => (string) $definition['label'], '%count' => $count)), ); } @@ -76,26 +98,20 @@ public function buildForm(array $form, FormStateInterface $form_state) { * {@inheritdoc} */ public function submitForm(array &$form, FormStateInterface $form_state) { - foreach ($form_state->getValues() as $key => $value) { - if ($value) { - if ($key === 'all_aliases') { - db_delete('url_alias') - ->execute(); - drupal_set_message(t('All of your path aliases have been deleted.')); - } - $args = func_get_args(); - // Remove $hook from the arguments. - unset($args[0]); - $objects = \Drupal::moduleHandler()->invokeAll('path_alias_types', $args); - if (array_key_exists($key, $objects)) { - db_delete('url_alias') - ->condition('source', db_like($key) . '%', 'LIKE') - ->execute(); - drupal_set_message(t('All of your %type path aliases have been deleted.', array('%type' => $objects[$key]))); - } - } + if ($form_state->getValue(['delete', 'all_aliases'])) { + db_delete('url_alias') + ->execute(); + drupal_set_message($this->t('All of your path aliases have been deleted.')); + } + foreach (array_keys(array_filter($form_state->getValue(['delete', 'plugins']))) as $id) { + /** @var \Drupal\pathauto\AliasTypeInterface $alias_type */ + $alias_type = $this->aliasTypeManager->createInstance($id); + db_delete('url_alias') + ->condition('source', db_like($alias_type->getSourcePrefix()) . '%', 'LIKE') + ->execute(); + drupal_set_message(t('All of your %label path aliases have been deleted.', array('%label' => $alias_type->getLabel()))); } - $form_state->setRedirect('pathauto.bulk.update.form'); + $form_state->setRedirect('pathauto.admin.delete'); } } diff --git a/src/Form/PathautoBulkUpdateForm.php b/src/Form/PathautoBulkUpdateForm.php index 8b7cd25..706b18a 100644 --- a/src/Form/PathautoBulkUpdateForm.php +++ b/src/Form/PathautoBulkUpdateForm.php @@ -9,12 +9,41 @@ use Drupal\Core\Form\FormBase; use Drupal\Core\Form\FormStateInterface; +use Drupal\pathauto\AliasTypeBatchUpdateInterface; +use Drupal\pathauto\AliasTypeManager; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * Configure file system settings for this site. */ class PathautoBulkUpdateForm extends FormBase { + /** + * The alias type manager. + * + * @var \Drupal\pathauto\AliasTypeManager + */ + protected $aliasTypeManager; + + /** + * Constructs a PathautoBulkUpdateForm object. + * + * @param \Drupal\pathauto\AliasTypeManager $alias_type_manager + * The alias type manager. + */ + public function __construct(AliasTypeManager $alias_type_manager) { + $this->aliasTypeManager = $alias_type_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('plugin.manager.alias_type') + ); + } + /** * {@inheritdoc} */ @@ -38,12 +67,12 @@ public function buildForm(array $form, FormStateInterface $form_state) { '#default_value' => array(), ); - $pathauto_settings = \Drupal::moduleHandler()->invokeAll('pathauto', array('settings')); + $definitions = $this->aliasTypeManager->getDefinitions(); - foreach ($pathauto_settings as $settings) { - if (!empty($settings->batch_update_callback)) { - $form['#update_callbacks'][$settings->batch_update_callback] = $settings; - $form['update']['#options'][$settings->batch_update_callback] = $settings->groupheader; + foreach ($definitions as $id => $definition) { + $alias_type = $this->aliasTypeManager->createInstance($id); + if ($alias_type instanceof AliasTypeBatchUpdateInterface) { + $form['update']['#options'][$id] = $alias_type->getLabel(); } } @@ -68,15 +97,9 @@ public function submitForm(array &$form, FormStateInterface $form_state) { 'finished' => 'Drupal\pathauto\Form\PathautoBulkUpdateForm::batchFinished', ); - foreach ($form_state->getValue('update') as $callback) { - if (!empty($callback)) { - $settings = $form['#update_callbacks'][$callback]; - if (!empty($settings->batch_file)) { - $batch['operations'][] = array('Drupal\pathauto\Form\PathautoBulkUpdateForm::batchProcess', array($callback, $settings)); - } - else { - $batch['operations'][] = array($callback, array()); - } + foreach ($form_state->getValue('update') as $id) { + if (!empty($id)) { + $batch['operations'][] = array('Drupal\pathauto\Form\PathautoBulkUpdateForm::batchProcess', array($id)); } } @@ -95,11 +118,10 @@ public static function batchStart(&$context) { * * Required to load our include the proper batch file. */ - public static function batchProcess($callback, $settings, &$context) { - if (!empty($settings->batch_file)) { - require_once DRUPAL_ROOT . '/' . $settings->batch_file; - } - return $callback($context); + public static function batchProcess($id, &$context) { + /** @var \Drupal\pathauto\AliasTypeBatchUpdateInterface $alias_type */ + $alias_type = \Drupal::service('plugin.manager.alias_type')->createInstance($id); + $alias_type->batchUpdate($context); } /** diff --git a/src/Form/PathautoPatternsForm.php b/src/Form/PathautoPatternsForm.php deleted file mode 100644 index a00d683..0000000 --- a/src/Form/PathautoPatternsForm.php +++ /dev/null @@ -1,158 +0,0 @@ -aliasTypeManager = $alias_type_manager; - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container) { - return new static( - $container->get('config.factory'), - $container->get('plugin.manager.alias_type') - ); - } - - /** - * {@inheritdoc} - */ - public function getFormId() { - return 'pathauto_patterns_form'; - } - - /** - * {@inheritdoc} - */ - protected function getEditableConfigNames() { - return ['pathauto.pattern']; - } - - /** - * {@inheritdoc} - */ - public function buildForm(array $form, FormStateInterface $form_state) { - - $definitions = $this->aliasTypeManager->getDefinitions(); - - $config = $this->config('pathauto.pattern'); - - $all_settings = \Drupal::moduleHandler()->invokeAll('pathauto', array('settings')); - - foreach ($all_settings as $settings) { - $module = $settings->module; - $patterndescr = $settings->patterndescr; - $groupheader = $settings->groupheader; - - $form[$module] = array( - '#type' => 'fieldset', - '#title' => $groupheader, - '#collapsible' => TRUE, - '#collapsed' => FALSE, - '#tree' => TRUE, - ); - - // Prompt for the default pattern for this module. - $key = 'default'; - - $form[$module][$key] = array( - '#type' => 'textfield', - '#title' => $patterndescr, - '#default_value' => $config->get('patterns.' . $module . '.' . $key), - '#size' => 65, - '#maxlength' => 1280, - '#element_validate' => array('token_element_validate'), - '#after_build' => array('token_element_validate'), - '#token_types' => array($settings->token_type), - '#min_tokens' => 1, - ); - - // If the module supports a set of specialized patterns, set - // them up here. - if (isset($settings->patternitems)) { - foreach ($settings->patternitems as $itemname => $itemlabel) { - $key = 'default'; - - $form[$module]['bundles'][$itemname][$key] = array( - '#type' => 'textfield', - '#title' => $itemlabel, - '#default_value' => $config->get('patterns.'. $module . '.bundles.' . $itemname . '.' . $key), - '#size' => 65, - '#maxlength' => 1280, - '#element_validate' => array('token_element_validate'), - '#after_build' => array('token_element_validate'), - '#token_types' => array($settings->token_type), - '#min_tokens' => 1, - ); - } - } - - // Display the user documentation of placeholders supported by - // this module, as a description on the last pattern. - $form[$module]['token_help'] = array( - '#title' => t('Replacement patterns'), - '#type' => 'fieldset', - '#collapsible' => TRUE, - '#collapsed' => TRUE, - ); - $form[$module]['token_help']['help'] = array( - '#theme' => 'token_tree', - '#token_types' => array($settings->token_type), - ); - } - - return parent::buildForm($form, $form_state); - } - - /** - * {@inheritdoc} - */ - public function submitForm(array &$form, FormStateInterface $form_state) { - - $config = $this->config('pathauto.pattern'); - - $all_settings = \Drupal::moduleHandler()->invokeAll('pathauto', array('settings')); - - foreach ($all_settings as $settings) { - $module = $settings->module; - $config->set('patterns.' . $module, $form_state->getValue($module)); - } - - $config->save(); - - parent::submitForm($form, $form_state); - } - -} diff --git a/src/Form/PathautoSettingsForm.php b/src/Form/PathautoSettingsForm.php index 78647fe..edfb7dd 100644 --- a/src/Form/PathautoSettingsForm.php +++ b/src/Form/PathautoSettingsForm.php @@ -2,14 +2,15 @@ /** * @file - * Contains \Drupal\pathauto\Form\MaillogSettingsForm. + * Contains \Drupal\pathauto\Form\PathautoSettingsForm. */ namespace Drupal\pathauto\Form; use Drupal\Component\Utility\SafeMarkup; use Drupal\Core\Form\ConfigFormBase; -use Drupal\pathauto\PathautoManagerInterface; +use Drupal\Core\Url; +use Drupal\pathauto\PathautoGeneratorInterface; use Drupal\Core\Form\FormStateInterface; /** @@ -78,6 +79,11 @@ public function buildForm(array $form, FormStateInterface $form_state) { $max_length = \Drupal::service('pathauto.alias_storage_helper')->getAliasSchemaMaxlength(); + $help_link = ''; + if (\Drupal::moduleHandler()->moduleExists('help')) { + $help_link = ' ' . t('See Pathauto help for details.', [':pathauto-help' => Url::fromRoute('help.page', ['name' => 'pathauto'])->toString()]); + } + $form['max_length'] = array( '#type' => 'number', '#title' => t('Maximum alias length'), @@ -86,7 +92,7 @@ public function buildForm(array $form, FormStateInterface $form_state) { '#default_value' => $config->get('max_length'), '#min' => 1, '#max' => $max_length, - '#description' => t('Maximum length of aliases to generate. 100 is the recommended length. @max is the maximum possible length. See Pathauto help for details.', array('@pathauto-help' => $this->getUrlGenerator()->generateFromPath('admin/help/pathauto'), '@max' => $max_length)), + '#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, ); $form['max_component_length'] = array( @@ -97,15 +103,15 @@ public function buildForm(array $form, FormStateInterface $form_state) { '#default_value' => $config->get('max_component_length'), '#min' => 1, '#max' => $max_length, - '#description' => t('Maximum text length of any component in the alias (e.g., [title]). 100 is the recommended length. @max is the maximum possible length. See Pathauto help for details.', array('@pathauto-help' => $this->getUrlGenerator()->generateFromPath('admin/help/pathauto'), '@max' => $max_length)), + '#description' => t('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, ); $description = t('What should Pathauto do when updating an existing content item which already has an alias?'); if (\Drupal::moduleHandler()->moduleExists('redirect')) { - $description .= ' ' . t('The Redirect module settings affect whether a redirect is created when an alias is deleted.', array('!url' => \Drupal::url('redirect.settings'))); + $description .= ' ' . t('The Redirect module settings affect whether a redirect is created when an alias is deleted.', array(':url' => \Drupal::url('redirect.settings'))); } else { - $description .= ' ' . t('Considering installing the Redirect module to get redirects when your aliases change.', array('!url' => 'http://drupal.org/project/redirect')); + $description .= ' ' . t('Considering installing the Redirect module to get redirects when your aliases change.', array(':url' => 'http://drupal.org/project/redirect')); } $form['update_action'] = array( @@ -113,9 +119,9 @@ public function buildForm(array $form, FormStateInterface $form_state) { '#title' => t('Update action'), '#default_value' => $config->get('update_action'), '#options' => array( - PathautoManagerInterface::UPDATE_ACTION_NO_NEW => t('Do nothing. Leave the old alias intact.'), - PathautoManagerInterface::UPDATE_ACTION_LEAVE => t('Create a new alias. Leave the existing alias functioning.'), - PathautoManagerInterface::UPDATE_ACTION_DELETE => t('Create a new alias. Delete the old alias.'), + PathautoGeneratorInterface::UPDATE_ACTION_NO_NEW => t('Do nothing. Leave the old alias intact.'), + PathautoGeneratorInterface::UPDATE_ACTION_LEAVE => t('Create a new alias. Leave the existing alias functioning.'), + PathautoGeneratorInterface::UPDATE_ACTION_DELETE => t('Create a new alias. Delete the old alias.'), ), '#description' => $description, ); @@ -150,21 +156,25 @@ public function buildForm(array $form, FormStateInterface $form_state) { '#tree' => TRUE, ); - $punctuation = \Drupal::service('pathauto.manager')->getPunctuationCharacters(); + $punctuation = \Drupal::service('pathauto.alias_cleaner')->getPunctuationCharacters(); foreach ($punctuation as $name => $details) { - $details['default'] = PathautoManagerInterface::PUNCTUATION_REMOVE; - if ($details['value'] == $config->get('separator')) { - $details['default'] = PathautoManagerInterface::PUNCTUATION_REPLACE; + // Use the value from config if it exists. + if ($config->get('punctuation.' . $name) !== NULL) { + $details['default'] = $config->get('punctuation.' . $name) !== NULL; + } + else { + // Otherwise use the correct default. + $details['default'] = $details['value'] == $config->get('separator') ? PathautoGeneratorInterface::PUNCTUATION_REPLACE : PathautoGeneratorInterface::PUNCTUATION_REMOVE; } - $form['punctuation']['punctuation' . $name] = array( + $form['punctuation'][$name] = array( '#type' => 'select', '#title' => $details['name'] . ' (' . SafeMarkup::checkPlain($details['value']) . ')', '#default_value' => $details['default'], '#options' => array( - PathautoManagerInterface::PUNCTUATION_REMOVE => t('Remove'), - PathautoManagerInterface::PUNCTUATION_REPLACE => t('Replace by separator'), - PathautoManagerInterface::PUNCTUATION_DO_NOTHING => t('No action (do not replace)'), + PathautoGeneratorInterface::PUNCTUATION_REMOVE => t('Remove'), + PathautoGeneratorInterface::PUNCTUATION_REPLACE => t('Replace by separator'), + PathautoGeneratorInterface::PUNCTUATION_DO_NOTHING => t('No action (do not replace)'), ), ); } diff --git a/src/Form/PatternEditForm.php b/src/Form/PatternEditForm.php new file mode 100644 index 0000000..02615fa --- /dev/null +++ b/src/Form/PatternEditForm.php @@ -0,0 +1,283 @@ +get('plugin.manager.alias_type'), + $container->get('entity_type.bundle.info'), + $container->get('entity_type.manager'), + $container->get('language_manager') + ); + } + + /** + * PatternEditForm constructor. + * + * @param \Drupal\pathauto\AliasTypeManager $manager + * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager + */ + function __construct(AliasTypeManager $manager, EntityTypeBundleInfoInterface $entity_type_bundle_info, EntityTypeManagerInterface $entity_type_manager, LanguageManagerInterface $language_manager) { + $this->manager = $manager; + $this->entityTypeBundleInfo = $entity_type_bundle_info; + $this->entityTypeManager = $entity_type_manager; + $this->languageManager= $language_manager; + } + + /** + * {@inheritDoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + + $options = []; + foreach ($this->manager->getDefinitions() as $plugin_id => $plugin_definition) { + $options[$plugin_id] = $plugin_definition['label']; + } + $form['type'] = [ + '#type' => 'select', + '#title' => $this->t('Pattern type'), + '#default_value' => $this->entity->getType(), + '#options' => $options, + '#required' => TRUE, + '#limit_validation_errors' => array(array('type')), + '#submit' => array('::submitSelectType'), + '#executes_submit_callback' => TRUE, + '#ajax' => array( + 'callback' => '::ajaxReplacePatternForm', + 'wrapper' => 'pathauto-pattern', + 'method' => 'replace', + ), + ]; + + $form['pattern_container'] = [ + '#type' => 'container', + '#prefix' => '

', + '#suffix' => '
', + ]; + + // if there is no type yet, stop here. + if ($this->entity->getType()) { + + $alias_type = $this->entity->getAliasType(); + + $form['pattern_container']['pattern'] = array( + '#type' => 'textfield', + '#title' => 'Path pattern', + '#default_value' => $this->entity->getPattern(), + '#size' => 65, + '#maxlength' => 1280, + '#element_validate' => array('token_element_validate'), + '#after_build' => array('token_element_validate'), + '#token_types' => $alias_type->getTokenTypes(), + '#min_tokens' => 1, + ); + + // Show the token help relevant to this pattern type. + $form['pattern_container']['token_help'] = array( + '#theme' => 'token_tree_link', + '#token_types' => $alias_type->getTokenTypes(), + ); + + // Expose bundle and language conditions. + if ($alias_type->getDerivativeId() && $entity_type = $this->entityTypeManager->getDefinition($alias_type->getDerivativeId())) { + + $default_bundles = []; + $default_languages = []; + foreach ($this->entity->getSelectionConditions() as $condition_id => $condition) { + if ($condition->getPluginId() == 'entity_bundle:' . $entity_type->id()) { + $default_bundles = $condition->getConfiguration()['bundles']; + } + elseif ($condition->getPluginId() == 'language') { + $default_languages = $condition->getConfiguration()['langcodes']; + } + } + + if ($entity_type->hasKey('bundle') && $bundles = $this->entityTypeBundleInfo->getBundleInfo($entity_type->id())) { + $bundle_options = []; + foreach ($bundles as $id => $info) { + $bundle_options[$id] = $info['label']; + } + $form['pattern_container']['bundles'] = array( + '#title' => $entity_type->getBundleLabel(), + '#type' => 'checkboxes', + '#options' => $bundle_options, + '#default_value' => $default_bundles, + '#description' => t('Check to which types this pattern should be applied. Leave empty to allow any.'), + ); + } + + if ($this->languageManager->isMultilingual() && $entity_type->isTranslatable()) { + $language_options = []; + foreach ($this->languageManager->getLanguages() as $id => $language) { + $language_options[$id] = $language->getName(); + } + $form['pattern_container']['languages'] = array( + '#title' => $this->t('Languages'), + '#type' => 'checkboxes', + '#options' => $language_options, + '#default_value' => $default_languages, + '#description' => t('Check to which languages this pattern should be applied. Leave empty to allow any.'), + ); + } + } + } + + $form['label'] = array( + '#type' => 'textfield', + '#title' => $this->t('Label'), + '#maxlength' => 255, + '#default_value' => $this->entity->label(), + '#required' => TRUE, + ); + + $form['id'] = array( + '#type' => 'machine_name', + '#title' => $this->t('ID'), + '#maxlength' => 255, + '#default_value' => $this->entity->id(), + '#required' => TRUE, + '#disabled' => !$this->entity->isNew(), + '#machine_name' => array( + 'exists' => 'Drupal\pathauto\Entity\PathautoPattern::load', + ), + ); + + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritDoc} + */ + public function buildEntity(array $form, FormStateInterface $form_state) { + /** @var \Drupal\pathauto\PathautoPatternInterface $entity */ + $entity = parent::buildEntity($form, $form_state); + + $default_weight = 0; + + $alias_type = $entity->getAliasType(); + if ($alias_type->getDerivativeId() && $this->entityTypeManager->hasDefinition($alias_type->getDerivativeId())) { + $entity_type = $alias_type->getDerivativeId(); + // First, remove bundle and language conditions. + foreach ($entity->getSelectionConditions() as $condition_id => $condition) { + + if ($condition->getPluginId() == 'entity_bundle:' . $entity_type || $condition->getPluginId() == 'language') { + $entity->removeSelectionCondition($condition_id); + } + } + + if ($bundles = array_filter((array) $form_state->getValue('bundles'))) { + $default_weight -= 5; + $entity->addSelectionCondition( + [ + 'id' => 'entity_bundle:' . $entity_type, + 'bundles' => $bundles, + 'negate' => FALSE, + 'context_mapping' => [ + $entity_type => $entity_type, + ] + ] + ); + } + + if ($languages = array_filter((array) $form_state->getValue('languages'))) { + $default_weight -= 5; + $language_mapping = $entity_type . ':' . $this->entityTypeManager->getDefinition($entity_type)->getKey('langcode') . ':language'; + $entity->addSelectionCondition( + [ + 'id' => 'language', + 'langcodes' => array_combine($languages, $languages), + 'negate' => FALSE, + 'context_mapping' => [ + 'language' => $language_mapping, + ] + ] + ); + $new_definition = new ContextDefinition('language', 'Language'); + $new_context = new Context($new_definition); + $entity->addContext($language_mapping, $new_context); + } + + } + + $entity->setWeight($default_weight); + + return $entity; + } + + /** + * {@inheritDoc} + */ + public function save(array $form, FormStateInterface $form_state) { + parent::save($form, $form_state); + drupal_set_message($this->t('Pattern @label saved.', ['@label' => $this->entity->label()])); + $form_state->setRedirectUrl($this->entity->toUrl('collection')); + } + + /** + * Handles switching the type selector. + */ + public function ajaxReplacePatternForm($form, FormStateInterface $form_state) { + return $form['pattern_container']; + } + + /** + * Handles submit call when alias type is selected. + */ + public function submitSelectType(array $form, FormStateInterface $form_state) { + $this->entity = $this->buildEntity($form, $form_state); + $form_state->setRebuild(); + } + +} diff --git a/src/PathautoGenerator.php b/src/PathautoGenerator.php new file mode 100644 index 0000000..407070b --- /dev/null +++ b/src/PathautoGenerator.php @@ -0,0 +1,342 @@ +configFactory = $config_factory; + $this->moduleHandler = $module_handler; + $this->token = $token; + $this->aliasCleaner = $alias_cleaner; + $this->aliasStorageHelper = $alias_storage_helper; + $this->aliasUniquifier = $alias_uniquifier; + $this->messenger = $messenger; + $this->stringTranslation = $string_translation; + $this->tokenEntityMapper = $token_entity_mappper; + } + + /** + * {@inheritdoc} + */ + public function createEntityAlias(EntityInterface $entity, $op) { + // Retrieve and apply the pattern for this content type. + $pattern = $this->getPatternByEntity($entity); + if (empty($pattern)) { + // No pattern? Do nothing (otherwise we may blow away existing aliases...) + return NULL; + } + + $source = '/' . $entity->toUrl()->getInternalPath(); + $config = $this->configFactory->get('pathauto.settings'); + $langcode = $entity->language()->getId(); + + // Build token data. + $data = [ + $this->tokenEntityMapper->getTokenTypeForEntityType($entity->getEntityTypeId()) => $entity, + ]; + + // Allow other modules to alter the pattern. + $context = array( + 'module' => $entity->getEntityType()->getProvider(), + 'op' => $op, + 'source' => $source, + 'data' => $data, + 'bundle' => $entity->bundle(), + 'language' => &$langcode, + ); + // @todo Is still hook still useful? + $this->moduleHandler->alter('pathauto_pattern', $pattern, $context); + + // Special handling when updating an item which is already aliased. + $existing_alias = NULL; + if ($op == 'update' || $op == 'bulkupdate') { + if ($existing_alias = $this->aliasStorageHelper->loadBySource($source, $langcode)) { + switch ($config->get('update_action')) { + case PathautoGeneratorInterface::UPDATE_ACTION_NO_NEW: + // If an alias already exists, + // and the update action is set to do nothing, + // then gosh-darn it, do nothing. + return NULL; + } + } + } + + // Replace any tokens in the pattern. + // Uses callback option to clean replacements. No sanitization. + // Pass empty BubbleableMetadata object to explicitly ignore cacheablity, + // as the result is never rendered. + $alias = $this->token->replace($pattern->getPattern(), $data, array( + 'clear' => TRUE, + 'callback' => array($this->aliasCleaner, 'cleanTokenValues'), + 'langcode' => $langcode, + 'pathauto' => TRUE, + ), new BubbleableMetadata()); + + // Check if the token replacement has not actually replaced any values. If + // that is the case, then stop because we should not generate an alias. + // @see token_scan() + $pattern_tokens_removed = preg_replace('/\[[^\s\]:]*:[^\s\]]*\]/', '', $pattern->getPattern()); + if ($alias === $pattern_tokens_removed) { + return NULL; + } + + $alias = $this->aliasCleaner->cleanAlias($alias); + + // Allow other modules to alter the alias. + $context['source'] = &$source; + $context['pattern'] = $pattern; + $this->moduleHandler->alter('pathauto_alias', $alias, $context); + + // If we have arrived at an empty string, discontinue. + if (!Unicode::strlen($alias)) { + return NULL; + } + + // If the alias already exists, generate a new, hopefully unique, variant. + $original_alias = $alias; + $this->aliasUniquifier->uniquify($alias, $source, $langcode); + if ($original_alias != $alias) { + // Alert the user why this happened. + $this->messenger->addMessage($this->t('The automatically generated alias %original_alias conflicted with an existing alias. Alias changed to %alias.', array( + '%original_alias' => $original_alias, + '%alias' => $alias, + )), $op); + } + + // Return the generated alias if requested. + if ($op == 'return') { + return $alias; + } + + // Build the new path alias array and send it off to be created. + $path = array( + 'source' => $source, + 'alias' => $alias, + 'language' => $langcode, + ); + + return $this->aliasStorageHelper->save($path, $existing_alias, $op); + } + + /** + * Loads pathauto patterns for a given entity type ID + * + * @param string $entity_type_id + * An entity type ID. + * + * @return \Drupal\pathauto\PathautoPatternInterface[] + * A list of patterns, sorted by weight. + */ + protected function getPatternByEntityType($entity_type_id) { + if (!isset($this->patternsByEntityType[$entity_type_id])) { + $ids = \Drupal::entityQuery('pathauto_pattern') + ->condition('type', array_keys(\Drupal::service('plugin.manager.alias_type') + ->getPluginDefinitionByType($this->tokenEntityMapper->getTokenTypeForEntityType($entity_type_id)))) + ->sort('weight') + ->execute(); + + $this->patternsByEntityType[$entity_type_id] = \Drupal::entityTypeManager() + ->getStorage('pathauto_pattern') + ->loadMultiple($ids); + } + + return $this->patternsByEntityType[$entity_type_id]; + } + + /** + * {@inheritdoc} + */ + public function getPatternByEntity(EntityInterface $entity) { + $langcode = $entity->language()->getId(); + if (!isset($this->patterns[$entity->getEntityTypeId()][$entity->id()][$langcode])) { + foreach ($this->getPatternByEntityType($entity->getEntityTypeId()) as $pattern) { + if ($pattern->applies($entity)) { + $this->patterns[$entity->getEntityTypeId()][$entity->id()][$langcode] = $pattern; + break; + } + } + // If still not set. + if (!isset($this->patterns[$entity->getEntityTypeId()][$entity->id()][$langcode])) { + $this->patterns[$entity->getEntityTypeId()][$entity->id()][$langcode] = NULL; + } + } + return $this->patterns[$entity->getEntityTypeId()][$entity->id()][$langcode]; + } + + /** + * {@inheritdoc} + */ + public function resetCaches() { + $this->patterns = []; + $this->patternsByEntityType = []; + $this->aliasCleaner->resetCaches(); + } + + /** + * {@inheritdoc} + */ + public function updateEntityAlias(EntityInterface $entity, $op, array $options = array()) { + // Skip if the entity does not have the path field. + if (!($entity instanceof ContentEntityInterface) || !$entity->hasField('path')) { + return NULL; + } + + // Skip if pathauto processing is disabled. + if ($entity->path->pathauto != PathautoState::CREATE && empty($options['force'])) { + return NULL; + } + + $options += array('language' => $entity->language()->getId()); + $type = $entity->getEntityTypeId(); + + // Skip processing if the entity has no pattern. + if (!$this->getPatternByEntity($entity)) { + return NULL; + } + + // Deal with taxonomy specific logic. + // @todo Update and test forum related code. + if ($type == 'taxonomy_term') { + + $config_forum = $this->configFactory->get('forum.settings'); + if ($entity->getVocabularyId() == $config_forum->get('vocabulary')) { + $type = 'forum'; + } + } + + $result = $this->createEntityAlias($entity, $op); + + // @todo Move this to a method on the pattern plugin. + if ($type == 'taxonomy_term') { + foreach ($this->loadTermChildren($entity->id()) as $subterm) { + $this->updateEntityAlias($subterm, $op, $options); + } + } + + return $result; + } + + /** + * Finds all children of a term ID. + * + * @param int $tid + * Term ID to retrieve parents for. + * + * @return \Drupal\taxonomy\TermInterface[] + * An array of term objects that are the children of the term $tid. + */ + protected function loadTermChildren($tid) { + return \Drupal::entityManager()->getStorage('taxonomy_term')->loadChildren($tid); + } + +} diff --git a/src/PathautoGeneratorInterface.php b/src/PathautoGeneratorInterface.php new file mode 100644 index 0000000..e6371b1 --- /dev/null +++ b/src/PathautoGeneratorInterface.php @@ -0,0 +1,93 @@ +pathauto) || empty($this->pathauto)) { - parent::insert(); - } + public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) { + $properties = parent::propertyDefinitions($field_definition); + $properties['pathauto'] = DataDefinition::create('integer') + ->setLabel(t('Pathauto state')) + ->setDescription(t('Whether an automated alias should be created or not.')) + ->setComputed(TRUE) + ->setClass('\Drupal\pathauto\PathautoState'); + return $properties; } /** * {@inheritdoc} */ - public function update() { + public function postSave($update) { // Only allow the parent implementation to act if pathauto will not create // an alias. - if (!isset($this->pathauto) || empty($this->pathauto)) { - parent::update(); + if ($this->pathauto == PathautoState::SKIP) { + parent::postSave($update); } + $this->get('pathauto')->persist(); + } + + /** + * {@inheritdoc} + */ + public function isEmpty() { + // Make sure that the pathauto state flag does not get lost if just that is + // changed. + return !$this->alias && !$this->get('pathauto')->hasValue(); } + /** + * {@inheritdoc} + */ + public function applyDefaultValue($notify = TRUE) { + parent::applyDefaultValue($notify); + // Created fields default creating a new alias. + $this->setValue(array('pathauto' => PathautoState::CREATE), $notify); + return $this; + } + + } diff --git a/src/PathautoManager.php b/src/PathautoManager.php deleted file mode 100644 index 94cf490..0000000 --- a/src/PathautoManager.php +++ /dev/null @@ -1,547 +0,0 @@ -configFactory = $config_factory; - $this->languageManager = $language_manager; - $this->cacheBackend = $cache_backend; - $this->moduleHandler = $module_handler; - $this->token = $token; - $this->aliasCleaner = $alias_cleaner; - $this->aliasStorageHelper = $alias_storage_helper; - $this->aliasUniquifier = $alias_uniquifier; - $this->messenger = $messenger; - $this->stringTranslation = $string_translation; - } - - /** - * {@inheritdoc} - */ - public function cleanString($string, array $options = array()) { - if (empty($this->cleanStringCache)) { - // Generate and cache variables used in this method. - $config = $this->configFactory->get('pathauto.settings'); - $this->cleanStringCache = array( - 'separator' => $config->get('separator'), - 'strings' => array(), - 'transliterate' => $config->get('transliterate'), - 'punctuation' => array(), - 'reduce_ascii' => (bool) $config->get('reduce_ascii'), - 'ignore_words_regex' => FALSE, - 'lowercase' => (bool) $config->get('case'), - 'maxlength' => min($config->get('max_component_length'), $this->aliasStorageHelper->getAliasSchemaMaxLength()), - ); - - // Generate and cache the punctuation replacements for strtr(). - $punctuation = $this->getPunctuationCharacters(); - foreach ($punctuation as $name => $details) { - $action = $config->get('punctuation.' . $name); - switch ($action) { - case PathautoManagerInterface::PUNCTUATION_REMOVE: - $cache['punctuation'][$details['value']] = ''; - $this->cleanStringCache; - - case PathautoManagerInterface::PUNCTUATION_REPLACE: - $this->cleanStringCache['punctuation'][$details['value']] = $this->cleanStringCache['separator']; - break; - - case PathautoManagerInterface::PUNCTUATION_DO_NOTHING: - // Literally do nothing. - break; - } - } - - // Generate and cache the ignored words regular expression. - $ignore_words = $config->get('ignore_words'); - $ignore_words_regex = preg_replace(array('/^[,\s]+|[,\s]+$/', '/[,\s]+/'), array('', '\b|\b'), $ignore_words); - if ($ignore_words_regex) { - $this->cleanStringCache['ignore_words_regex'] = '\b' . $ignore_words_regex . '\b'; - if (function_exists('mb_eregi_replace')) { - $this->cleanStringCache['ignore_words_callback'] = 'mb_eregi_replace'; - } - else { - $this->cleanStringCache['ignore_words_callback'] = 'preg_replace'; - $this->cleanStringCache['ignore_words_regex'] = '/' . $this->cleanStringCache['ignore_words_regex'] . '/i'; - } - } - } - - // Empty strings do not need any proccessing. - if ($string === '' || $string === NULL) { - return ''; - } - - $langcode = NULL; - if (!empty($options['language'])) { - $langcode = $options['language']->getId(); - } - elseif (!empty($options['langcode'])) { - $langcode = $options['langcode']; - } - - // Check if the string has already been processed, and if so return the - // cached result. - if (isset($this->cleanStringCache['strings'][$langcode][$string])) { - return $this->cleanStringCache['strings'][$langcode][$string]; - } - - // Remove all HTML tags from the string. - $output = strip_tags(Html::decodeEntities($string)); - - // Optionally transliterate. - if ($this->cleanStringCache['transliterate']) { - // If the reduce strings to letters and numbers is enabled, don't bother - // replacing unknown characters with a question mark. Use an empty string - // instead. - $output = \Drupal::service('transliteration')->transliterate($output, $this->cleanStringCache['reduce_ascii'] ? '' : '?', $langcode); - } - - // Replace or drop punctuation based on user settings. - $output = strtr($output, $this->cleanStringCache['punctuation']); - - // Reduce strings to letters and numbers. - if ($this->cleanStringCache['reduce_ascii']) { - $output = preg_replace('/[^a-zA-Z0-9\/]+/', $this->cleanStringCache['separator'], $output); - } - - // Get rid of words that are on the ignore list. - if ($this->cleanStringCache['ignore_words_regex']) { - $words_removed = $this->cleanStringCache['ignore_words_callback']($this->cleanStringCache['ignore_words_regex'], '', $output); - if (Unicode::strlen(trim($words_removed)) > 0) { - $output = $words_removed; - } - } - - // Always replace whitespace with the separator. - $output = preg_replace('/\s+/', $this->cleanStringCache['separator'], $output); - - // Trim duplicates and remove trailing and leading separators. - $output = $this->aliasCleaner->getCleanSeparators($this->aliasCleaner->getCleanSeparators($output, $this->cleanStringCache['separator'])); - - // Optionally convert to lower case. - if ($this->cleanStringCache['lowercase']) { - $output = Unicode::strtolower($output); - } - - // Shorten to a logical place based on word boundaries. - $output = Unicode::truncate($output, $this->cleanStringCache['maxlength'], TRUE); - - // Cache this result in the static array. - $this->cleanStringCache['strings'][$langcode][$string] = $output; - - return $output; - } - - - /** - * {@inheritdoc} - */ - public function getPunctuationCharacters() { - if (empty($this->punctuationCharacters)) { - $langcode = $this->languageManager->getCurrentLanguage()->getId(); - - $cid = 'pathauto:punctuation:' . $langcode; - if ($cache = $this->cacheBackend->get($cid)) { - $this->punctuationCharacters = $cache->data; - } - else { - $punctuation = array(); - $punctuation['double_quotes'] = array('value' => '"', 'name' => t('Double quotation marks')); - $punctuation['quotes'] = array('value' => '\'', 'name' => t("Single quotation marks (apostrophe)")); - $punctuation['backtick'] = array('value' => '`', 'name' => t('Back tick')); - $punctuation['comma'] = array('value' => ',', 'name' => t('Comma')); - $punctuation['period'] = array('value' => '.', 'name' => t('Period')); - $punctuation['hyphen'] = array('value' => '-', 'name' => t('Hyphen')); - $punctuation['underscore'] = array('value' => '_', 'name' => t('Underscore')); - $punctuation['colon'] = array('value' => ':', 'name' => t('Colon')); - $punctuation['semicolon'] = array('value' => ';', 'name' => t('Semicolon')); - $punctuation['pipe'] = array('value' => '|', 'name' => t('Vertical bar (pipe)')); - $punctuation['left_curly'] = array('value' => '{', 'name' => t('Left curly bracket')); - $punctuation['left_square'] = array('value' => '[', 'name' => t('Left square bracket')); - $punctuation['right_curly'] = array('value' => '}', 'name' => t('Right curly bracket')); - $punctuation['right_square'] = array('value' => ']', 'name' => t('Right square bracket')); - $punctuation['plus'] = array('value' => '+', 'name' => t('Plus sign')); - $punctuation['equal'] = array('value' => '=', 'name' => t('Equal sign')); - $punctuation['asterisk'] = array('value' => '*', 'name' => t('Asterisk')); - $punctuation['ampersand'] = array('value' => '&', 'name' => t('Ampersand')); - $punctuation['percent'] = array('value' => '%', 'name' => t('Percent sign')); - $punctuation['caret'] = array('value' => '^', 'name' => t('Caret')); - $punctuation['dollar'] = array('value' => '$', 'name' => t('Dollar sign')); - $punctuation['hash'] = array('value' => '#', 'name' => t('Number sign (pound sign, hash)')); - $punctuation['at'] = array('value' => '@', 'name' => t('At sign')); - $punctuation['exclamation'] = array('value' => '!', 'name' => t('Exclamation mark')); - $punctuation['tilde'] = array('value' => '~', 'name' => t('Tilde')); - $punctuation['left_parenthesis'] = array('value' => '(', 'name' => t('Left parenthesis')); - $punctuation['right_parenthesis'] = array('value' => ')', 'name' => t('Right parenthesis')); - $punctuation['question_mark'] = array('value' => '?', 'name' => t('Question mark')); - $punctuation['less_than'] = array('value' => '<', 'name' => t('Less-than sign')); - $punctuation['greater_than'] = array('value' => '>', 'name' => t('Greater-than sign')); - $punctuation['slash'] = array('value' => '/', 'name' => t('Slash')); - $punctuation['back_slash'] = array('value' => '\\', 'name' => t('Backslash')); - - // Allow modules to alter the punctuation list and cache the result. - $this->moduleHandler->alter('pathauto_punctuation_chars', $punctuation); - $this->cacheBackend->set($cid, $punctuation); - $this->punctuationCharacters = $punctuation; - } - } - - return $this->punctuationCharacters; - } - - - /** - * {@inheritdoc} - */ - public function createAlias($module, $op, $source, $data, $type = NULL, $langcode = LanguageInterface::LANGCODE_NOT_SPECIFIED) { - $config = $this->configFactory->get('pathauto.settings'); - - // Retrieve and apply the pattern for this content type. - $pattern = $this->getPatternByEntity($module, $type, $langcode); - - // Allow other modules to alter the pattern. - $context = array( - 'module' => $module, - 'op' => $op, - 'source' => $source, - 'data' => $data, - 'type' => $type, - 'language' => &$langcode, - ); - $this->moduleHandler->alter('pathauto_pattern', $pattern, $context); - - if (empty($pattern)) { - // No pattern? Do nothing (otherwise we may blow away existing aliases...) - return NULL; - } - - // Special handling when updating an item which is already aliased. - $existing_alias = NULL; - if ($op == 'update' || $op == 'bulkupdate') { - if ($existing_alias = $this->aliasStorageHelper->loadBySource($source, $langcode)) { - switch ($config->get('update_action')) { - case PathautoManagerInterface::UPDATE_ACTION_NO_NEW: - // If an alias already exists, - // and the update action is set to do nothing, - // then gosh-darn it, do nothing. - return NULL; - } - } - } - - // Replace any tokens in the pattern. - // Uses callback option to clean replacements. No sanitization. - $alias = $this->token->replace($pattern, $data, array( - 'sanitize' => FALSE, - 'clear' => TRUE, - 'callback' => array($this, 'cleanTokenValues'), - 'langcode' => $langcode, - 'pathauto' => TRUE, - )); - - // Check if the token replacement has not actually replaced any values. If - // that is the case, then stop because we should not generate an alias. - // @see token_scan() - $pattern_tokens_removed = preg_replace('/\[[^\s\]:]*:[^\s\]]*\]/', '', $pattern); - if ($alias === $pattern_tokens_removed) { - return NULL; - } - - $alias = $this->aliasCleaner->cleanAlias($alias); - - // Allow other modules to alter the alias. - $context['source'] = &$source; - $context['pattern'] = $pattern; - $this->moduleHandler->alter('pathauto_alias', $alias, $context); - - // If we have arrived at an empty string, discontinue. - if (!Unicode::strlen($alias)) { - return NULL; - } - - // If the alias already exists, generate a new, hopefully unique, variant. - $original_alias = $alias; - $this->aliasUniquifier->uniquify($alias, $source, $langcode); - if ($original_alias != $alias) { - // Alert the user why this happened. - $this->messenger->addMessage($this->t('The automatically generated alias %original_alias conflicted with an existing alias. Alias changed to %alias.', array( - '%original_alias' => $original_alias, - '%alias' => $alias, - )), $op); - } - - // Return the generated alias if requested. - if ($op == 'return') { - return $alias; - } - - // Build the new path alias array and send it off to be created. - $path = array( - 'source' => $source, - 'alias' => $alias, - 'language' => $langcode, - ); - - return $this->aliasStorageHelper->save($path, $existing_alias, $op); - } - - /** - * {@inheritdoc} - */ - public function getPatternByEntity($entity_type_id, $bundle = '', $language = LanguageInterface::LANGCODE_NOT_SPECIFIED) { - $config = $this->configFactory->get('pathauto.pattern'); - - $pattern_id = "$entity_type_id:$bundle:$language"; - if (!isset($this->patterns[$pattern_id])) { - $pattern = ''; - $variables = array(); - if ($language != LanguageInterface::LANGCODE_NOT_SPECIFIED) { - $variables[] = "{$entity_type_id}.bundles.{$bundle}.languages.{$language}"; - } - if ($bundle) { - $variables[] = "{$entity_type_id}.bundles.{$bundle}.default"; - } - $variables[] = "{$entity_type_id}.default"; - - foreach ($variables as $variable) { - if ($pattern = trim($config->get('patterns.' . $variable))) { - break; - } - } - - $this->patterns[$pattern_id] = $pattern; - } - - return $this->patterns[$pattern_id]; - } - - /** - * Resets internal caches. - */ - public function resetCaches() { - $this->patterns = array(); - $this->cleanStringCache = array(); - } - - /** - * {@inheritdoc} - */ - public function updateAlias(EntityInterface $entity, $op, array $options = array()) { - // Skip if the entity does not have the path field. - if (!($entity instanceof ContentEntityInterface) || !$entity->hasField('path')) { - return NULL; - } - - // Skip if pathauto processing is disabled. - if (isset($entity->path->pathauto) && empty($entity->path->pathauto) && empty($options['force'])) { - return NULL; - } - - $options += array('language' => $entity->language()->getId()); - $type = $entity->getEntityTypeId(); - $bundle = $entity->bundle(); - - // Skip processing if the entity has no pattern. - if (!$this->getPatternByEntity($type, $bundle, $options['language'])) { - return NULL; - } - - // Deal with taxonomy specific logic. - if ($type == 'taxonomy_term') { - - $config_forum = $this->configFactory->get('forum.settings'); - if ($entity->getVocabularyId() == $config_forum->get('vocabulary')) { - $type = 'forum'; - } - } - - $result = $this->createAlias( - $type, $op, $entity->getSystemPath(), array($type => $entity), $bundle, $options['language']); - - if ($type == 'taxonomy_term' && empty($options['is_child'])) { - // For all children generate new aliases. - $options['is_child'] = TRUE; - unset($options['language']); - foreach ($this->getTermTree($entity->getVocabularyId(), $entity->id(), NULL, TRUE) as $subterm) { - $this->updateAlias($subterm, $op, $options); - } - } - - return $result; - } - - /** - * Create a hierarchical representation of a vocabulary. - * - * Wrapper of taxonomy_get_tree() for testing. - * - * @param int $vid - * The vocabulary ID to generate the tree for. - * @param int $parent - * The term ID under which to generate the tree. If 0, generate the tree - * for the entire vocabulary. - * @param int $max_depth - * The number of levels of the tree to return. Leave NULL to return all levels. - * @param bool $load_entities - * If TRUE, a full entity load will occur on the term objects. Otherwise they - * are partial objects queried directly from the {taxonomy_term_field_data} - * table to save execution time and memory consumption when listing large - * numbers of terms. Defaults to FALSE. - * - * @return array - * An array of all term objects in the tree. Each term object is extended - * to have "depth" and "parents" attributes in addition to its normal ones. - * Results are statically cached. Term objects will be partial or complete - * depending on the $load_entities parameter. - */ - protected function getTermTree($vid, $parent = 0, $max_depth = NULL, $load_entities = FALSE) { - return taxonomy_get_tree($vid, $parent, $max_depth, $load_entities); - } - - /** - * {@inheritdoc} - */ - public function cleanTokenValues(&$replacements, $data = array(), $options = array()) { - foreach ($replacements as $token => $value) { - // Only clean non-path tokens. - if (!preg_match('/(path|alias|url|url-brief)\]$/', $token)) { - $replacements[$token] = $this->cleanString($value, $options); - } - } - } -} diff --git a/src/PathautoManagerInterface.php b/src/PathautoManagerInterface.php deleted file mode 100644 index 9d56abd..0000000 --- a/src/PathautoManagerInterface.php +++ /dev/null @@ -1,167 +0,0 @@ -type). - * @param string $langcode - * A string specify the path's language. - * - * @return array|string - * The alias that was created. - * - * @see _pathauto_set_alias() - */ - public function createAlias($module, $op, $source, $data, $type = NULL, $langcode = LanguageInterface::LANGCODE_NOT_SPECIFIED); - - /** - * Return an array of arrays for punctuation values. - * - * Returns an array of arrays for punctuation values keyed by a name, including - * the value and a textual description. - * Can and should be expanded to include "all" non text punctuation values. - * - * @return array - * An array of arrays for punctuation values keyed by a name, including the - * value and a textual description. - */ - public function getPunctuationCharacters(); - - /** - * Creates or updates an alias for the given entity. - * - * @param EntityInterface $entity - * Entity for which to update the alias. - * @param string $op - * The operation performed (insert, update) - * @param array $options - * - force: will force updating the path - * - language: the language for which to create the alias - * - * @return array|null - * - An array with alias data in case the alias has been created or updated. - * - NULL if no operation performed. - */ - public function updateAlias(EntityInterface $entity, $op, array $options = array()); - - /** - * Clean tokens so they are URL friendly. - * - * @param array $replacements - * An array of token replacements - * that need to be "cleaned" for use in the URL. - * @param array $data - * An array of objects used to generate the replacements. - * @param array $options - * An array of options used to generate the replacements. - */ - public function cleanTokenValues(&$replacements, $data = array(), $options = array()); - -} diff --git a/src/PathautoPatternInterface.php b/src/PathautoPatternInterface.php new file mode 100644 index 0000000..33d67d2 --- /dev/null +++ b/src/PathautoPatternInterface.php @@ -0,0 +1,159 @@ +t('Label'); + $header['pattern'] = $this->t('Pattern'); + $header['type'] = $this->t('Pattern type'); + $header['conditions'] = $this->t('Conditions'); + return $header + parent::buildHeader(); + } + + /** + * {@inheritdoc} + */ + public function buildRow(EntityInterface $entity) { + /* @var \Drupal\pathauto\PathautoPatternInterface $entity */ + $row['label'] = $entity->label(); + $row['patern']['#markup'] = $entity->getPattern(); + $row['type']['#markup'] = $entity->getAliasType()->getLabel(); + $row['conditions']['#theme'] = 'item_list'; + foreach ($entity->getSelectionConditions() as $condition) { + $row['conditions']['#items'][] = $condition->summary(); + } + return $row + parent::buildRow($entity); + } + +} diff --git a/src/PathautoState.php b/src/PathautoState.php new file mode 100644 index 0000000..16b8a26 --- /dev/null +++ b/src/PathautoState.php @@ -0,0 +1,118 @@ +value === NULL) { + $entity = $this->parent->getEntity(); + + // @todo: Investigate why this happens. + if ($entity->isNew()) { + $this->value = static::CREATE; + return $this->value; + } + + // If no value has been set or loaded yet, try to load a value if this + // entity has already been saved. + $this->value = \Drupal::keyValue($this->getCollection()) + ->get($this->parent->getEntity()->id()); + // If it was not yet saved or no value was found, try to detect based on + // an existing alias if the entity is not new. + if ($this->value === NULL) { + $entity_path = '/' . $entity->toUrl()->getInternalPath(); + $path = \Drupal::service('path.alias_manager') + ->getAliasByPath( + $entity_path, $entity->language()->getId() + ); + $pathauto_alias = \Drupal::service('pathauto.generator') + ->createEntityAlias($entity, 'return'); + if (($path != $entity_path && $path == $pathauto_alias)) { + $this->value = static::CREATE; + } + else { + $this->value = static::SKIP; + } + } + } + return $this->value; + } + + /** + * {@inheritdoc} + */ + public function setValue($value, $notify = TRUE) { + $this->value = $value; + // Notify the parent of any changes. + if ($notify && isset($this->parent)) { + $this->parent->onChange($this->name); + } + } + + /** + * Returns TRUE if a value was set. + */ + public function hasValue() { + return $this->value !== NULL; + } + + /** + * Persists the state. + */ + public function persist() { + \Drupal::keyValue($this->getCollection())->set( + $this->parent->getEntity() + ->id(), $this->value + ); + } + + /** + * Deletes the stored state. + */ + public function purge() { + \Drupal::keyValue($this->getCollection()) + ->delete($this->parent->getEntity()->id()); + } + + /** + * Returns the key value collection that should be used for the given entity. + * @return string + */ + protected function getCollection() { + return 'pathauto_state.' . $this->parent->getEntity()->getEntityTypeId(); + } +} diff --git a/src/Plugin/Field/FieldWidget/PathautoWidget.php b/src/PathautoWidget.php similarity index 62% rename from src/Plugin/Field/FieldWidget/PathautoWidget.php rename to src/PathautoWidget.php index 8acde06..053dc71 100644 --- a/src/Plugin/Field/FieldWidget/PathautoWidget.php +++ b/src/PathautoWidget.php @@ -1,26 +1,17 @@ getPatternByEntity($entity->getEntityTypeId(), $entity->bundle(), $entity->language()->getId()); + $pattern = \Drupal::service('pathauto.generator')->getPatternByEntity($entity); if (empty($pattern)) { return $element; } - - if (!isset($entity->path->pathauto)) { - if (!$entity->isNew()) { - module_load_include('inc', 'pathauto'); - $path = \Drupal::service('path.alias_manager')->getAliasByPath($entity->getSystemPath(), $entity->language()->getId()); - $pathauto_alias = \Drupal::service('pathauto.manager')->createAlias($entity->getEntityTypeId(), 'return', $entity->getSystemPath(), array($entity->getEntityType()->id() => $entity), $entity->bundle(), $entity->language()->getId()); - $entity->path->pathauto = ($path != $entity->getSystemPath() && $path == $pathauto_alias); - } - else { - $entity->path->pathauto = TRUE; - } - } $element['pathauto'] = array( '#type' => 'checkbox', '#title' => $this->t('Generate automatic URL alias'), '#default_value' => $entity->path->pathauto, - '#description' => $this->t('Uncheck this to create a custom alias below. Configure URL alias patterns.', array('@admin_link' => \Drupal::url('pathauto.patterns.form'))), + '#description' => $this->t('Uncheck this to create a custom alias below. Configure URL alias patterns.', array('@admin_link' => \Drupal::url('entity.pathauto_pattern.collection'))), '#weight' => -1, ); @@ -85,9 +64,9 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen // Override path.module's vertical tabs summary. $element['alias']['#attached']['library'] = ['pathauto/widget']; - if ($entity->path->pathauto && !empty($entity->old_alias) && empty($entity->path->alias)) { - $element['alias']['#default_value'] = $entity->old_alias; - $entity->path->alias = $entity->old_alias; + if ($entity->path->pathauto == PathautoState::CREATE && !empty($entity->path->old_alias) && empty($entity->path->alias)) { + $element['alias']['#default_value'] = $entity->path->old_alias; + $entity->path->alias = $entity->path->old_alias; } diff --git a/src/Plugin/Action/UpdateAction.php b/src/Plugin/Action/UpdateAction.php index fd7b102..75170eb 100644 --- a/src/Plugin/Action/UpdateAction.php +++ b/src/Plugin/Action/UpdateAction.php @@ -9,6 +9,7 @@ use Drupal\Core\Access\AccessResult; use Drupal\Core\Action\ActionBase; use Drupal\Core\Session\AccountInterface; +use Drupal\pathauto\PathautoState; /** * Pathauto entity update action. @@ -24,9 +25,8 @@ class UpdateAction extends ActionBase { * {@inheritdoc} */ public function execute($entity = NULL) { - $entity->path = new \stdClass(); - $entity->path->pathauto = TRUE; - \Drupal::service('pathauto.manager')->updateAlias($entity, 'bulkupdate', array('message' => TRUE)); + $entity->path->pathauto = PathautoState::CREATE; + \Drupal::service('pathauto.generator')->updateEntityAlias($entity, 'bulkupdate', array('message' => TRUE)); } /** diff --git a/src/Plugin/Deriver/EntityAliasTypeDeriver.php b/src/Plugin/Deriver/EntityAliasTypeDeriver.php new file mode 100644 index 0000000..f846d48 --- /dev/null +++ b/src/Plugin/Deriver/EntityAliasTypeDeriver.php @@ -0,0 +1,99 @@ +entityTypeManager = $entity_type_manager; + $this->entityFieldManager = $entity_field_manager; + $this->stringTranslation = $string_translation; + $this->tokenEntityMapper = $token_entity_mapper; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, $base_plugin_id) { + return new static( + $container->get('entity_type.manager'), + $container->get('entity_field.manager'), + $container->get('string_translation'), + $container->get('token.entity_mapper') + ); + } + + /** + * {@inheritdoc} + */ + public function getDerivativeDefinitions($base_plugin_definition) { + foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) { + // An entity type must have a canonical link template and support fields. + if ($entity_type->hasLinkTemplate('canonical') && is_subclass_of($entity_type->getClass(), FieldableEntityInterface::class)) { + $base_fields = $this->entityFieldManager->getBaseFieldDefinitions($entity_type_id); + if (!isset($base_fields['path'])) { + // The entity type does not have a path field and is therefore not + // supported. + // @todo: Add a UI to enable that base field on any content entity. + continue; + } + $this->derivatives[$entity_type_id] = $base_plugin_definition; + $this->derivatives[$entity_type_id]['label'] = $entity_type->getLabel(); + $this->derivatives[$entity_type_id]['types'] = [$this->tokenEntityMapper->getTokenTypeForEntityType($entity_type_id)]; + $this->derivatives[$entity_type_id]['provider'] = $entity_type->getProvider(); + $this->derivatives[$entity_type_id]['context'] = [ + $entity_type_id => new ContextDefinition("entity:$entity_type_id", $this->t('@label being aliased', ['@label' => $entity_type->getLabel()])) + ]; + } + } + return $this->derivatives; + } + +} diff --git a/src/Plugin/pathauto/AliasType/AliasTypeBase.php b/src/Plugin/pathauto/AliasType/AliasTypeBase.php deleted file mode 100644 index 174e936..0000000 --- a/src/Plugin/pathauto/AliasType/AliasTypeBase.php +++ /dev/null @@ -1,138 +0,0 @@ -configuration; - } - - /** - * {@inheritdoc} - */ - public function setConfiguration(array $configuration) { - $this->configuration = $configuration; - } - - /** - * {@inheritdoc} - */ - public function defaultConfiguration() { - return array(); - } - - /** - * {@inheritdoc} - */ - public function getLabel() { - $definition = $this->getPluginDefinition(); - // Cast the admin label to a string since it is an object. - // @see \Drupal\Core\StringTranslation\TranslationWrapper - return (string) $definition['label']; - } - - /** - * {@inheritdoc} - */ - public function getTokenTypes() { - $definition = $this->getPluginDefinition(); - return $definition['types']; - } - - /** - * {@inheritdoc} - */ - public function buildConfigurationForm(array $form, FormStateInterface $form_state) { - $plugin_id = $this->getPluginId(); - - $form[$plugin_id] = array( - '#type' => 'fieldset', - '#title' => $this->getLabel(), - '#collapsible' => TRUE, - '#collapsed' => FALSE, - '#tree' => TRUE, - ); - - // Prompt for the default pattern for this module. - $key = '_default'; - - $form[$plugin_id][$key] = array( - '#type' => 'textfield', - '#title' => $this->getPatternDescription(), - '#default_value' => $this->configuration['patternitems'], - '#size' => 65, - '#maxlength' => 1280, - '#element_validate' => array('token_element_validate'), - '#after_build' => array('token_element_validate'), - '#token_types' => array($this->getTokenTypes()), - '#min_tokens' => 1, - ); - - // If the module supports a set of specialized patterns, set - // them up here. - $patterns = $this->getPatterns(); - foreach ($patterns as $itemname => $itemlabel) { - $key = '_default'; - - $form[$plugin_id][$itemname][$key] = array( - '#type' => 'textfield', - '#title' => $itemlabel, - '#default_value' => $this->configuration[$plugin_id . '.' . $itemname . '.' . $key], - '#size' => 65, - '#maxlength' => 1280, - '#element_validate' => array('token_element_validate'), - '#after_build' => array('token_element_validate'), - '#token_types' => array($this->getTokenTypes()), - '#min_tokens' => 1, - ); - } - - // Display the user documentation of placeholders supported by - // this module, as a description on the last pattern. - $form[$plugin_id]['token_help'] = array( - '#title' => t('Replacement patterns'), - '#type' => 'fieldset', - '#collapsible' => TRUE, - '#collapsed' => TRUE, - ); - $form[$plugin_id]['token_help']['help'] = array( - '#theme' => 'token_tree', - '#token_types' => array($this->getTokenTypes()), - ); - } - - /** - * {@inheritdoc} - */ - public function calculateDependencies() { - } - - /** - * {@inheritdoc} - */ - public function validateConfigurationForm(array &$form, FormStateInterface $form_state) { - } - - /** - * {@inheritdoc} - */ - public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { - } - -} diff --git a/src/Plugin/pathauto/AliasType/EntityAliasTypeBase.php b/src/Plugin/pathauto/AliasType/EntityAliasTypeBase.php new file mode 100644 index 0000000..543901e --- /dev/null +++ b/src/Plugin/pathauto/AliasType/EntityAliasTypeBase.php @@ -0,0 +1,221 @@ +moduleHandler = $module_handler; + $this->languageManager = $language_manager; + $this->entityTypeManager = $entity_type_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('module_handler'), + $container->get('language_manager'), + $container->get('entity_type.manager') + ); + } + + /** + * {@inheritdoc} + */ + public function getLabel() { + $definition = $this->getPluginDefinition(); + // Cast the admin label to a string since it is an object. + // @see \Drupal\Core\StringTranslation\TranslationWrapper + return (string) $definition['label']; + } + + /** + * {@inheritdoc} + */ + public function getTokenTypes() { + $definition = $this->getPluginDefinition(); + return $definition['types']; + } + + /** + * {@inheritdoc} + */ + public function batchUpdate(&$context) { + if (!isset($context['sandbox']['current'])) { + $context['sandbox']['count'] = 0; + $context['sandbox']['current'] = 0; + } + + $entity_type = $this->entityTypeManager->getDefinition($this->getEntityTypeId()); + $id_key = $entity_type->getKey('id'); + + $query = db_select($entity_type->get('base_table'), 'base_table'); + $query->leftJoin('url_alias', 'ua', "CONCAT('" . $this->getSourcePrefix() . "' , base_table.$id_key) = ua.source"); + $query->addField('base_table', $id_key, 'id'); + $query->isNull('ua.source'); + $query->condition('base_table.' . $id_key, $context['sandbox']['current'], '>'); + $query->orderBy('base_table.' . $id_key); + $query->addTag('pathauto_bulk_update'); + $query->addMetaData('entity', $this->getEntityTypeId()); + + // Get the total amount of items to process. + if (!isset($context['sandbox']['total'])) { + $context['sandbox']['total'] = $query->countQuery()->execute()->fetchField(); + + // If there are no entities to update, then stop immediately. + if (!$context['sandbox']['total']) { + $context['finished'] = 1; + return; + } + } + + $query->range(0, 25); + $ids = $query->execute()->fetchCol(); + + $this->bulkUpdate($ids); + $context['sandbox']['count'] += count($ids); + $context['sandbox']['current'] = max($ids); + $context['message'] = t('Updated alias for %label @id.', array('%label' => $entity_type->getLabel(), '@id' => end($ids))); + + if ($context['sandbox']['count'] != $context['sandbox']['total']) { + $context['finished'] = $context['sandbox']['count'] / $context['sandbox']['total']; + } + } + + /** + * Returns the entity type ID. + * + * @return string + * The entity type ID. + */ + protected function getEntityTypeId() { + return $this->getDerivativeId(); + } + + /** + * Update the URL aliases for multiple entities. + * + * @param array $ids + * An array of entity IDs IDs. + * @param array $options + * An optional array of additional options. + */ + protected function bulkUpdate(array $ids, array $options = array()) { + $options += array('message' => FALSE); + + $entities = $this->entityTypeManager->getStorage($this->getEntityTypeId())->loadMultiple($ids); + foreach ($entities as $entity) { + // Update aliases for the entity's default language and its translations. + foreach ($entity->getTranslationLanguages() as $langcode => $language) { + $translated_entity = $entity->getTranslation($langcode); + \Drupal::service('pathauto.generator')->updateEntityAlias($translated_entity, 'bulkupdate', $options); + } + } + + if (!empty($options['message'])) { + drupal_set_message(\Drupal::translation()->formatPlural(count($ids), 'Updated 1 %label URL alias.', 'Updated @count %label URL aliases.'), array('%label' => $this->getLabel())); + } + } + + /** + * {@inheritdoc} + */ + public function calculateDependencies() { + $dependencies = []; + $dependencies['module'][] = $this->entityTypeManager->getDefinition($this->getEntityTypeId())->getProvider(); + return $dependencies; + } + + /** + * {@inheritdoc} + */ + public function applies($object) { + return $object instanceof FieldableEntityInterface && $object->getEntityTypeId() == $this->getEntityTypeId(); + } + + /** + * {@inheritdoc} + */ + public function getSourcePrefix() { + if (empty($this->prefix)) { + $entity_type = $this->entityTypeManager->getDefinition($this->getEntityTypeId()); + $path = $entity_type->getLinkTemplate('canonical'); + $this->prefix = substr($path, 0, strpos($path, '{')); + } + return $this->prefix; + } + +} diff --git a/src/Plugin/pathauto/AliasType/ForumAliasType.php b/src/Plugin/pathauto/AliasType/ForumAliasType.php new file mode 100644 index 0000000..bbb1833 --- /dev/null +++ b/src/Plugin/pathauto/AliasType/ForumAliasType.php @@ -0,0 +1,51 @@ +configFactory->get('forum.settings'); + return $object->getVocabularyId() == $config_forum->get('vocabulary'); + } + return FALSE; + } + +} diff --git a/src/Plugin/pathauto/AliasType/NodeAliasType.php b/src/Plugin/pathauto/AliasType/NodeAliasType.php deleted file mode 100644 index 6c6ff0e..0000000 --- a/src/Plugin/pathauto/AliasType/NodeAliasType.php +++ /dev/null @@ -1,146 +0,0 @@ -moduleHandler = $module_handler; - $this->languageManager = $language_manager; - $this->entityManager = $entity_manager; - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { - return new static( - $configuration, - $plugin_id, - $plugin_definition, - $container->get('module_handler'), - $container->get('language_manager'), - $container->get('entity.manager') - ); - } - - /** - * {@inheritdoc} - */ - public function getPatternDescription() { - $this->t('Default path pattern (applies to all content types with blank patterns below)'); - } - - /** - * {@inheritdoc} - */ - public function getPatterns() { - $patterns = []; - $languages = $this->languageManager->getLanguages(); - foreach ($this->getNodeTypeNames() as $node_type => $node_type_name) { - if (count($languages) && $this->isContentTranslationEnabled($node_type)) { - $patterns[$node_type] = $this->t('Default path pattern for @node_type (applies to all @node_type content types with blank patterns below)', array('@node_type' => $node_type_name)); - foreach ($languages as $language) { - $patterns[$node_type . '_' . $language->getId()] = $this->t('Pattern for all @language @node_type paths', array('@node_type' => $node_type_name, '@language' => $language->getName())); - } - } - else { - $patterns[$node_type] = $this->t('Pattern for all @node_type paths', array('@node_type' => $node_type_name)); - } - } - return $patterns; - } - - /** - * {@inheritdoc} - */ - public function defaultConfiguration() { - return array('patternitems' => array('content/[node:title]')) + parent::defaultConfiguration(); - } - - /** - * Wraps node_type_get_names(). - * - * @return array - * An array of node type names, keyed by type. - */ - protected function getNodeTypeNames() { - return array_map(function ($bundle_info) { - return $bundle_info['label']; - }, $this->entityManager->getBundleInfo('node')); - } - - /** - * Wraps content_translation_enabled(). - * - * @param string $node_type - * The node type. - * - * @return bool - * TRUE if content translation is enabled for the content type. - */ - protected function isContentTranslationEnabled($node_type) { - return $this->moduleHandler->moduleExists('content_translation') && \Drupal::service('content_translation.manager')->isEnabled('node', $node_type); - } - -} diff --git a/src/Tests/AliasType/NodeAliasTest.php b/src/Tests/AliasType/NodeAliasTest.php deleted file mode 100644 index 61f186e..0000000 --- a/src/Tests/AliasType/NodeAliasTest.php +++ /dev/null @@ -1,53 +0,0 @@ -container->get('plugin.manager.alias_type'); - - /** @var \Drupal\pathauto\AliasTypeInterface $node_type */ - $node_type = $manager->createInstance('node'); - - $patterns = $node_type->getPatterns(); - $this->assertTrue((array_key_exists('node', $patterns)), "Node pattern exists."); - $this->assertEqual($patterns['node'], 'Pattern for all Content paths', "Node pattern description matches."); - - $token_types = $node_type->getTokenTypes(); - $this->assertTrue(in_array('node', $token_types), "Node token type exists."); - - $label = $node_type->getLabel(); - $this->assertEqual($label, 'Content', "Plugin label matches."); - - $default_config = $node_type->defaultConfiguration(); - - $this->assertTrue(array_key_exists('patternitems', $default_config), "Patternitems key exists."); - $this->assertEqual($default_config['patternitems'][0], 'content/[node:title]', "Default content pattern matches."); - - } - -} diff --git a/src/Tests/PathautoBulkUpdateTest.php b/src/Tests/PathautoBulkUpdateTest.php index 01cd7d4..149ae9b 100644 --- a/src/Tests/PathautoBulkUpdateTest.php +++ b/src/Tests/PathautoBulkUpdateTest.php @@ -7,6 +7,7 @@ namespace Drupal\pathauto\Tests; +use Drupal\pathauto\PathautoState; use Drupal\simpletest\WebTestBase; /** @@ -53,8 +54,10 @@ function setUp() { ); $this->adminUser = $this->drupalCreateUser($permissions); $this->drupalLogin($this->adminUser); - } + $this->createPattern('node', '/content/[node:title]'); + $this->createPattern('user', '/users/[user:name]'); + } function testBulkUpdate() { // Create some nodes. @@ -69,11 +72,15 @@ function testBulkUpdate() { // Bulk create aliases. $edit = array( - 'update[node_pathauto_bulk_update_batch_process]' => TRUE, - 'update[user_pathauto_bulk_update_batch_process]' => TRUE, + 'update[canonical_entities:node]' => TRUE, + 'update[canonical_entities:user]' => TRUE, ); $this->drupalPostForm('admin/config/search/path/update_bulk', $edit, t('Update')); - $this->assertText('Generated 7 URL aliases.'); // 5 nodes + 2 users + + // This has generated 6 aliases. 5 nodes and one user that we created. There + // is also UID 1 but that user was created before the path field existed, + // so he does not have a pathauto state. + $this->assertText('Generated 6 URL aliases.'); // Check that aliases have actually been created. foreach ($this->nodes as $node) { @@ -82,12 +89,12 @@ function testBulkUpdate() { $this->assertEntityAliasExists($this->adminUser); // Add a new node. - $new_node = $this->drupalCreateNode(array('path' => array('alias' => '', 'pathauto' => FALSE))); + $new_node = $this->drupalCreateNode(array('path' => array('alias' => '', 'pathauto' => PathautoState::SKIP))); - // Run the update again which should only run against the new node. + // Run the update again which should not run against any nodes. $this->drupalPostForm('admin/config/search/path/update_bulk', $edit, t('Update')); - $this->assertText('Generated 1 URL alias.'); // 1 node + 0 users + $this->assertText('No new URL aliases to generate.'); - $this->assertEntityAliasExists($new_node); + $this->assertNoEntityAliasExists($new_node); } } diff --git a/src/Tests/PathautoLocaleTest.php b/src/Tests/PathautoLocaleTest.php index 187a18b..feffb25 100644 --- a/src/Tests/PathautoLocaleTest.php +++ b/src/Tests/PathautoLocaleTest.php @@ -8,7 +8,9 @@ namespace Drupal\pathauto\Tests; use Drupal\Core\Language\Language; +use Drupal\Core\Language\LanguageInterface; use Drupal\language\Entity\ConfigurableLanguage; +use Drupal\pathauto\PathautoState; use Drupal\simpletest\WebTestBase; /** @@ -25,14 +27,17 @@ class PathautoLocaleTest extends WebTestBase { * * @var array */ - public static $modules = array('node', 'pathauto', 'locale'); + public static $modules = array('node', 'pathauto', 'locale', 'content_translation'); /** - * Admin user. - * - * @var \Drupal\user\UserInterface + * {@inheritdoc} */ - protected $adminUser; + protected function setUp() { + parent::setUp(); + + // Create Article node type. + $this->drupalCreateContentType(array('type' => 'article', 'name' => 'Article')); + } /** * Test that when an English node is updated, its old English alias is @@ -40,6 +45,8 @@ class PathautoLocaleTest extends WebTestBase { */ function testLanguageAliases() { + $this->createPattern('node', '/content/[node:title]'); + // Add predefined French language. ConfigurableLanguage::createFromLangcode('fr')->save(); @@ -47,30 +54,123 @@ function testLanguageAliases() { 'title' => 'English node', 'langcode' => 'en', 'path' => array(array( - 'alias' => 'english-node', + 'alias' => '/english-node', 'pathauto' => FALSE, )), ); $node = $this->drupalCreateNode($node); - $english_alias = \Drupal::service('path.alias_storage')->load(array('alias' => 'english-node', 'langcode' => 'en')); + $english_alias = \Drupal::service('path.alias_storage')->load(array('alias' => '/english-node', 'langcode' => 'en')); $this->assertTrue($english_alias, 'Alias created with proper language.'); // Also save a French alias that should not be left alone, even though // it is the newer alias. - $this->saveEntityAlias($node, 'french-node', 'fr'); + $this->saveEntityAlias($node, '/french-node', 'fr'); // Add an alias with the soon-to-be generated alias, causing the upcoming // alias update to generate a unique alias with the '-0' suffix. - $this->saveAlias('node/invalid', 'content/english-node', Language::LANGCODE_NOT_SPECIFIED); + $this->saveAlias('/node/invalid', '/content/english-node', Language::LANGCODE_NOT_SPECIFIED); // Update the node, triggering a change in the English alias. - $node->path->pathauto = TRUE; + $node->path->pathauto = PathautoState::CREATE; $node->save(); // Check that the new English alias replaced the old one. - $this->assertEntityAlias($node, 'content/english-node-0', 'en'); - $this->assertEntityAlias($node, 'french-node', 'fr'); - $this->assertAliasExists(array('pid' => $english_alias['pid'], 'alias' => 'content/english-node-0')); + $this->assertEntityAlias($node, '/content/english-node-0', 'en'); + $this->assertEntityAlias($node, '/french-node', 'fr'); + $this->assertAliasExists(array('pid' => $english_alias['pid'], 'alias' => '/content/english-node-0')); + + // Create a new node with the same title as before but without + // specifying a language. + $node = $this->drupalCreateNode(array('title' => 'English node', 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED)); + + // Check that the new node had a unique alias generated with the '-1' + // suffix. + $this->assertEntityAlias($node, '/content/english-node-1', LanguageInterface::LANGCODE_NOT_SPECIFIED); } -} + /** + * Test that patterns work on multilingual content. + */ + function testLanguagePatterns() { + $this->drupalLogin($this->rootUser); + + // Add French language. + $edit = array( + 'predefined_langcode' => 'fr', + ); + $this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add language')); + + // Enable content translation on articles. + $this->drupalGet('admin/config/regional/content-language'); + $edit = array( + 'entity_types[node]' => TRUE, + 'settings[node][article][translatable]' => TRUE, + 'settings[node][article][settings][language][language_alterable]' => TRUE, + ); + $this->drupalPostForm(NULL, $edit, t('Save configuration')); + + // Create a pattern for English articles. + $this->drupalGet('admin/config/search/path/patterns/add'); + $edit = array( + 'type' => 'canonical_entities:node', + ); + $this->drupalPostAjaxForm(NULL, $edit, 'type'); + $edit += array( + 'pattern' => '/the-articles/[node:title]', + 'label' => 'English articles', + 'id' => 'english_articles', + 'bundles[article]' => TRUE, + 'languages[en]' => TRUE, + ); + $this->drupalPostForm(NULL, $edit, 'Save'); + $this->assertText('Pattern English articles saved.'); + + // Create a pattern for French articles. + $this->drupalGet('admin/config/search/path/patterns/add'); + $edit = array( + 'type' => 'canonical_entities:node', + ); + $this->drupalPostAjaxForm(NULL, $edit, 'type'); + $edit += array( + 'pattern' => '/les-articles/[node:title]', + 'label' => 'French articles', + 'id' => 'french_articles', + 'bundles[article]' => TRUE, + 'languages[fr]' => TRUE, + ); + $this->drupalPostForm(NULL, $edit, 'Save'); + $this->assertText('Pattern French articles saved.'); + + // Create a node and its translation. Assert aliases. + $edit = array( + 'title[0][value]' => 'English node', + 'langcode[0][value]' => 'en', + ); + $this->drupalPostForm('node/add/article', $edit, t('Save and publish')); + $english_node = $this->drupalGetNodeByTitle('English node'); + $this->assertAlias('/node/' . $english_node->id(), '/the-articles/english-node', 'en'); + + $this->drupalGet('node/' . $english_node->id() . '/translations'); + $this->clickLink(t('Add')); + $edit = array( + 'title[0][value]' => 'French node', + ); + $this->drupalPostForm(NULL, $edit, t('Save and keep published (this translation)')); + $this->rebuildContainer(); + $english_node = $this->drupalGetNodeByTitle('English node'); + $french_node = $english_node->getTranslation('fr'); + $this->assertAlias('/node/' . $french_node->id(), '/les-articles/french-node', 'fr'); + + // Bulk delete and Bulk generate patterns. Assert aliases. + $this->deleteAllAliases(); + // Bulk create aliases. + $edit = array( + 'update[canonical_entities:node]' => TRUE, + ); + $this->drupalPostForm('admin/config/search/path/update_bulk', $edit, t('Update')); + $this->assertText(t('Generated 2 URL aliases.')); + $this->assertAlias('/node/' . $english_node->id(), '/the-articles/english-node', 'en'); + $this->assertAlias('/node/' . $french_node->id(), '/les-articles/french-node', 'fr'); + } + +} diff --git a/src/Tests/PathautoMassDeleteTest.php b/src/Tests/PathautoMassDeleteTest.php new file mode 100644 index 0000000..89cdec6 --- /dev/null +++ b/src/Tests/PathautoMassDeleteTest.php @@ -0,0 +1,174 @@ +adminUser = $this->drupalCreateUser($permissions); + $this->drupalLogin($this->adminUser); + + $this->createPattern('node', '/content/[node:title]'); + $this->createPattern('user', '/users/[user:name]'); + $this->createPattern('taxonomy_term', '/[term:vocabulary]/[term:name]'); + } + + /** + * Tests the deletion of all the aliases. + */ + function testDeleteAll() { + // 1. Test that deleting all the aliases, of any type, works. + $this->generateAliases(); + $edit = array( + 'delete[all_aliases]' => TRUE, + ); + $this->drupalPostForm('admin/config/search/path/delete_bulk', $edit, t('Delete aliases now!')); + $this->assertText(t('All of your path aliases have been deleted.')); + $this->assertUrl(\Drupal::url('pathauto.admin.delete')); + + // Make sure that all of them are actually deleted. + $aliases = db_select('url_alias', 'ua')->fields('ua', array())->execute()->fetchAll(); + $this->assertEqual($aliases, array(), "All the aliases have been deleted."); + + // 2. Test deleting only specific (entity type) aliases. + $manager = $this->container->get('plugin.manager.alias_type'); + $pathauto_plugins = array('canonical_entities:node' => 'nodes', 'canonical_entities:taxonomy_term' => 'terms', 'canonical_entities:user' => 'accounts'); + foreach ($pathauto_plugins as $pathauto_plugin => $attribute) { + $this->generateAliases(); + $edit = array( + 'delete[plugins][' . $pathauto_plugin . ']' => TRUE, + ); + $this->drupalPostForm('admin/config/search/path/delete_bulk', $edit, t('Delete aliases now!')); + $alias_type = $manager->createInstance($pathauto_plugin); + $this->assertRaw(t('All of your %label path aliases have been deleted.', array('%label' => $alias_type->getLabel()))); + // Check that the aliases were actually deleted. + foreach ($this->{$attribute} as $entity) { + $this->assertNoEntityAlias($entity); + } + + // Check that the other aliases are not deleted. + foreach ($pathauto_plugins as $_pathauto_plugin => $_attribute) { + // Skip the aliases that should be deleted. + if ($_pathauto_plugin == $pathauto_plugin) { + continue; + } + foreach ($this->{$_attribute} as $entity) { + $this->assertEntityAliasExists($entity); + } + } + } + } + + /** + * Helper function to generate aliases. + */ + function generateAliases() { + // We generate a bunch of aliases for nodes, users and taxonomy terms. If + // the entities are already created we just update them, otherwise we create + // them. + if (empty($this->nodes)) { + for ($i = 1; $i <= 5; $i++) { + $node = $this->drupalCreateNode(); + $this->nodes[$node->id()] = $node; + } + } + else { + foreach ($this->nodes as $node) { + $node->save(); + } + } + + if (empty($this->accounts)) { + for ($i = 1; $i <= 5; $i++) { + $account = $this->drupalCreateUser(); + $this->accounts[$account->id()] = $account; + } + } + else { + foreach ($this->accounts as $id => $account) { + $account->save(); + } + } + + if (empty($this->terms)) { + $vocabulary = $this->addVocabulary(array('name' => 'test vocabulary', 'vid' => 'test_vocabulary')); + for ($i = 1; $i <= 5; $i++) { + $term = $this->addTerm($vocabulary); + $this->terms[$term->id()] = $term; + } + } + else { + foreach ($this->terms as $term) { + $term->save(); + } + } + + // Check that we have aliases for the entities. + foreach (array('nodes', 'accounts', 'terms') as $attribute) { + foreach ($this->{$attribute} as $entity) { + $this->assertEntityAliasExists($entity); + } + } + } + +} diff --git a/src/Tests/PathautoNodeWebTest.php b/src/Tests/PathautoNodeWebTest.php index 1e2626a..83a3fb5 100644 --- a/src/Tests/PathautoNodeWebTest.php +++ b/src/Tests/PathautoNodeWebTest.php @@ -6,6 +6,9 @@ */ namespace Drupal\pathauto\Tests; +use Drupal\pathauto\Entity\PathautoPattern; +use Drupal\node\Entity\Node; +use Drupal\pathauto\PathautoState; use Drupal\simpletest\WebTestBase; /** @@ -51,6 +54,9 @@ function setUp() { ); $this->adminUser = $this->drupalCreateUser($permissions); $this->drupalLogin($this->adminUser); + + $this->createPattern('node', '/content/[node:title]'); + } /** @@ -63,7 +69,7 @@ function testNodeEditing() { // Create a node by saving the node form. $title = ' Testing: node title ['; - $automatic_alias = 'content/testing-node-title'; + $automatic_alias = '/content/testing-node-title'; $this->drupalPostForm(NULL, array('title[0][value]' => $title), t('Save and publish')); $node = $this->drupalGetNodeByTitle($title); @@ -77,12 +83,12 @@ function testNodeEditing() { $this->assertText($title, 'Node accessible through automatic alias.'); // Manually set the node's alias. - $manual_alias = 'content/' . $node->id(); + $manual_alias = '/content/' . $node->id(); $edit = array( 'path[0][pathauto]' => FALSE, 'path[0][alias]' => $manual_alias, ); - $this->drupalPostForm("{$node->getSystemPath()}/edit", $edit, t('Save and keep published')); + $this->drupalPostForm($node->urlInfo('edit-form'), $edit, t('Save and keep published')); $this->assertRaw(t('@type %title has been updated.', array('@type' => 'page', '%title' => $title))); // Check that the automatic alias checkbox is now unchecked by default. @@ -107,19 +113,21 @@ function testNodeEditing() { $edit = array( 'title[0][value]' => $title, 'path[0][pathauto]' => TRUE, - 'path[0][alias]' => 'should-not-get-created', + 'path[0][alias]' => '/should-not-get-created', ); $this->drupalPostForm('node/add/page', $edit, t('Save and publish')); $this->assertNoAliasExists(array('alias' => 'should-not-get-created')); $node = $this->drupalGetNodeByTitle($title); - $this->assertEntityAlias($node, 'content/automatic-title'); + $this->assertEntityAlias($node, '/content/automatic-title'); // Remove the pattern for nodes, the pathauto checkbox should not be // displayed. - $config = $this->config('pathauto.pattern'); - $config->set('patterns.node.default', ''); - $config->save(); - \Drupal::service('pathauto.manager')->resetCaches(); + $ids = \Drupal::entityQuery('pathauto_pattern') + ->condition('type', 'canonical_entities:node') + ->execute(); + foreach (PathautoPattern::loadMultiple($ids) as $pattern) { + $pattern->delete(); + } $this->drupalGet('node/add/article'); $this->assertNoFieldById('edit-path-0-pathauto'); @@ -132,7 +140,7 @@ function testNodeEditing() { $node = $this->drupalGetNodeByTitle($edit['title']); // Pathauto checkbox should still not exist. - $this->drupalGet($node->getSystemPath() . '/edit'); + $this->drupalGet($node->urlInfo('edit-form')); $this->assertNoFieldById('edit-path-0-pathauto'); $this->assertFieldByName('path[0][alias]', ''); $this->assertNoEntityAlias($node); @@ -158,8 +166,91 @@ function testNodeOperations() { '%action' => 'Update URL-Alias', ))); - $this->assertEntityAlias($node1, 'content/' . $node1->getTitle()); - $this->assertEntityAlias($node2, 'node/' . $node2->id()); + $this->assertEntityAlias($node1, '/content/' . $node1->getTitle()); + $this->assertEntityAlias($node2, '/node/' . $node2->id()); + } + + /** + * @todo Merge this with existing node test methods? + */ + public function testNodeState() { + $nodeNoAliasUser = $this->drupalCreateUser(array('bypass node access')); + $nodeAliasUser = $this->drupalCreateUser(array('bypass node access', 'create url aliases')); + + $node = $this->drupalCreateNode(array( + 'title' => 'Node version one', + 'type' => 'page', + 'path' => array( + 'pathauto' => PathautoState::SKIP, + ), + )); + + $this->assertNoEntityAlias($node); + + // Set a manual path alias for the node. + $node->path->alias = '/test-alias'; + $node->save(); + + // Ensure that the pathauto field was saved to the database. + \Drupal::entityManager()->getStorage('node')->resetCache(); + $node = Node::load($node->id()); + $this->assertIdentical($node->path->pathauto, PathautoState::SKIP); + + // Ensure that the manual path alias was saved and an automatic alias was not generated. + $this->assertEntityAlias($node, '/test-alias'); + $this->assertNoEntityAliasExists($node, '/content/node-version-one'); + + // Save the node as a user who does not have access to path fieldset. + $this->drupalLogin($nodeNoAliasUser); + $this->drupalGet('node/' . $node->id() . '/edit'); + $this->assertNoFieldByName('path[0][pathauto]'); + + $edit = array('title[0][value]' => 'Node version two'); + $this->drupalPostForm(NULL, $edit, 'Save'); + $this->assertText('Basic page Node version two has been updated.'); + + $this->assertEntityAlias($node, '/test-alias'); + $this->assertNoEntityAliasExists($node, '/content/node-version-one'); + $this->assertNoEntityAliasExists($node, '/content/node-version-two'); + + // Load the edit node page and check that the Pathauto checkbox is unchecked. + $this->drupalLogin($nodeAliasUser); + $this->drupalGet('node/' . $node->id() . '/edit'); + $this->assertNoFieldChecked('edit-path-0-pathauto'); + + // Edit the manual alias and save the node. + $edit = array( + 'title[0][value]' => 'Node version three', + 'path[0][alias]' => '/manually-edited-alias', + ); + $this->drupalPostForm(NULL, $edit, 'Save'); + $this->assertText('Basic page Node version three has been updated.'); + + $this->assertEntityAlias($node, '/manually-edited-alias'); + $this->assertNoEntityAliasExists($node, '/test-alias'); + $this->assertNoEntityAliasExists($node, '/content/node-version-one'); + $this->assertNoEntityAliasExists($node, '/content/node-version-two'); + $this->assertNoEntityAliasExists($node, '/content/node-version-three'); + + // Programatically save the node with an automatic alias. + \Drupal::entityManager()->getStorage('node')->resetCache(); + $node = Node::load($node->id()); + $node->path->pathauto = PathautoState::CREATE; + $node->save(); + + // Ensure that the pathauto field was saved to the database. + \Drupal::entityManager()->getStorage('node')->resetCache(); + $node = Node::load($node->id()); + $this->assertIdentical($node->path->pathauto, PathautoState::CREATE); + + $this->assertEntityAlias($node, '/content/node-version-three'); + $this->assertNoEntityAliasExists($node, '/manually-edited-alias'); + $this->assertNoEntityAliasExists($node, '/test-alias'); + $this->assertNoEntityAliasExists($node, '/content/node-version-one'); + $this->assertNoEntityAliasExists($node, '/content/node-version-two'); + + $node->delete(); + $this->assertNull(\Drupal::keyValue('pathauto_state.node')->get($node->id()), 'Pathauto state was deleted'); } } diff --git a/src/Tests/PathautoTaxonomyWebTest.php b/src/Tests/PathautoTaxonomyWebTest.php index 46828a6..4183f27 100644 --- a/src/Tests/PathautoTaxonomyWebTest.php +++ b/src/Tests/PathautoTaxonomyWebTest.php @@ -46,6 +46,8 @@ function setUp() { ); $this->adminUser = $this->drupalCreateUser($permissions); $this->drupalLogin($this->adminUser); + + $this->createPattern('taxonomy_term', '/[term:vocabulary]/[term:name]'); } @@ -61,7 +63,7 @@ function testTermEditing() { // Create term for testing. $name = 'Testing: term name ['; - $automatic_alias = 'tags/testing-term-name'; + $automatic_alias = '/tags/testing-term-name'; $this->drupalPostForm('admin/structure/taxonomy/manage/tags/add', array('name[0][value]' => $name), 'Save'); $name = trim($name); $this->assertText("Created new term $name."); @@ -77,7 +79,7 @@ function testTermEditing() { $this->assertText($name, 'Term accessible through automatic alias.'); // Manually set the term's alias. - $manual_alias = 'tags/' . $term->id(); + $manual_alias = '/tags/' . $term->id(); $edit = array( 'path[0][pathauto]' => FALSE, 'path[0][alias]' => $manual_alias, diff --git a/src/Tests/PathautoTestHelperTrait.php b/src/Tests/PathautoTestHelperTrait.php index 304cb50..01704d5 100644 --- a/src/Tests/PathautoTestHelperTrait.php +++ b/src/Tests/PathautoTestHelperTrait.php @@ -10,6 +10,9 @@ use Drupal\Component\Utility\Unicode; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Language\Language; +use Drupal\Core\Render\BubbleableMetadata; +use Drupal\pathauto\Entity\PathautoPattern; +use Drupal\pathauto\PathautoPatternInterface; use Drupal\taxonomy\VocabularyInterface; /** @@ -17,8 +20,58 @@ */ trait PathautoTestHelperTrait { + /** + * Creates a pathauto pattern. + * + * @param string $entity_type_id + * The entity type. + * @param string $pattern + * The path pattern. + * @param int $weight + * (optional) The pattern weight. + * + * @return \Drupal\pathauto\PathautoPatternInterface + * The created pattern. + */ + protected function createPattern($entity_type_id, $pattern, $weight = 10) { + $pattern = PathautoPattern::create([ + 'id' => Unicode::strtolower($this->randomMachineName()), + 'type' => 'canonical_entities:' . $entity_type_id, + 'pattern' => $pattern, + 'weight' => $weight, + ]); + $pattern->save(); + return $pattern; + } + + /** + * Add a bundle condition to a pathauto pattern. + * + * @param \Drupal\pathauto\PathautoPatternInterface $pattern + * The pattern. + * @param string $entity_type + * The entity type ID. + * @param string $bundle + * The bundle + */ + protected function addBundleCondition(PathautoPatternInterface $pattern, $entity_type, $bundle) { + $pattern->addSelectionCondition( + [ + 'id' => 'entity_bundle:' . $entity_type, + 'bundles' => [ + $bundle => $bundle, + ], + 'negate' => FALSE, + 'context_mapping' => [ + $entity_type => $entity_type, + ] + ] + ); + } + public function assertToken($type, $object, $token, $expected) { - $tokens = \Drupal::token()->generate($type, array($token => $token), array($type => $object)); + $bubbleable_metadata = new BubbleableMetadata(); + $tokens = \Drupal::token()->generate($type, array($token => $token), array($type => $object), [], $bubbleable_metadata); $tokens += array($token => ''); $this->assertIdentical($tokens[$token], $expected, t("Token value for [@type:@token] was '@actual', expected value '@expected'.", array('@type' => $type, '@token' => $token, '@actual' => $tokens[$token], '@expected' => $expected))); } @@ -33,7 +86,7 @@ public function saveEntityAlias(EntityInterface $entity, $alias, $langcode = NUL if (!$langcode) { $langcode = $entity->language()->getId(); } - return $this->saveAlias($entity->getSystemPath(), $alias, $langcode); + return $this->saveAlias('/' . $entity->urlInfo()->getInternalPath(), $alias, $langcode); } public function assertEntityAlias(EntityInterface $entity, $expected_alias, $langcode = NULL) { @@ -41,11 +94,11 @@ public function assertEntityAlias(EntityInterface $entity, $expected_alias, $lan if (!$langcode) { $langcode = $entity->language()->getId(); } - $this->assertAlias($entity->getSystemPath(), $expected_alias, $langcode); + $this->assertAlias('/' . $entity->urlInfo()->getInternalPath(), $expected_alias, $langcode); } public function assertEntityAliasExists(EntityInterface $entity) { - return $this->assertAliasExists(array('source' => $entity->getSystemPath())); + return $this->assertAliasExists(array('source' => '/' . $entity->urlInfo()->getInternalPath())); } public function assertNoEntityAlias(EntityInterface $entity, $langcode = NULL) { @@ -53,11 +106,15 @@ public function assertNoEntityAlias(EntityInterface $entity, $langcode = NULL) { if (!$langcode) { $langcode = $entity->language()->getId(); } - $this->assertEntityAlias($entity, $entity->getSystemPath(), $langcode); + $this->assertEntityAlias($entity, '/' . $entity->urlInfo()->getInternalPath(), $langcode); } - public function assertNoEntityAliasExists(EntityInterface $entity) { - $this->assertNoAliasExists(array('source' => $entity->getSystemPath())); + public function assertNoEntityAliasExists(EntityInterface $entity, $alias = NULL) { + $path = array('source' => '/' . $entity->urlInfo()->getInternalPath()); + if (!empty($alias)) { + $path['alias'] = $alias; + } + $this->assertNoAliasExists($path); } public function assertAlias($source, $expected_alias, $langcode = Language::LANGCODE_NOT_SPECIFIED) { @@ -68,8 +125,8 @@ public function assertAlias($source, $expected_alias, $langcode = Language::LANG break; } } - $this->assertIdentical($alias['alias'], $expected_alias, t("Alias for %source with language '@language' was %actual, expected %expected.", - array('%source' => $source, '%actual' => $alias['alias'], '%expected' => $expected_alias, '@language' => $langcode))); + $this->assertIdentical($alias['alias'], $expected_alias, t("Alias for %source with language '@language' is correct.", + array('%source' => $source, '@language' => $langcode))); } public function assertAliasExists($conditions) { @@ -116,9 +173,15 @@ public function addTerm(VocabularyInterface $vocabulary, array $values = array() } public function assertEntityPattern($entity_type, $bundle, $langcode = Language::LANGCODE_NOT_SPECIFIED, $expected) { - \Drupal::service('pathauto.manager')->resetCaches(); - $pattern = \Drupal::service('pathauto.manager')->getPatternByEntity($entity_type, $bundle, $langcode); - $this->assertIdentical($expected, $pattern); + + $values = [ + 'langcode' => $langcode, + \Drupal::entityTypeManager()->getDefinition($entity_type)->getKey('bundle') => $bundle, + ]; + $entity = \Drupal::entityTypeManager()->getStorage($entity_type)->create($values); + + $pattern = \Drupal::service('pathauto.generator')->getPatternByEntity($entity); + $this->assertIdentical($expected, $pattern->getPattern()); } public function drupalGetTermByName($name, $reset = FALSE) { diff --git a/src/Tests/PathautoTokenTest.php b/src/Tests/PathautoTokenTest.php index fbca356..2ae50c9 100644 --- a/src/Tests/PathautoTokenTest.php +++ b/src/Tests/PathautoTokenTest.php @@ -7,6 +7,7 @@ namespace Drupal\pathauto\Tests; +use Drupal\Core\Render\BubbleableMetadata; use Drupal\simpletest\KernelTestBase; /** @@ -39,9 +40,9 @@ public function testPathautoTokens() { $replacements = $this->assertTokens('array', $data, $tokens); // Ensure that the cleanTokenValues() method does not alter this token value. - /* @var \Drupal\pathauto\PathautoManagerInterface $manager */ - $manager = \Drupal::service('pathauto.manager'); - $manager->cleanTokenValues($replacements, $data, array()); + /* @var \Drupal\pathauto\AliasCleanerInterface $alias_cleaner */ + $alias_cleaner = \Drupal::service('pathauto.alias_cleaner'); + $alias_cleaner->cleanTokenValues($replacements, $data, array()); $this->assertEqual($replacements['[array:join-path]'], 'test-first-arg/array-value'); } @@ -50,7 +51,8 @@ public function testPathautoTokens() { */ public function assertTokens($type, array $data, array $tokens, array $options = array()) { $input = $this->mapTokenNames($type, array_keys($tokens)); - $replacements = \Drupal::token()->generate($type, $input, $data, $options); + $bubbleable_metadata = new BubbleableMetadata(); + $replacements = \Drupal::token()->generate($type, $input, $data, $options, $bubbleable_metadata); foreach ($tokens as $name => $expected) { $token = $input[$name]; if (!isset($expected)) { diff --git a/src/Tests/PathautoUiTest.php b/src/Tests/PathautoUiTest.php index 1415aa9..b11d47d 100644 --- a/src/Tests/PathautoUiTest.php +++ b/src/Tests/PathautoUiTest.php @@ -84,20 +84,26 @@ function testSettingsValidation() { } function testPatternsValidation() { - $edit = array(); - $this->drupalGet('admin/config/search/path/patterns'); - $edit['node[default]'] = '[node:title]/[user:name]/[term:name]'; - $edit['node[bundles][page][default]'] = 'page'; - $this->drupalPostForm('admin/config/search/path/patterns', $edit, 'Save configuration'); - $this->assertText('The Default path pattern (applies to all content types with blank patterns below) is using the following invalid tokens: [user:name], [term:name].'); - $this->assertText('The Pattern for all Basic page paths cannot contain fewer than one token.'); + // Try to save an invalid pattern. + $this->drupalGet('admin/config/search/path/patterns/add'); + $edit = array( + 'type' => 'canonical_entities:node', + ); + $this->drupalPostAjaxForm(NULL, $edit, 'type'); + $edit += array( + 'pattern' => '[node:title]/[user:name]/[term:name]', + 'bundles[page]' => TRUE, + 'label' => 'Page pattern', + 'id' => 'page_pattern', + ); + $this->drupalPostForm(NULL, $edit, 'Save'); + $this->assertText('The Path pattern is using the following invalid tokens: [user:name], [term:name].'); $this->assertNoText('The configuration options have been saved.'); - $edit['node[default]'] = '[node:title]'; - $edit['node[bundles][page][default]'] = 'page/[node:title]'; - $edit['node[bundles][article][default]'] = ''; - $this->drupalPostForm('admin/config/search/path/patterns', $edit, 'Save configuration'); - $this->assertText('The configuration options have been saved.'); + // Fix the pattern, then check that it gets saved successfully. + $edit['pattern'] = '[node:title]'; + $this->drupalPostForm(NULL, $edit, 'Save'); + $this->assertText('Pattern Page pattern saved.'); } } diff --git a/src/Tests/PathautoUnitTest.php b/src/Tests/PathautoUnitTest.php index 41a2015..1a89d5e 100644 --- a/src/Tests/PathautoUnitTest.php +++ b/src/Tests/PathautoUnitTest.php @@ -7,10 +7,21 @@ namespace Drupal\pathauto\Tests; -use Drupal\Component\Utility\SafeMarkup; +use Drupal\Component\Utility\Html; +use Drupal\Component\Utility\Unicode; use Drupal\Core\Language\Language; -use Drupal\pathauto\PathautoManagerInterface; +use Drupal\Core\Language\LanguageInterface; +use Drupal\Core\Plugin\Context\Context; +use Drupal\Core\Plugin\Context\ContextDefinition; +use Drupal\language\Entity\ConfigurableLanguage; +use Drupal\field\Entity\FieldConfig; +use Drupal\field\Entity\FieldStorageConfig; +use Drupal\node\Entity\NodeType; +use Drupal\pathauto\PathautoGeneratorInterface; +use Drupal\pathauto\PathautoState; use Drupal\simpletest\KernelTestBase; +use Drupal\taxonomy\Entity\Term; +use Drupal\taxonomy\Entity\Vocabulary; /** * Unit tests for Pathauto functions. @@ -21,22 +32,41 @@ class PathautoUnitTest extends KernelTestBase { use PathautoTestHelperTrait; - public static $modules = array('system', 'entity', 'field', 'text', 'user', 'node', 'path', 'pathauto', 'taxonomy', 'token', 'menu_link', 'filter'); + public static $modules = array('system', 'field', 'text', 'user', 'node', 'path', 'pathauto', 'taxonomy', 'token', 'filter', 'ctools', 'language'); protected $currentUser; + /** + * @var \Drupal\pathauto\PathautoPatternInterface + */ + protected $nodePattern; + + /** + * @var \Drupal\pathauto\PathautoPatternInterface + */ + protected $userPattern; + public function setUp() { parent::setup(); - $this->installConfig(array('pathauto', 'taxonomy', 'system')); + $this->installConfig(array('pathauto', 'taxonomy', 'system', 'node')); $this->installEntitySchema('user'); $this->installEntitySchema('node'); $this->installEntitySchema('taxonomy_term'); + ConfigurableLanguage::createFromLangcode('fr')->save(); + $this->installSchema('node', array('node_access')); $this->installSchema('system', array('url_alias', 'sequences', 'router')); + $type = NodeType::create(['type' => 'page']); + $type->save(); + node_add_body_field($type); + + $this->nodePattern = $this->createPattern('node', '/content/[node:title]'); + $this->userPattern = $this->createPattern('user', '/users/[user:name]'); + \Drupal::service('router.builder')->rebuild(); $this->currentUser = entity_create('user', array('name' => $this->randomMachineName())); @@ -54,58 +84,85 @@ public function testGetSchemaAliasMaxLength() { * Test pathauto_pattern_load_by_entity(). */ public function testPatternLoadByEntity() { - $this->config('pathauto.pattern') - ->set('patterns.node.bundles.article.default', 'article/[node:title]') - ->set('patterns.node.bundles.article.languages.en', 'article/en/[node:title]') - ->set('patterns.node.bundles.page.default', '[node:title]') - ->save(); + $pattern = $this->createPattern('node', '/article/[node:title]', -1); + $this->addBundleCondition($pattern, 'node', 'article'); + $pattern->save(); + + $pattern = $this->createPattern('node', '/article/en/[node:title]', -2); + $this->addBundleCondition($pattern, 'node', 'article'); + $pattern->addSelectionCondition( + [ + 'id' => 'language', + 'langcodes' => [ + 'en' => 'en', + ], + 'negate' => FALSE, + 'context_mapping' => [ + 'language' => 'node:langcode:language', + ] + ] + ); + + $new_definition = new ContextDefinition('language', 'Language'); + $new_context = new Context($new_definition); + $pattern->addContext('node:langcode:language', $new_context); + $pattern->save(); + + $pattern = $this->createPattern('node', '/[node:title]', -1); + $this->addBundleCondition($pattern, 'node', 'page'); + $pattern->save(); $tests = array( array( 'entity' => 'node', - 'bundle' => 'article', - 'language' => 'fr', - 'expected' => 'article/[node:title]', + 'values' => [ + 'title' => 'Article fr', + 'type' => 'article', + 'langcode' => 'fr', + ], + 'expected' => '/article/[node:title]', ), array( 'entity' => 'node', - 'bundle' => 'article', - 'language' => 'en', - 'expected' => 'article/en/[node:title]', + 'values' => [ + 'title' => 'Article en', + 'type' => 'article', + 'langcode' => 'en', + ], + 'expected' => '/article/en/[node:title]', ), array( 'entity' => 'node', - 'bundle' => 'article', - 'language' => Language::LANGCODE_NOT_SPECIFIED, - 'expected' => 'article/[node:title]', + 'values' => [ + 'title' => 'Article und', + 'type' => 'article', + 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED, + ], + 'expected' => '/article/[node:title]', ), array( 'entity' => 'node', - 'bundle' => 'page', - 'language' => 'en', - 'expected' => '[node:title]', + 'values' => [ + 'title' => 'Page', + 'type' => 'page', + ], + 'expected' => '/[node:title]', ), array( 'entity' => 'user', - 'bundle' => 'user', - 'language' => Language::LANGCODE_NOT_SPECIFIED, - 'expected' => 'users/[user:name]', - ), - array( - 'entity' => 'invalid-entity', - 'bundle' => '', - 'language' => Language::LANGCODE_NOT_SPECIFIED, - 'expected' => '', + 'values' => [ + 'name' => 'User', + ], + 'expected' => '/users/[user:name]', ), ); foreach ($tests as $test) { - $actual = \Drupal::service('pathauto.manager')->getPatternByEntity($test['entity'], $test['bundle'], $test['language']); - $this->assertIdentical($actual, $test['expected'], t("pathauto_pattern_load_by_entity('@entity', '@bundle', '@language') returned '@actual', expected '@expected'", array( + $entity = \Drupal::entityManager()->getStorage($test['entity'])->create($test['values']); + $entity->save(); + $actual = \Drupal::service('pathauto.generator')->getPatternByEntity($entity); + $this->assertIdentical($actual->getPattern(), $test['expected'], t("Correct pattern returned for @entity_type with @values", array( '@entity' => $test['entity'], - '@bundle' => $test['bundle'], - '@language' => $test['language'], - '@actual' => $actual, - '@expected' => $test['expected'], + '@values' => print_r($test['values'], TRUE), ))); } } @@ -122,7 +179,7 @@ public function testCleanString() { $config->set('max_component_length', 35); $config->set('transliterate', TRUE); $config->save(); - \Drupal::service('pathauto.manager')->resetCaches(); + \Drupal::service('pathauto.generator')->resetCaches(); // Test the 'ignored words' removal. $tests['this'] = 'this'; @@ -134,14 +191,14 @@ public function testCleanString() { // Test that HTML tags are removed. $tests['This text has
HTML tags.'] = 'text-has-html-tags'; - $tests[SafeMarkup::checkPlain('This text has
HTML tags.')] = 'text-has-html-tags'; + $tests[Html::escape('This text has
HTML tags.')] = 'text-has-html-tags'; // Transliteration. $tests['ľščťžýáíéňô'] = 'lsctzyaieno'; foreach ($tests as $input => $expected) { - $output = \Drupal::service('pathauto.manager')->cleanString($input); - $this->assertEqual($output, $expected, t("Drupal::service('pathauto.manager')->cleanString('@input') expected '@expected', actual '@output'", array( + $output = \Drupal::service('pathauto.alias_cleaner')->cleanString($input); + $this->assertEqual($output, $expected, t("Drupal::service('pathauto.alias_cleaner')->cleanString('@input') expected '@expected', actual '@output'", array( '@input' => $input, '@expected' => $expected, '@output' => $output, @@ -154,15 +211,15 @@ public function testCleanString() { */ public function testCleanAlias() { $tests = array(); - $tests['one/two/three'] = 'one/two/three'; - $tests['/one/two/three/'] = 'one/two/three'; - $tests['one//two///three'] = 'one/two/three'; - $tests['one/two--three/-/--/-/--/four---five'] = 'one/two-three/four-five'; - $tests['one/-//three--/four'] = 'one/three/four'; + $tests['one/two/three'] = '/one/two/three'; + $tests['/one/two/three/'] = '/one/two/three'; + $tests['one//two///three'] = '/one/two/three'; + $tests['one/two--three/-/--/-/--/four---five'] = '/one/two-three/four-five'; + $tests['one/-//three--/four'] = '/one/three/four'; foreach ($tests as $input => $expected) { $output = \Drupal::service('pathauto.alias_cleaner')->cleanAlias($input); - $this->assertEqual($output, $expected, t("Drupal::service('pathauto.manager')->cleanAlias('@input') expected '@expected', actual '@output'", array( + $this->assertEqual($output, $expected, t("Drupal::service('pathauto.generator')->cleanAlias('@input') expected '@expected', actual '@output'", array( '@input' => $input, '@expected' => $expected, '@output' => $output, @@ -174,176 +231,207 @@ public function testCleanAlias() { * Test pathauto_path_delete_multiple(). */ public function testPathDeleteMultiple() { - $this->saveAlias('node/1', 'node-1-alias'); - $this->saveAlias('node/1/view', 'node-1-alias/view'); - $this->saveAlias('node/1', 'node-1-alias-en', 'en'); - $this->saveAlias('node/1', 'node-1-alias-fr', 'fr'); - $this->saveAlias('node/2', 'node-2-alias'); - - pathauto_path_delete_all('node/1'); - $this->assertNoAliasExists(array('source' => "node/1")); - $this->assertNoAliasExists(array('source' => "node/1/view")); - $this->assertAliasExists(array('source' => "node/2")); + $this->saveAlias('/node/1', '/node-1-alias'); + $this->saveAlias('/node/1/view', '/node-1-alias/view'); + $this->saveAlias('/node/1', '/node-1-alias-en', 'en'); + $this->saveAlias('/node/1', '/node-1-alias-fr', 'fr'); + $this->saveAlias('/node/2', '/node-2-alias'); + + \Drupal::service('pathauto.alias_storage_helper')->deleteAll('/node/1'); + $this->assertNoAliasExists(array('source' => "/node/1")); + $this->assertNoAliasExists(array('source' => "/node/1/view")); + $this->assertAliasExists(array('source' => "/node/2")); } /** - * Test the different update actions in \Drupal::service('pathauto.manager')->createAlias(). + * Test the different update actions in \Drupal::service('pathauto.generator')->createEntityAlias(). */ public function testUpdateActions() { $config = $this->config('pathauto.settings'); // Test PATHAUTO_UPDATE_ACTION_NO_NEW with unaliased node and 'insert'. - $config->set('update_action', PathautoManagerInterface::UPDATE_ACTION_NO_NEW); + $config->set('update_action', PathautoGeneratorInterface::UPDATE_ACTION_NO_NEW); $config->save(); $node = $this->drupalCreateNode(array('title' => 'First title')); - $this->assertEntityAlias($node, 'content/first-title'); + $this->assertEntityAlias($node, '/content/first-title'); - $node->path->pathauto = TRUE; + $node->path->pathauto = PathautoState::CREATE; // Default action is PATHAUTO_UPDATE_ACTION_DELETE. - $config->set('update_action', PathautoManagerInterface::UPDATE_ACTION_DELETE); + $config->set('update_action', PathautoGeneratorInterface::UPDATE_ACTION_DELETE); $config->save(); $node->setTitle('Second title'); $node->save(); - $this->assertEntityAlias($node, 'content/second-title'); - $this->assertNoAliasExists(array('alias' => 'content/first-title')); + $this->assertEntityAlias($node, '/content/second-title'); + $this->assertNoAliasExists(array('alias' => '/content/first-title')); // Test PATHAUTO_UPDATE_ACTION_LEAVE - $config->set('update_action', PathautoManagerInterface::UPDATE_ACTION_LEAVE); + $config->set('update_action', PathautoGeneratorInterface::UPDATE_ACTION_LEAVE); $config->save(); $node->setTitle('Third title'); $node->save(); - $this->assertEntityAlias($node, 'content/third-title'); - $this->assertAliasExists(array('source' => $node->getSystemPath(), 'alias' => 'content/second-title')); + $this->assertEntityAlias($node, '/content/third-title'); + $this->assertAliasExists(array('source' => '/' . $node->urlInfo()->getInternalPath(), 'alias' => '/content/second-title')); - $config->set('update_action', PathautoManagerInterface::UPDATE_ACTION_DELETE); + $config->set('update_action', PathautoGeneratorInterface::UPDATE_ACTION_DELETE); $config->save(); $node->setTitle('Fourth title'); $node->save(); - $this->assertEntityAlias($node, 'content/fourth-title'); - $this->assertNoAliasExists(array('alias' => 'content/third-title')); + $this->assertEntityAlias($node, '/content/fourth-title'); + $this->assertNoAliasExists(array('alias' => '/content/third-title')); // The older second alias is not deleted yet. - $older_path = $this->assertAliasExists(array('source' => $node->getSystemPath(), 'alias' => 'content/second-title')); + $older_path = $this->assertAliasExists(array('source' => '/' . $node->urlInfo()->getInternalPath(), 'alias' => '/content/second-title')); \Drupal::service('path.alias_storage')->delete($older_path); - $config->set('update_action', PathautoManagerInterface::UPDATE_ACTION_NO_NEW); + $config->set('update_action', PathautoGeneratorInterface::UPDATE_ACTION_NO_NEW); $config->save(); $node->setTitle('Fifth title'); $node->save(); - $this->assertEntityAlias($node, 'content/fourth-title'); - $this->assertNoAliasExists(array('alias' => 'content/fifth-title')); + $this->assertEntityAlias($node, '/content/fourth-title'); + $this->assertNoAliasExists(array('alias' => '/content/fifth-title')); // Test PATHAUTO_UPDATE_ACTION_NO_NEW with unaliased node and 'update'. $this->deleteAllAliases(); $node->save(); - $this->assertEntityAlias($node, 'content/fifth-title'); + $this->assertEntityAlias($node, '/content/fifth-title'); // Test PATHAUTO_UPDATE_ACTION_NO_NEW with unaliased node and 'bulkupdate'. $this->deleteAllAliases(); $node->setTitle('Sixth title'); - \Drupal::service('pathauto.manager')->updateAlias($node, 'bulkupdate'); - $this->assertEntityAlias($node, 'content/sixth-title'); + \Drupal::service('pathauto.generator')->updateEntityAlias($node, 'bulkupdate'); + $this->assertEntityAlias($node, '/content/sixth-title'); } /** - * Test that \Drupal::service('pathauto.manager')->createAlias() will not create an alias for a pattern + * Test that \Drupal::service('pathauto.generator')->createEntityAlias() will not create an alias for a pattern * that does not get any tokens replaced. */ public function testNoTokensNoAlias() { - $node = $this->drupalCreateNode(array('title' => '')); + $this->installConfig(['filter']); + $this->nodePattern + ->setPattern('/content/[node:body]') + ->save(); + + $node = $this->drupalCreateNode(); $this->assertNoEntityAliasExists($node); - $node->setTitle('hello'); + $node->body->value = 'hello'; $node->save(); - $this->assertEntityAlias($node, 'content/hello'); + $this->assertEntityAlias($node, '/content/hello'); } /** * Test the handling of path vs non-path tokens in pathauto_clean_token_values(). */ public function testPathTokens() { - $config = $this->config('pathauto.pattern'); - $config->set('patterns.taxonomy_term.default', '[term:parent:url:path]/[term:name]'); - $config->save(); + $this->createPattern('taxonomy_term', '/[term:parent:url:path]/[term:name]'); $vocab = $this->addVocabulary(); $term1 = $this->addTerm($vocab, array('name' => 'Parent term')); - $this->assertEntityAlias($term1, 'parent-term'); + $this->assertEntityAlias($term1, '/parent-term'); $term2 = $this->addTerm($vocab, array('name' => 'Child term', 'parent' => $term1->id())); - $this->assertEntityAlias($term2, 'parent-term/child-term'); + $this->assertEntityAlias($term2, '/parent-term/child-term'); - $this->saveEntityAlias($term1, 'My Crazy/Alias/'); + $this->saveEntityAlias($term1, '/My Crazy/Alias/'); $term2->save(); - $this->assertEntityAlias($term2, 'My Crazy/Alias/child-term'); + $this->assertEntityAlias($term2, '/My Crazy/Alias/child-term'); } - public function testEntityBundleRenamingDeleting() { - $config = $this->config('pathauto.pattern'); + /** + * Test using fields for path structures. + */ + function testParentChildPathTokens() { + // First create a field which will be used to create the path. It must + // begin with a letter. - // Create a vocabulary and test that it's pattern variable works. - $vocab = $this->addVocabulary(array('vid' => 'old_name')); - $config->set('patterns.taxonomy_term.default', 'base'); - $config->set('patterns.taxonomy_term.bundles.old_name.default', 'bundle'); - $config->save(); + $this->installEntitySchema('taxonomy_term'); + + Vocabulary::create(['vid' => 'tags'])->save(); + + $fieldname = 'a' . Unicode::strtolower($this->randomMachineName()); + $field_storage = FieldStorageConfig::create(['entity_type' => 'taxonomy_term', 'field_name' => $fieldname, 'type' => 'string']); + $field_storage->save(); + $field = FieldConfig::create(['field_storage' => $field_storage, 'bundle' => 'tags']); + $field->save(); - $this->assertEntityPattern('taxonomy_term', 'old_name', Language::LANGCODE_NOT_SPECIFIED, 'bundle'); + $display = entity_get_display('taxonomy_term', 'tags', 'default'); + $display->setComponent($fieldname, ['type' => 'string']); + $display->save(); - // Rename the vocabulary's machine name, which should cause its pattern - // variable to also be renamed. - $vocab->set('vid', 'new_name'); - $vocab->save(); - $this->assertEntityPattern('taxonomy_term', 'new_name', Language::LANGCODE_NOT_SPECIFIED, 'bundle'); - $this->assertEntityPattern('taxonomy_term', 'old_name', Language::LANGCODE_NOT_SPECIFIED, 'base'); + // Make the path pattern of a field use the value of this field appended + // to the parent taxonomy term's pattern if there is one. + $this->createPattern('taxonomy_term', '/[term:parents:join-path]/[term:' . $fieldname . ']'); - // Delete the vocabulary, which should cause its pattern variable to also - // be deleted. - $vocab->delete(); - $this->assertEntityPattern('taxonomy_term', 'new_name', Language::LANGCODE_NOT_SPECIFIED, 'base'); + // Start by creating a parent term. + $parent = Term::create(['vid' => 'tags', $fieldname => $this->randomMachineName(), 'name' => $this->randomMachineName()]); + $parent->save(); + + // Create the child term. + $child = Term::create(['vid' => 'tags', $fieldname => $this->randomMachineName(), 'parent' => $parent, 'name' => $this->randomMachineName()]); + $child->save(); + $this->assertEntityAlias($child, '/' . Unicode::strtolower($parent->getName() . '/' . $child->$fieldname->value)); + + // Re-saving the parent term should not modify the child term's alias. + $parent->save(); + $this->assertEntityAlias($child, '/' . Unicode::strtolower($parent->getName() . '/' . $child->$fieldname->value)); } - function testNoExistingPathAliases() { + /** + * Tests aliases on taxonomy terms. + */ + public function testTaxonomyPattern() { + // Create a vocabulary and test that it's pattern variable works. + $vocab = $this->addVocabulary(array('vid' => 'name')); + $this->createPattern('taxonomy_term', 'base'); + $pattern = $this->createPattern('taxonomy_term', 'bundle', -1); + $this->addBundleCondition($pattern, 'taxonomy_term', 'name'); + $pattern->save(); + $this->assertEntityPattern('taxonomy_term', 'name', Language::LANGCODE_NOT_SPECIFIED, 'bundle'); + } + function testNoExistingPathAliases() { $this->config('pathauto.settings') - ->set('punctuation.period', PathautoManagerInterface::PUNCTUATION_DO_NOTHING) + ->set('punctuation.period', PathautoGeneratorInterface::PUNCTUATION_DO_NOTHING) ->save(); - $this->config('pathauto.pattern') - ->set('patterns.node.bundles.page.default', '[node:title]') + + $this->nodePattern + ->setPattern('[node:title]') ->save(); - \Drupal::service('pathauto.manager')->resetCaches(); // Check that Pathauto does not create an alias of '/admin'. $node = $this->drupalCreateNode(array('title' => 'Admin', 'type' => 'page')); - $this->assertEntityAlias($node, 'admin-0'); + $this->assertEntityAlias($node, '/admin-0'); // Check that Pathauto does not create an alias of '/modules'. $node->setTitle('Modules'); $node->save(); - $this->assertEntityAlias($node, 'modules-0'); + $this->assertEntityAlias($node, '/modules-0'); // Check that Pathauto does not create an alias of '/index.php'. $node->setTitle('index.php'); $node->save(); - $this->assertEntityAlias($node, 'index.php-0'); + $this->assertEntityAlias($node, '/index.php-0'); // Check that a safe value gets an automatic alias. This is also a control // to ensure the above tests work properly. $node->setTitle('Safe value'); $node->save(); - $this->assertEntityAlias($node, 'safe-value'); + $this->assertEntityAlias($node, '/safe-value'); } /** * Test programmatic entity creation for aliases. */ function testProgrammaticEntityCreation() { + $this->createPattern('taxonomy_term', '/[term:vocabulary]/[term:name]'); $node = $this->drupalCreateNode(array('title' => 'Test node', 'path' => array('pathauto' => TRUE))); - $this->assertEntityAlias($node, 'content/test-node'); + $this->assertEntityAlias($node, '/content/test-node'); $vocabulary = $this->addVocabulary(array('name' => 'Tags')); $term = $this->addTerm($vocabulary, array('name' => 'Test term', 'path' => array('pathauto' => TRUE))); - $this->assertEntityAlias($term, 'tags/test-term'); + $this->assertEntityAlias($term, '/tags/test-term'); $edit['name'] = 'Test user'; $edit['mail'] = 'test-user@example.com'; @@ -352,9 +440,36 @@ function testProgrammaticEntityCreation() { $edit['status'] = 1; $account = entity_create('user', $edit); $account->save(); - $this->assertEntityAlias($account, 'users/test-user'); + $this->assertEntityAlias($account, '/users/test-user'); } + /** + * Tests word safe alias truncating. + */ + function testPathAliasUniquifyWordsafe() { + $this->config('pathauto.settings') + ->set('max_length', 26) + ->save(); + + $node_1 = $this->drupalCreateNode(array('title' => 'thequick brownfox jumpedover thelazydog', 'type' => 'page')); + $node_2 = $this->drupalCreateNode(array('title' => 'thequick brownfox jumpedover thelazydog', 'type' => 'page')); + + // Check that alias uniquifying is truncating with $wordsafe param set to + // TRUE. + // If it doesn't path alias result would be content/thequick-brownf-0 + $this->assertEntityAlias($node_1, '/content/thequick-brownfox'); + $this->assertEntityAlias($node_2, '/content/thequick-0'); + } + + /** + * Creates a node programmatically. + * + * @param array $settings + * The array of values for the node. + * + * @return \Drupal\node\Entity\Node + * The created node. + */ protected function drupalCreateNode(array $settings = array()) { // Populate defaults array. $settings += array( diff --git a/src/Tests/PathautoUserWebTest.php b/src/Tests/PathautoUserWebTest.php index 3b6f898..a2b30f5 100644 --- a/src/Tests/PathautoUserWebTest.php +++ b/src/Tests/PathautoUserWebTest.php @@ -48,6 +48,8 @@ function setUp() { ); $this->adminUser = $this->drupalCreateUser($permissions); $this->drupalLogin($this->adminUser); + + $this->createPattern('user', '/users/[user:name]'); } @@ -90,8 +92,8 @@ function testUserOperations() { '%action' => 'Update URL-Alias', ))); - $this->assertEntityAlias($account, 'users/' . Unicode::strtolower($account->getUsername())); - $this->assertEntityAlias($this->adminUser, 'user/' . $this->adminUser->id()); + $this->assertEntityAlias($account, '/users/' . Unicode::strtolower($account->getUsername())); + $this->assertEntityAlias($this->adminUser, '/user/' . $this->adminUser->id()); } } diff --git a/tests/src/VerboseMessengerTest.php b/tests/src/VerboseMessengerTest.php index 622de3c..e71cb3f 100644 --- a/tests/src/VerboseMessengerTest.php +++ b/tests/src/VerboseMessengerTest.php @@ -34,7 +34,6 @@ protected function setUp() { ->withAnyParameters() ->willReturn(TRUE); - // The messenger under test. $this->messenger = new VerboseMessenger($config_factory, $account); } @@ -43,16 +42,13 @@ protected function setUp() { * @covers ::addMessage */ public function testAddMessage() { - // Test adding a message. $this->assertTrue($this->messenger->addMessage("Test message"), "The message was added"); } /** - * Tests add messages. * @covers ::addMessage */ - public function testNotAddMessage() { - // Test adding a message. + public function testDoNotAddMessageWhileBulkupdate() { $this->assertFalse($this->messenger->addMessage("Test message", "bulkupdate"), "The message was NOT added"); } }