Pathauto and dependencies

This commit is contained in:
Rob Davies 2017-05-22 15:12:47 +01:00
parent 4b1a293d57
commit 24ffcb956b
257 changed files with 29510 additions and 0 deletions

View file

@ -0,0 +1,339 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.

View file

@ -0,0 +1,83 @@
#Pathauto
If you are developing for this module, have a look at pathauto.api.php.
##Description
The Pathauto module provides support functions for other modules to
automatically generate aliases based on appropriate criteria, with a
central settings path for site administrators.
Implementations are provided for core entity types: content, taxonomy terms,
and users (including blogs and forum pages).
Pathauto also provides a way to delete large numbers of aliases. This feature
is available at Administer > Configuration > Search and metadata > URL aliases >
Delete aliases.
##Benefits
Besides making the page address more reflective of its content than
"node/138", it's important to know that modern search engines give
heavy weight to search terms which appear in a page's URL. By
automatically using keywords based directly on the page content in the URL,
relevant search engine hits for your page can be significantly
enhanced.
##Notices
Pathauto just adds URL aliases to content, users, and taxonomy terms.
Because it's an alias, the standard Drupal URL (for example node/123 or
taxonomy/term/1) will still function as normal. If you have external links
to your site pointing to standard Drupal URLs, or hardcoded links in a module,
template, content or menu which point to standard Drupal URLs it will bypass
the alias set by Pathauto.
There are reasons you might not want two URLs for the same content on your
site. If this applies to you, please note that you will need to update any
hard coded links in your content or blocks.
If you use the "system path" (i.e. node/10) for menu items and settings like
that, Drupal will replace it with the url_alias.
For external links, you might want to consider the Path Redirect or
Global Redirect modules, which allow you to set forwarding either per item or
across the site to your aliased URLs.
###URLs (not) Getting Replaced With Aliases:
Please bear in mind that only URLs passed through Drupal's 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)
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.
## Installing Pathauto
1. Install the module as normal, note that there are two dependencies.
2. Configure the module at admin/config/search/path/patterns - add a new pattern by creating by clicking "Add Pathauto pattern".
3. Fill out "Path pattern" with fx [node:title], choose which content types this applies to, give it a label (the name) and save it.
4. When you save new content from now on, it should automatically be assigned an alternative URL
##Credits:
The original module combined the functionality of Mike Ryan's autopath with
Tommy Sundstrom's path_automatic.
Significant enhancements were contributed by jdmquin @ www.bcdems.net.
Matt England added the tracker support (tracker support has been removed in
recent changes).
Other suggestions and patches contributed by the Drupal community.
Current maintainers:
- Dave Reid - http://www.davereid.net
- Greg Knaddison - http://www.knaddison.com
- Mike Ryan - http://mikeryan.name
- Frederik 'Freso' S. Olesen - http://freso.dk

View file

@ -0,0 +1,13 @@
enabled_entity_types:
- user
punctuation:
hyphen: 1
verbose : FALSE
separator : '-'
max_length : 100
max_component_length: 100
transliterate : TRUE
reduce_ascii : FALSE
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

View file

@ -0,0 +1,9 @@
id: pathauto_update_alias_node
label: 'Update URL alias'
status: true
langcode: en
type: node
plugin: pathauto_update_alias
dependencies:
module:
- node

View file

@ -0,0 +1,9 @@
id: pathauto_update_alias_user
label: 'Update URL alias'
status: true
langcode: en
type: user
plugin: pathauto_update_alias
dependencies:
module:
- user

View file

@ -0,0 +1,36 @@
pathauto.settings:
type: config_object
mapping:
enabled_entity_types:
label: Enabled entity types
type: sequence
sequence:
type: string
punctuation:
type: sequence
sequence:
type: integer
verbose:
type: boolean
separator:
type: string
max_length:
type: integer
max_component_length:
type: integer
transliterate:
type: boolean
reduce_ascii:
type: boolean
ignore_words:
type: string
case:
type: boolean
ignore_words:
type: string
update_action:
type: integer
action.configuration.pathauto_update_alias:
type: action_configuration_default
label: 'Update URL alias'

View file

@ -0,0 +1,40 @@
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'
relationships:
type: sequence
label: 'Context definitions'
sequence:
- type: mapping
label: 'Relationship'
mapping:
label:
type: label
label: 'Label'

View file

@ -0,0 +1,160 @@
<?php
/**
* @file
* Documentation for pathauto API.
*/
use Drupal\Core\Language\Language;
/**
* @todo Update for 8.x-1.x
*
* It may be helpful to review some examples of integration from
* pathauto.pathauto.inc.
*
* Pathauto works by using tokens in path patterns. Thus the simplest
* integration is just to provide tokens. Token support is provided by Drupal
* core. To provide additional token from your module, implement the following
* hooks:
*
* hook_tokens() - http://api.drupal.org/api/function/hook_tokens
* hook_token_info() - http://api.drupal.org/api/function/hook_token_info
*
* If you wish to provide pathauto integration for custom paths provided by your
* module, there are a few steps involved.
*
* 1. hook_pathauto()
* Provide information required by pathauto for the settings form as well as
* bulk generation. See the documentation for hook_pathauto() for more
* details.
*
* 2. pathauto_create_alias()
* At the appropriate time (usually when a new item is being created for
* which a generated alias is desired), call pathauto_create_alias() with the
* appropriate parameters to generate and create the alias. See the user,
* taxonomy, and node hook implementations in pathauto.module for examples.
* Also see the documentation for pathauto_create_alias().
*
* 3. pathauto_path_delete_all()
* At the appropriate time (usually when an item is being deleted), call
* pathauto_path_delete_all() to remove any aliases that were created for the
* content being removed. See the documentation for
* pathauto_path_delete_all() for more details.
*
* 4. hook_path_alias_types()
* For modules that create new types of content that can be aliased with
* pathauto, a hook implementation is needed to allow the user to delete them
* all at once. See the documentation for hook_path_alias_types() below for
* more information.
*
* There are other integration points with pathauto, namely alter hooks that
* allow you to change the data used by pathauto at various points in the
* process. See the below hook documentation for details.
*/
/**
* Alter pathauto alias type definitions.
*
* @param array &$definitions
* Alias type definitions.
*/
function hook_path_alias_types_alter(array &$definitions) {
}
/**
* Determine if a possible URL alias would conflict with any existing paths.
*
* Returning TRUE from this function will trigger pathauto_alias_uniquify() to
* generate a similar URL alias with a suffix to avoid conflicts.
*
* @param string $alias
* The potential URL alias.
* @param string $source
* The source path for the alias (e.g. 'node/1').
* @param string $langcode
* The language code for the alias (e.g. 'en').
*
* @return bool
* TRUE if $alias conflicts with an existing, reserved path, or FALSE/NULL if
* it does not match any reserved paths.
*
* @see pathauto_alias_uniquify()
*/
function hook_pathauto_is_alias_reserved($alias, $source, $langcode) {
// Check our module's list of paths and return TRUE if $alias matches any of
// them.
return (bool) \Drupal::database()->query("SELECT 1 FROM {mytable} WHERE path = :path", [':path' => $alias])->fetchField();
}
/**
* Alter the pattern to be used before an alias is generated by Pathauto.
*
* This hook will only be called if a default pattern is configured (on
* admin/config/search/path/patterns).
*
* @param string $pattern
* The alias pattern for Pathauto to pass to token_replace() to generate the
* URL alias.
* @param array $context
* An associative array of additional options, with the following elements:
* - 'module': The module or entity type being aliased.
* - 'op': A string with the operation being performed on the object being
* aliased. Can be either 'insert', 'update', 'return', or 'bulkupdate'.
* - 'source': A string of the source path for the alias (e.g. 'node/1').
* - 'data': An array of keyed objects to pass to token_replace().
* - 'type': The sub-type or bundle of the object being aliased.
* - 'language': A string of the language code for the alias (e.g. 'en').
* This can be altered by reference.
*/
function hook_pathauto_pattern_alter(&$pattern, array $context) {
// Switch out any [node:created:*] tokens with [node:updated:*] on update.
if ($context['module'] == 'node' && ($context['op'] == 'update')) {
$pattern = preg_replace('/\[node:created(\:[^]]*)?\]/', '[node:updated$1]', $pattern);
}
}
/**
* Alter Pathauto-generated aliases before saving.
*
* @param string $alias
* The automatic alias after token replacement and strings cleaned.
* @param array $context
* An associative array of additional options, with the following elements:
* - 'module': The module or entity type being aliased.
* - 'op': A string with the operation being performed on the object being
* aliased. Can be either 'insert', 'update', 'return', or 'bulkupdate'.
* - 'source': A string of the source path for the alias (e.g. 'node/1').
* This can be altered by reference.
* - 'data': An array of keyed objects to pass to token_replace().
* - 'type': The sub-type or bundle of the object being aliased.
* - 'language': A string of the language code for the alias (e.g. 'en').
* This can be altered by reference.
* - 'pattern': A string of the pattern used for aliasing the object.
*/
function hook_pathauto_alias_alter(&$alias, array &$context) {
// Add a suffix so that all aliases get saved as 'content/my-title.html'
$alias .= '.html';
// Force all aliases to be saved as language neutral.
$context['language'] = Language::LANGCODE_NOT_SPECIFIED;
}
/**
* Alter the list of punctuation characters for Pathauto control.
*
* @param $punctuation
* An array of punctuation to be controlled by Pathauto during replacement
* keyed by punctuation name. Each punctuation record should be an array
* with the following key/value pairs:
* - value: The raw value of the punctuation mark.
* - name: The human-readable name of the punctuation mark. This must be
* translated using t() already.
*/
function hook_pathauto_punctuation_chars_alter(array &$punctuation) {
// Add the trademark symbol.
$punctuation['trademark'] = array('value' => '™', 'name' => t('Trademark symbol'));
// Remove the dollar sign.
unset($punctuation['dollar']);
}

View file

@ -0,0 +1,20 @@
name : 'Pathauto'
description : 'Provides a mechanism for modules to automatically generate aliases for the content they manage.'
# core: 8.x
type: module
dependencies:
- ctools:ctools
- drupal:path
- token:token
configure: entity.pathauto_pattern.collection
recommends:
- redirect:redirect
# Information added by Drupal.org packaging script on 2017-04-29
version: '8.x-1.0'
core: '8.x'
project: 'pathauto'
datestamp: 1493468049

View file

@ -0,0 +1,301 @@
<?php
/**
* @file
* Install, update, and uninstall functions for Pathauto.
*
* @ingroup pathauto
*/
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Core\Plugin\Context\Context;
use Drupal\Core\Plugin\Context\ContextDefinition;
use Drupal\pathauto\Entity\PathautoPattern;
/**
* Implements hook_install().
*/
function pathauto_install() {
// Set the weight to 1
module_set_weight('pathauto', 1);
// Ensure the url_alias table exists.
_pathauto_ensure_url_alias_table_exists();
}
/**
* Helper function to ensure the url_alias table exists.
*
* Only necessary on Drupal 8.1.x.
*
* @see https://www.drupal.org/node/2704821
*/
function _pathauto_ensure_url_alias_table_exists() {
$alias_storage = \Drupal::service('path.alias_storage');
if (method_exists($alias_storage, 'schemaDefinition')) {
$database_schema = \Drupal::database()->schema();
if (!$database_schema->tableExists($alias_storage::TABLE)) {
$schema_definition = $alias_storage->schemaDefinition();
$database_schema->createTable($alias_storage::TABLE, $schema_definition);
}
}
}
/**
* Updates pathauto widgets to use the path widget ID.
*/
function pathauto_update_8001() {
// Replace values in the 'entity.definitions.installed' keyvalue collection.
$collection = \Drupal::service('keyvalue')->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() {
\Drupal::service('module_installer')->install(['ctools']);
$messages = array();
/** @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_bundle_info */
$entity_bundle_info = \Drupal::service('entity_type.bundle.info');
$entity_type_manager = \Drupal::entityTypeManager();
$language_manager = \Drupal::languageManager();
$entity_type_manager->clearCachedDefinitions();
\Drupal::service('plugin.manager.alias_type')->clearCachedDefinitions();
$entity_types = $entity_type_manager->getDefinitions();
// 1. Load all patterns.
$config = \Drupal::configFactory()->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 (!empty($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_bundle_info->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 => $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;
$extracted_bundle = NULL;
$language = 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.
$pattern->addRelationship($language_mapping, 'Language');
$pattern->save();
}
}
}
}
// 3. Delete the old configuration object that stores patterns.
$config->delete();
// 4. Print out messages.
if (!empty($messages)) {
return implode('</br>', $messages);
}
}
/**
* Update relationship storage.
*/
function pathauto_update_8101() {
foreach (\Drupal::configFactory()->listAll('pathauto.pattern.') as $pattern_config_name) {
$pattern_config = \Drupal::configFactory()->getEditable($pattern_config_name);
$relationships = [];
foreach ((array) $pattern_config->get('context_definitions') as $context_definition) {
$relationships[$context_definition['id']] = ['label' => $context_definition['label']];
}
$pattern_config->clear('context_definitions');
$pattern_config->set('relationships', $relationships);
$pattern_config->save();
}
}
/**
* Update node type conditions from entity_bundle to node_type.
*/
function pathauto_update_8102() {
// Load all pattern configuration entities.
foreach (\Drupal::configFactory()->listAll('pathauto.pattern.') as $pattern_config_name) {
$pattern_config = \Drupal::configFactory()->getEditable($pattern_config_name);
// Loop patterns and swap the entity_bundle:node plugin by the node_type
// plugin.
if ($pattern_config->get('type') == 'canonical_entities:node') {
$selection_criteria = $pattern_config->get('selection_criteria');
foreach ($selection_criteria as $uuid => $condition) {
if ($condition['id'] == 'entity_bundle:node') {
$selection_criteria[$uuid]['id'] = 'node_type';
$pattern_config->set('selection_criteria', $selection_criteria);
$pattern_config->save();
break;
}
}
}
}
}
/**
* Fix invalid default value for ignore_words.
*/
function pathauto_update_8103() {
$config_factory = \Drupal::configFactory();
$config = $config_factory->getEditable('pathauto.settings');
$ignore_words = $config->get('ignore_words');
if ($ignore_words === ', in, is,that, the , this, with, ') {
$config->set('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')->save(TRUE);
}
}
/**
* Resave patterns so that lookup keys are updated.
*/
function pathauto_update_8104() {
\Drupal::entityTypeManager()->clearCachedDefinitions();
// Load all pattern configuration entities and save them, so that the new
// status lookup keys are saved.
foreach (\Drupal::configFactory()->listAll('pathauto.pattern.') as $pattern_config_name) {
$pattern_config = \Drupal::configFactory()->getEditable($pattern_config_name);
$pattern_config->save();
}
}
/**
* Ensure the url_alias table exists.
*/
function pathauto_update_8105() {
_pathauto_ensure_url_alias_table_exists();
}
/**
* Update default configuration for enabled entity types.
*/
function pathauto_update_8106() {
$config_factory = \Drupal::configFactory();
$config = $config_factory->getEditable('pathauto.settings');
$config->set('enabled_entity_types', ['user']);
$config->save();
}

View file

@ -0,0 +1,21 @@
(function ($) {
'use strict';
Drupal.behaviors.pathFieldsetSummaries = {
attach: function (context) {
$('fieldset.path-form', context).drupalSetSummary(function (context) {
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');
}
else if (path) {
return Drupal.t('Alias: @alias', {'@alias': path});
}
else {
return Drupal.t('No alias');
}
});
}
};
})(jQuery);

View file

@ -0,0 +1,9 @@
widget:
version: 1.0
js:
pathauto.js: {}
dependencies:
- core/jquery
- core/drupal
- core/drupalSettings
- core/drupal.form

View file

@ -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

View file

@ -0,0 +1,23 @@
pathauto.patterns.form:
route_name: entity.pathauto_pattern.collection
base_route: path.admin_overview
title: 'Patterns'
weight: 10
pathauto.settings.form:
route_name: pathauto.settings.form
base_route: path.admin_overview
title: 'Settings'
weight: 20
pathauto.bulk.update.form:
route_name: pathauto.bulk.update.form
base_route: path.admin_overview
title: 'Bulk generate'
weight: 30
pathauto.admin.delete:
route_name: pathauto.admin.delete
base_route: path.admin_overview
title: 'Delete aliases'
weight: 40

View file

@ -0,0 +1,188 @@
<?php
/**
* @file
* @defgroup pathauto Pathauto: Automatically generates aliases for content
*
* The Pathauto module automatically generates path aliases for various kinds of
* content (nodes, categories, users) without requiring the user to manually
* specify the path alias. This allows you to get aliases like
* /category/my-node-title.html instead of /node/123. The aliases are based upon
* a "pattern" system which the administrator can control.
*/
/**
* @file
* Main file for the Pathauto module, which automatically generates aliases for content.
*
* @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\Form\FormStateInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Url;
use Drupal\pathauto\PathautoState;
/**
* The default ignore word list.
*/
define('PATHAUTO_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');
/**
* Implements hook_hook_info().
*/
function pathauto_hook_info() {
$hooks = array(
'pathauto_pattern_alter',
'pathauto_alias_alter',
'pathauto_is_alias_reserved',
);
return array_fill_keys($hooks, array('group' => 'pathauto'));
}
/**
* Implements hook_help().
*/
function pathauto_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
case 'help.page.pathauto':
$output = '<h3>' . t('About') . '</h3>';
$output .= '<p>' . t('The Pathauto module provides a mechanism to automate the creation of <a href="path">path</a> aliases. This makes URLs more readable and helps search engines index content more effectively. For more information, see the <a href=":online">online documentation for Pathauto</a>.', [':online' => 'https://www.drupal.org/documentation/modules/pathauto']) . '</p>';
$output .= '<dl>';
$output .= '<h3>' . t('Uses') . '</h3>';
$output .= '<dd>' . t('Pathauto is accessed from the tabs it adds to the list of <a href=":aliases">URL aliases</a>.', [':aliases' => Url::fromRoute('path.admin_overview')->toString()]) . '</dd>';
$output .= '<dt>' . t('Creating Pathauto Patterns') . '</dt>';
$output .= '<dd>' . t('The <a href=":pathauto_pattern">"Patterns"</a> page is used to configure automatic path aliasing. New patterns are created here using the <a href=":add_form">Add Pathauto pattern</a> button which presents a form to simplify pattern creation thru the use of <a href="token">available tokens</a>. The patterns page provides a list of all patterns on the site and allows you to edit and reorder them. An alias is generated for the first pattern that applies.', [':pathauto_pattern' => Url::fromRoute('entity.pathauto_pattern.collection')->toString(), ':add_form' => Url::fromRoute('entity.pathauto_pattern.add_form')->toString()]) . '</dd>';
$output .= '<dt>' . t('Pathauto Settings') . '</dt>';
$output .= '<dd>' . t('The <a href=":settings">"Settings"</a> page is used to customize global Pathauto settings for automated pattern creation.', [':settings' => Url::fromRoute('pathauto.settings.form')->toString()]) . '</dd>';
$output .= '<dd>' . t('The <strong>maximum alias length</strong> and <strong>maximum component length</strong> values default to 100 and have a limit of @max from Pathauto. You should enter a value that is the length of the "alias" column of the url_alias database table minus the length of any strings that might get added to the end of the URL. The recommended and default value is 100.', array('@max' => \Drupal::service('pathauto.alias_storage_helper')->getAliasSchemaMaxlength())) . '</dd>';
$output .= '<dt>' . t('Bulk Generation') . '</dt>';
$output .= '<dd>' . t('The <a href=":pathauto_bulk">"Bulk Generate"</a> page allows you to create URL aliases for items that currently have no aliases. This is typically used when installing Pathauto on a site that has existing un-aliased content that needs to be aliased in bulk.', [':pathauto_bulk' => Url::fromRoute('pathauto.bulk.update.form')->toString()]) . '</dd>';
$output .= '<dt>' . t('Delete Aliases') . '</dt>';
$output .= '<dd>' . t('The <a href=":pathauto_delete">"Delete Aliases"</a> page allows you to remove URL aliases from items that have previously been assigned aliases using pathauto.', [':pathauto_delete' => Url::fromRoute('pathauto.admin.delete')->toString()]) . '</dd>';
$output .= '</dl>';
return $output;
case 'entity.pathauto_pattern.collection':
$output = '<p>' . t('This page provides a list of all patterns on the site and allows you to edit and reorder them.') . '</p>';
return $output;
case 'entity.pathauto_pattern.add_form':
$output = '<p>' . t('You need to select a pattern type, then a pattern and filter, and a label. Additional types can be enabled on the <a href=":settings">Settings</a> page.', [':settings' => Url::fromRoute('pathauto.settings.form')->toString()]) . '</p>';
return $output;
case 'pathauto.bulk.update.form':
$output = '<p>' . t('Bulk generation can be used to generate URL aliases for items that currently have no aliases. This is typically used when installing Pathauto on a site that has existing un-aliased content that needs to be aliased in bulk.') . '<br>';
$output .= t('It can also be used to regenerate URL aliases for items that have an old alias and for which the Pathauto pattern has been changed.') . '</p>';
$output .= '<p>' . t('Note that this will only affect items which are configured to have their URL alias automatically set. Items whose URL alias is manually set are not affected.') . '</p>';
return $output;
}
}
/**
* Implements hook_entity_insert().
*/
function pathauto_entity_insert(EntityInterface $entity) {
\Drupal::service('pathauto.generator')->updateEntityAlias($entity, 'insert');
}
/**
* Implements hook_entity_update().
*/
function pathauto_entity_update(EntityInterface $entity) {
\Drupal::service('pathauto.generator')->updateEntityAlias($entity, 'update');
}
/**
* Implements hook_entity_update().
*/
function pathauto_entity_delete(EntityInterface $entity) {
if ($entity->hasLinkTemplate('canonical') && $entity instanceof ContentEntityInterface && $entity->hasField('path')) {
\Drupal::service('pathauto.alias_storage_helper')->deleteEntityPathAll($entity);
$entity->path->first()->get('pathauto')->purge();
}
}
/**
* Implements hook_field_info_alter().
*/
function pathauto_field_info_alter(&$info) {
$info['path']['class'] = '\Drupal\pathauto\PathautoItem';
}
/**
* Implements hook_field_widget_info_alter().
*/
function pathauto_field_widget_info_alter(&$widgets) {
$widgets['path']['class'] = 'Drupal\pathauto\PathautoWidget';
}
/**
* Implements hook_entity_base_field_info().
*/
function pathauto_entity_base_field_info(EntityTypeInterface $entity_type) {
$config = \Drupal::config('pathauto.settings');
// Verify that the configuration data isn't null (as is the case before the
// module's initialization, in tests), so that in_array() won't fail.
if ($enabled_entity_types = $config->get('enabled_entity_types')) {
if (in_array($entity_type->id(), $enabled_entity_types)) {
$fields['path'] = BaseFieldDefinition::create('path')
->setCustomStorage(TRUE)
->setLabel(t('URL alias'))
->setTranslatable(TRUE)
->setComputed(TRUE)
->setDisplayOptions('form', array(
'type' => 'path',
'weight' => 30,
))
->setDisplayConfigurable('form', TRUE);
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);
}
}
/**
* Validate the pattern field, to ensure it doesn't contain any characters that
* are invalid in URLs.
*/
function pathauto_pattern_validate($element, FormStateInterface $form_state) {
if (isset($element['#value'])) {
$title = empty($element['#title']) ? $element['#parents'][0] : $element['#title'];
$invalid_characters = ['#', '?', '&'];
$invalid_characters_used = [];
foreach ($invalid_characters as $invalid_character) {
if (strpos($element['#value'], $invalid_character) !== FALSE) {
$invalid_characters_used[] = $invalid_character;
}
}
if (!empty($invalid_characters_used)) {
$form_state->setError($element, t('The %element-title is using the following invalid characters: @invalid-characters.', array('%element-title' => $title, '@invalid-characters' => implode(', ', $invalid_characters_used))));
}
if (preg_match('/(\s$)+/', $element['#value'])) {
$form_state->setError($element, t('The %element-title doesn\'t allow the patterns ending with whitespace.', array('%element-title' => $title)));
}
}
return $element;
}

View file

@ -0,0 +1,6 @@
administer pathauto:
title: 'Administer pathauto'
description: 'Allows a user to configure patterns for automated aliases and bulk delete URL-aliases.'
notify of path changes:
title: 'Notify of Path Changes'
description: 'Determines whether or not users are notified.'

View file

@ -0,0 +1,54 @@
entity.pathauto_pattern.collection:
path: '/admin/config/search/path/patterns'
defaults:
_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'
defaults:
_form: '\Drupal\pathauto\Form\PathautoSettingsForm'
_title: 'Settings'
requirements:
_permission: 'administer pathauto'
entity.pathauto_pattern.enable:
path: '/admin/config/search/path/patterns/{pathauto_pattern}/enable'
defaults:
_entity_form: 'pathauto_pattern.enable'
requirements:
_entity_access: 'pathauto_pattern.update'
entity.pathauto_pattern.disable:
path: '/admin/config/search/path/patterns/{pathauto_pattern}/disable'
defaults:
_entity_form: 'pathauto_pattern.disable'
requirements:
_entity_access: 'pathauto_pattern.update'
pathauto.bulk.update.form:
path: '/admin/config/search/path/update_bulk'
defaults:
_form: '\Drupal\pathauto\Form\PathautoBulkUpdateForm'
_title: 'Bulk generate'
requirements:
_permission: 'administer url aliases'
pathauto.admin.delete:
path: '/admin/config/search/path/delete_bulk'
defaults:
_form: '\Drupal\pathauto\Form\PathautoAdminDelete'
_title: 'Delete aliases'
requirements:
_permission: 'administer url aliases'

View file

@ -0,0 +1,26 @@
services:
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', '@entity_type.manager']
pathauto.alias_cleaner:
class: Drupal\pathauto\AliasCleaner
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']
tags:
- { name: backend_overridable }
pathauto.alias_uniquifier:
class: Drupal\pathauto\AliasUniquifier
arguments: ['@config.factory', '@pathauto.alias_storage_helper','@module_handler', '@router.route_provider', '@path.alias_manager']
pathauto.verbose_messenger:
class: Drupal\pathauto\VerboseMessenger
arguments: ['@config.factory', '@current_user']
plugin.manager.alias_type:
class: Drupal\pathauto\AliasTypeManager
parent: default_plugin_manager
pathauto.settings_cache_tag:
class: Drupal\pathauto\EventSubscriber\PathautoSettingsCacheTag
arguments: ['@entity_field.manager', '@plugin.manager.alias_type']
tags:
- { name: event_subscriber }

View file

@ -0,0 +1,49 @@
<?php
/**
* @file
* Token integration for the Pathauto module.
*/
use Drupal\Core\Render\BubbleableMetadata;
/**
* Implements hook_token_info().
*/
function pathauto_token_info() {
$info = array();
$info['tokens']['array']['join-path'] = array(
'name' => t('Joined path'),
'description' => t('The array values each cleaned by Pathauto and then joined with the slash into a string that resembles an URL.'),
);
return $info;
}
/**
* Implements hook_tokens().
*/
function pathauto_tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) {
$replacements = array();
if ($type == 'array' && !empty($data['array'])) {
$array = $data['array'];
foreach ($tokens as $name => $original) {
switch ($name) {
case 'join-path':
$values = array();
foreach (token_element_children($array) as $key) {
$value = is_array($array[$key]) ? render($array[$key]) : (string) $array[$key];
$value = \Drupal::service('pathauto.alias_cleaner')->cleanString($value, $options);
$values[] = $value;
}
$replacements[$original] = implode('/', $values);
break;
}
}
}
return $replacements;
}

View file

@ -0,0 +1,351 @@
<?php
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.
*/
class AliasCleaner implements AliasCleanerInterface {
/**
* The config factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* The alias storage helper.
*
* @var AliasStorageHelperInterface
*/
protected $aliasStorageHelper;
/**
* Language manager.
*
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
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.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* 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, 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) {
$config = $this->configFactory->get('pathauto.settings');
$alias_max_length = min($config->get('max_length'), $this->aliasStorageHelper->getAliasSchemaMaxLength());
$output = $alias;
// Trim duplicate, leading, and trailing separators. Do this before cleaning
// backslashes since a pattern like "[token1]/[token2]-[token3]/[token4]"
// could end up like "value1/-/value2" and if backslashes were cleaned first
// this would result in a duplicate blackslash.
$output = $this->getCleanSeparators($output);
// Trim duplicate, leading, and trailing backslashes.
$output = $this->getCleanSeparators($output, '/');
// Shorten to a logical place based on word boundaries.
$output = Unicode::truncate($output, $alias_max_length, TRUE);
return $output;
}
/**
* {@inheritdoc}
*/
public function getCleanSeparators($string, $separator = NULL) {
$config = $this->configFactory->get('pathauto.settings');
if (!isset($separator)) {
$separator = $config->get('separator');
}
$output = $string;
if (strlen($separator)) {
// Trim any leading or trailing separators.
$output = trim($output, $separator);
// Escape the separator for use in regular expressions.
$seppattern = preg_quote($separator, '/');
// Replace multiple separators with a single one.
$output = preg_replace("/$seppattern+/", $separator, $output);
// Replace trailing separators around slashes.
if ($separator !== '/') {
$output = preg_replace("/\/+$seppattern\/+|$seppattern\/+|\/+$seppattern/", "/", $output);
}
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:
$this->cleanStringCache['punctuation'][$details['value']] = '';
break;
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();
}
}

View file

@ -0,0 +1,103 @@
<?php
namespace Drupal\pathauto;
/**
* @todo add class comment.
*/
interface AliasCleanerInterface {
/**
* Clean up an URL alias.
*
* Performs the following alterations:
* - Trim duplicate, leading, and trailing back-slashes.
* - Trim duplicate, leading, and trailing separators.
* - Shorten to a desired length and logical position based on word boundaries.
*
* @param string $alias
* A string with the URL alias to clean up.
*
* @return string
* The cleaned URL alias.
*/
public function cleanAlias($alias);
/**
* Trims duplicate, leading, and trailing separators from a string.
*
* @param string $string
* The string to clean path separators from.
* @param string $separator
* The path separator to use when cleaning.
*
* @return string
* The cleaned version of the string.
*
* @see pathauto_cleanstring()
* @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();
}

View file

@ -0,0 +1,255 @@
<?php
namespace Drupal\pathauto;
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;
use Drupal\Core\StringTranslation\TranslationInterface;
/**
* Provides helper methods for accessing alias storage.
*/
class AliasStorageHelper implements AliasStorageHelperInterface {
use StringTranslationTrait;
/**
* Alias schema max length.
*
* @var int
*/
protected $aliasSchemaMaxLength = 255;
/**
* Config factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* The alias storage.
*
* @var \Drupal\Core\Path\AliasStorageInterface
*/
protected $aliasStorage;
/**
* The database connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected $database;
/**
* The messenger.
*
* @var \Drupal\pathauto\MessengerInterface
*/
protected $messenger;
/**
* The config factory.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
* @param \Drupal\Core\Path\AliasStorageInterface $alias_storage
* The alias storage.
* @param \Drupal\Core\Database\Connection $database
* The database connection.
* @param MessengerInterface $messenger
* The messenger.
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* The string translation service.
*/
public function __construct(ConfigFactoryInterface $config_factory, AliasStorageInterface $alias_storage, Connection $database, MessengerInterface $messenger, TranslationInterface $string_translation) {
$this->configFactory = $config_factory;
$this->aliasStorage = $alias_storage;
$this->database = $database;
$this->messenger = $messenger;
$this->stringTranslation = $string_translation;
}
/**
* {@inheritdoc}
*/
public function getAliasSchemaMaxLength() {
return $this->aliasSchemaMaxLength;
}
/**
* {@inheritdoc}
*/
public function save(array $path, $existing_alias = NULL, $op = NULL) {
$config = $this->configFactory->get('pathauto.settings');
// Alert users if they are trying to create an alias that is the same as the
// internal path.
if ($path['source'] == $path['alias']) {
$this->messenger->addMessage($this->t('Ignoring alias %alias because it is the same as the internal path.', array('%alias' => $path['alias'])));
return NULL;
}
// Skip replacing the current alias with an identical alias.
if (empty($existing_alias) || $existing_alias['alias'] != $path['alias']) {
$path += array(
'pathauto' => TRUE,
'original' => $existing_alias,
'pid' => NULL,
);
// If there is already an alias, respect some update actions.
if (!empty($existing_alias)) {
switch ($config->get('update_action')) {
case PathautoGeneratorInterface::UPDATE_ACTION_NO_NEW:
// Do not create the alias.
return NULL;
case PathautoGeneratorInterface::UPDATE_ACTION_LEAVE:
// Create a new alias instead of overwriting the existing by leaving
// $path['pid'] empty.
break;
case PathautoGeneratorInterface::UPDATE_ACTION_DELETE:
// The delete actions should overwrite the existing alias.
$path['pid'] = $existing_alias['pid'];
break;
}
}
// Save the path array.
$this->aliasStorage->save($path['source'], $path['alias'], $path['language'], $path['pid']);
if (!empty($existing_alias['pid'])) {
$this->messenger->addMessage($this->t(
'Created new alias %alias for %source, replacing %old_alias.',
array(
'%alias' => $path['alias'],
'%source' => $path['source'],
'%old_alias' => $existing_alias['alias'],
)
)
);
}
else {
$this->messenger->addMessage($this->t('Created new alias %alias for %source.', array(
'%alias' => $path['alias'],
'%source' => $path['source'],
)));
}
return $path;
}
}
/**
* {@inheritdoc}
*/
public function loadBySource($source, $language = LanguageInterface::LANGCODE_NOT_SPECIFIED) {
$alias = $this->aliasStorage->load([
'source' => $source,
'langcode' => $language,
]);
// If no alias was fetched and if a language was specified, fallbacks to
// undefined language.
if (!$alias && ($language !== LanguageInterface::LANGCODE_NOT_SPECIFIED)) {
$alias = $this->aliasStorage->load([
'source' => $source,
'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
]);
}
return $alias;
}
/**
* {@inheritdoc}
*/
public function deleteBySourcePrefix($source) {
$pids = $this->loadBySourcePrefix($source);
if ($pids) {
$this->deleteMultiple($pids);
}
}
/**
* {@inheritdoc}
*/
public function deleteAll() {
$this->database->truncate('url_alias')->execute();
}
/**
* {@inheritdoc}
*/
public function deleteEntityPathAll(EntityInterface $entity, $default_uri = NULL) {
$this->deleteBySourcePrefix('/' . $entity->toUrl('canonical')->getInternalPath());
if (isset($default_uri) && $entity->toUrl('canonical')->toString() != $default_uri) {
$this->deleteBySourcePrefix($default_uri);
}
}
/**
* {@inheritdoc}
*/
public function loadBySourcePrefix($source) {
$select = $this->database->select('url_alias', 'u')
->fields('u', array('pid'));
$or_group = $select->orConditionGroup()
->condition('source', $source)
->condition('source', rtrim($source, '/') . '/%', 'LIKE');
return $select
->condition($or_group)
->execute()
->fetchCol();
}
/**
* {@inheritdoc}
*/
public function countBySourcePrefix($source) {
$select = $this->database->select('url_alias', 'u')
->fields('u', array('pid'));
$or_group = $select->orConditionGroup()
->condition('source', $source)
->condition('source', rtrim($source, '/') . '/%', 'LIKE');
return $select
->condition($or_group)
->countQuery()
->execute()
->fetchField();
}
/**
* {@inheritdoc}
*/
public function countAll() {
return $this->database->select('url_alias')
->countQuery()
->execute()
->fetchField();
}
/**
* Delete multiple URL aliases.
*
* Intent of this is to abstract a potential path_delete_multiple() function
* for Drupal 7 or 8.
*
* @param int[] $pids
* An array of path IDs to delete.
*/
public function deleteMultiple($pids) {
foreach ($pids as $pid) {
$this->aliasStorage->delete(array('pid' => $pid));
}
}
}

View file

@ -0,0 +1,117 @@
<?php
namespace Drupal\pathauto;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Language\LanguageInterface;
/**
* Provides helper methods for accessing alias storage.
*/
interface AliasStorageHelperInterface {
/**
* Fetch the maximum length of the {url_alias}.alias field from the schema.
*
* @return int
* An integer of the maximum URL alias length allowed by the database.
*/
public function getAliasSchemaMaxLength();
/**
* Private function for Pathauto to create an alias.
*
* @param array $path
* An associative array containing the following keys:
* - source: The internal system path.
* - alias: The URL alias.
* - pid: (optional) Unique path alias identifier.
* - language: (optional) The language of the alias.
* @param array|bool|null $existing_alias
* (optional) An associative array of the existing path alias.
* @param string $op
* An optional string with the operation being performed.
*
* @return array|bool
* The saved path or NULL if the path was not saved.
*/
public function save(array $path, $existing_alias = NULL, $op = NULL);
/**
* Fetches an existing URL alias given a path and optional language.
*
* @param string $source
* An internal Drupal path.
* @param string $language
* An optional language code to look up the path in.
*
* @return bool|array
* FALSE if no alias was found or an associative array containing the
* following keys:
* - source (string): The internal system path with a starting slash.
* - alias (string): The URL alias with a starting slash.
* - pid (int): Unique path alias identifier.
* - langcode (string): The language code of the alias.
*/
public function loadBySource($source, $language = LanguageInterface::LANGCODE_NOT_SPECIFIED);
/**
* Delete all aliases by source url.
*
* @param string $source
* An internal Drupal path.
*
* @return bool
* The URL alias source.
*/
public function deleteBySourcePrefix($source);
/**
* Delete all aliases (truncate the url_alias table).
*/
public function deleteAll();
/**
* 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);
/**
* 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);
/**
* Returns the count of url aliases for the source.
*
* @param $source
* An internal Drupal path prefix.
*
* @return int
* Number of url aliases for the source.
*/
public function countBySourcePrefix($source);
/**
* Returns the total count of the url aliases.
*
* @return int
* Total number of aliases.
*/
public function countAll();
}

View file

@ -0,0 +1,31 @@
<?php
namespace Drupal\pathauto;
/**
* Alias types that support batch updates and deletions.
*/
interface AliasTypeBatchUpdateInterface extends AliasTypeInterface {
/**
* Gets called to batch update all entries.
*
* @param string $action
* One of:
* - 'create' to generate a URL alias for paths having none.
* - 'update' to recreate the URL alias for paths already having one, useful if the pattern changed.
* - 'all' to do both actions above at the same time.
* @param array $context
* Batch context.
*/
public function batchUpdate($action, &$context);
/**
* Gets called to batch delete all aliases created by pathauto.
*
* @param array $context
* Batch context.
*/
public function batchDelete(&$context);
}

View file

@ -0,0 +1,48 @@
<?php
namespace Drupal\pathauto;
use Drupal\Component\Plugin\DerivativeInspectionInterface;
use Drupal\Core\Plugin\ContextAwarePluginInterface;
/**
* Provides an interface for pathauto alias types.
*/
interface AliasTypeInterface extends ContextAwarePluginInterface, DerivativeInspectionInterface {
/**
* Get the label.
*
* @return string
* The label.
*/
public function getLabel();
/**
* Get the token types.
*
* @return string[]
* The token types.
*/
public function getTokenTypes();
/**
* Returns the source prefix; used for bulk delete.
*
* @return string
* The source path prefix.
*/
public function getSourcePrefix();
/**
* Determines if this plugin type can apply a given object.
*
* @param object $object
* The object used to determine if this plugin can apply.
*
* @return bool
* Whether this plugin applies to the given object.
*/
public function applies($object);
}

View file

@ -0,0 +1,71 @@
<?php
namespace Drupal\pathauto;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
use Drupal\Component\Plugin\FallbackPluginManagerInterface;
/**
* Manages pathauto alias type plugins.
*/
class AliasTypeManager extends DefaultPluginManager implements FallbackPluginManagerInterface {
/**
* Constructs a new AliasType manager instance.
*
* @param \Traversable $namespaces
* An object that implements \Traversable which contains the root paths
* keyed by the corresponding namespace to look for plugin implementations.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
* Cache backend instance to use.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler to invoke the alter hook with.
*/
public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
parent::__construct('Plugin/pathauto/AliasType', $namespaces, $module_handler, 'Drupal\pathauto\AliasTypeInterface', 'Drupal\pathauto\Annotation\AliasType');
$this->alterInfo('pathauto_alias_types');
$this->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;
}
/**
* {@inheritdoc}
*/
public function getFallbackPluginId($plugin_id, array $configuration = array()) {
return 'broken';
}
/**
* Gets the definition of all visible plugins for this type.
*
* @return array
* An array of plugin definitions (empty array if no definitions were
* found). Keys are plugin IDs.
*/
public function getVisibleDefinitions() {
$definitions = $this->getDefinitions();
unset($definitions['broken']);
return $definitions;
}
}

View file

@ -0,0 +1,167 @@
<?php
namespace Drupal\pathauto;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Path\AliasManagerInterface;
use Drupal\Core\Routing\RouteProviderInterface;
/**
* Provides a utility for creating a unique path alias.
*/
class AliasUniquifier implements AliasUniquifierInterface {
/**
* Config factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* The alias storage helper.
*
* @var \Drupal\pathauto\AliasStorageHelperInterface
*/
protected $aliasStorageHelper;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The route provider service.
*
* @var \Drupal\Core\Routing\RouteProviderInterface.
*/
protected $routeProvider;
/**
* The alias manager.
*
* @var \Drupal\Core\Path\AliasManagerInterface
*/
protected $aliasManager;
/**
* Creates a new AliasUniquifier.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
* @param \Drupal\pathauto\AliasStorageHelperInterface $alias_storage_helper
* The alias storage helper.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\Routing\RouteProviderInterface $route_provider
* The route provider service.
*/
public function __construct(ConfigFactoryInterface $config_factory, AliasStorageHelperInterface $alias_storage_helper, ModuleHandlerInterface $module_handler, RouteProviderInterface $route_provider, AliasManagerInterface $alias_manager) {
$this->configFactory = $config_factory;
$this->aliasStorageHelper = $alias_storage_helper;
$this->moduleHandler = $module_handler;
$this->routeProvider = $route_provider;
$this->aliasManager = $alias_manager;
}
/**
* {@inheritdoc}
*/
public function uniquify(&$alias, $source, $langcode) {
$config = $this->configFactory->get('pathauto.settings');
if (!$this->isReserved($alias, $source, $langcode)) {
return;
}
// If the alias already exists, generate a new, hopefully unique, variant.
$maxlength = min($config->get('max_length'), $this->aliasStorageHelper->getAliasSchemaMaxlength());
$separator = $config->get('separator');
$original_alias = $alias;
$i = 0;
do {
// Append an incrementing numeric suffix until we find a unique alias.
$unique_suffix = $separator . $i;
$alias = Unicode::truncate($original_alias, $maxlength - Unicode::strlen($unique_suffix), TRUE) . $unique_suffix;
$i++;
} while ($this->isReserved($alias, $source, $langcode));
}
/**
* {@inheritdoc}
*/
public function isReserved($alias, $source, $langcode = LanguageInterface::LANGCODE_NOT_SPECIFIED) {
// 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;
}
// Finally check if any other modules have reserved the alias.
$args = array(
$alias,
$source,
$langcode,
);
$implementations = $this->moduleHandler->getImplementations('pathauto_is_alias_reserved');
foreach ($implementations as $module) {
$result = $this->moduleHandler->invoke($module, 'pathauto_is_alias_reserved', $args);
if (!empty($result)) {
// As soon as the first module says that an alias is in fact reserved,
// then there is no point in checking the rest of the modules.
return TRUE;
}
}
return FALSE;
}
/**
* Verify if the given path is a valid route.
*
* @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)) {
// Do not allow existing files or directories to get assigned an automatic
// alias. Note that we do not need to use is_link() to check for symbolic
// links since this returns TRUE for either is_file() or is_dir() already.
return TRUE;
}
$routes = $this->routeProvider->getRoutesByPattern($path);
// Only return true for an exact match, ignore placeholders.
foreach ($routes as $route) {
if ($route->getPath() == $path) {
return TRUE;
}
}
return FALSE;
}
}

View file

@ -0,0 +1,42 @@
<?php
namespace Drupal\pathauto;
use Drupal\Core\Language\LanguageInterface;
/**
* Provides an interface for alias uniquifiers.
*/
interface AliasUniquifierInterface {
/**
* Check to ensure a path alias is unique and add suffix variants if necessary.
*
* Given an alias 'content/test' if a path alias with the exact alias already
* exists, the function will change the alias to 'content/test-0' and will
* increase the number suffix until it finds a unique alias.
*
* @param string $alias
* A string with the alias. Can be altered by reference.
* @param string $source
* A string with the path source.
* @param string $langcode
* A string with a language code.
*/
public function uniquify(&$alias, $source, $langcode);
/**
* Checks if an alias is reserved.
*
* @param string $alias
* The alias.
* @param string $source
* The source.
* @param string $langcode
* (optional) The language code.
*
* @return bool
* Returns TRUE if the alias is reserved.
*/
public function isReserved($alias, $source, $langcode = LanguageInterface::LANGCODE_NOT_SPECIFIED);
}

View file

@ -0,0 +1,37 @@
<?php
namespace Drupal\pathauto\Annotation;
use Drupal\Component\Annotation\Plugin;
/**
* Defines an AliasType annotation.
*
* @Annotation
*/
class AliasType extends Plugin {
/**
* The plugin ID.
*
* @var string
*/
public $id;
/**
* The human-readable name of the action plugin.
*
* @ingroup plugin_translatable
*
* @var \Drupal\Core\Annotation\Translation
*/
public $label;
/**
* The token types.
*
* @var string[]
*/
public $types = array();
}

View file

@ -0,0 +1,370 @@
<?php
namespace Drupal\pathauto\Entity;
use Drupal\Component\Plugin\Exception\ContextException;
use Drupal\Core\Condition\ConditionPluginCollection;
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Plugin\ContextAwarePluginInterface;
use Drupal\Core\Plugin\DefaultSingleLazyPluginCollection;
use Drupal\pathauto\PathautoPatternInterface;
/**
* Defines the Pathauto pattern entity.
*
* @ConfigEntityType(
* id = "pathauto_pattern",
* label = @Translation("Pathauto pattern"),
* handlers = {
* "list_builder" = "Drupal\pathauto\PathautoPatternListBuilder",
* "form" = {
* "default" = "Drupal\pathauto\Form\PatternEditForm",
* "delete" = "Drupal\Core\Entity\EntityDeleteForm",
* "enable" = "Drupal\pathauto\Form\PatternEnableForm",
* "disable" = "Drupal\pathauto\Form\PatternDisableForm"
* },
* "route_provider" = {
* "html" = "Drupal\Core\Entity\Routing\DefaultHtmlRouteProvider",
* },
* },
* config_prefix = "pattern",
* admin_permission = "administer pathauto",
* entity_keys = {
* "id" = "id",
* "label" = "label",
* "uuid" = "uuid",
* "weight" = "weight",
* "status" = "status"
* },
* config_export = {
* "id",
* "label",
* "type",
* "pattern",
* "selection_criteria",
* "selection_logic",
* "weight",
* "relationships"
* },
* lookup_keys = {
* "type",
* "status",
* },
* links = {
* "collection" = "/admin/config/search/path/patterns",
* "edit-form" = "/admin/config/search/path/patterns/{pathauto_pattern}",
* "delete-form" = "/admin/config/search/path/patterns/{pathauto_pattern}/delete",
* "enable" = "/admin/config/search/path/patterns/{pathauto_pattern}/enable",
* "disable" = "/admin/config/search/path/patterns/{pathauto_pattern}/disable"
* }
* )
*/
class PathautoPattern extends ConfigEntityBase implements PathautoPatternInterface {
/**
* The Pathauto pattern ID.
*
* @var string
*/
protected $id;
/**
* The Pathauto pattern label.
*
* @var string
*/
protected $label;
/**
* The pattern type.
*
* A string denoting the type of pathauto pattern this is. For a node path
* this would be 'node', for users it would be 'user', and so on. This allows
* for arbitrary non-entity patterns to be possible if applicable.
*
* @var string
*/
protected $type;
/**
* @var \Drupal\Core\Plugin\DefaultSingleLazyPluginCollection
*/
protected $aliasTypeCollection;
/**
* A tokenized string for alias generation.
*
* @var string
*/
protected $pattern;
/**
* The plugin configuration for the selection criteria condition plugins.
*
* @var array
*/
protected $selection_criteria = [];
/**
* The selection logic for this pattern entity (either 'and' or 'or').
*
* @var string
*/
protected $selection_logic = 'and';
/**
* @var int
*/
protected $weight = 0;
/**
* @var array[]
* Keys are context tokens, and values are arrays with the following keys:
* - label (string|null, optional): The human-readable label of this
* relationship.
*/
protected $relationships = [];
/**
* The plugin collection that holds the selection criteria condition plugins.
*
* @var \Drupal\Component\Plugin\LazyPluginCollection
*/
protected $selectionConditionCollection;
/**
* {@inheritdoc}
*
* Not using core's default logic around ConditionPluginCollection since it
* incorrectly assumes no condition will ever be applied twice.
*/
public function preSave(EntityStorageInterface $storage) {
parent::preSave($storage);
$criteria = [];
foreach ($this->getSelectionConditions() as $id => $condition) {
$criteria[$id] = $condition->getConfiguration();
}
$this->selection_criteria = $criteria;
// Invalidate the static caches.
\Drupal::service('pathauto.generator')->resetCaches();
}
/**
* {@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 getContexts() {
$contexts = $this->getAliasType()->getContexts();
foreach ($this->getRelationships() as $token => $definition) {
/** @var \Drupal\ctools\TypedDataResolver $resolver */
$resolver = \Drupal::service('ctools.typed_data.resolver');
$context = $resolver->convertTokenToContext($token, $contexts);
$context_definition = $context->getContextDefinition();
if (!empty($definition['label'])) {
$context_definition->setLabel($definition['label']);
}
$contexts[$token] = $context;
}
return $contexts;
}
/**
* {@inheritdoc}
*/
public function hasRelationship($token) {
return isset($this->relationships[$token]);
}
/**
* {@inheritdoc}
*/
public function addRelationship($token, $label = NULL) {
if (!$this->hasRelationship($token)) {
$this->relationships[$token] = [
'label' => $label,
];
}
return $this;
}
/**
* {@inheritdoc}
*/
public function replaceRelationship($token, $label) {
if ($this->hasRelationship($token)) {
$this->relationships[$token] = [
'label' => $label,
];
}
return $this;
}
/**
* {@inheritdoc}
*/
public function removeRelationship($token) {
unset($this->relationships[$token]);
return $this;
}
/**
* {@inheritdoc}
*/
public function getRelationships() {
return $this->relationships;
}
/**
* {@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 */
$contexts = $this->getContexts();
/** @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) {
try {
$context_handler->applyContextMapping($condition, $contexts);
}
catch (ContextException $e) {
watchdog_exception('pathauto', $e);
return FALSE;
}
}
$result = $condition->execute();
if ($this->getSelectionLogic() == 'and' && !$result) {
return FALSE;
}
elseif ($this->getSelectionLogic() == 'or' && $result) {
return TRUE;
}
}
return TRUE;
}
return FALSE;
}
}

View file

@ -0,0 +1,54 @@
<?php
namespace Drupal\pathauto\EventSubscriber;
use Drupal\Core\Config\ConfigCrudEvent;
use Drupal\Core\Config\ConfigEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\pathauto\AliasTypeManager;
/**
* A subscriber to clear fielddefinition cache when saving pathauto settings.
*/
class PathautoSettingsCacheTag implements EventSubscriberInterface {
protected $entityFieldManager;
protected $aliasTypeManager;
/**
* Constructs a PathautoSettingsCacheTag object.
*/
public function __construct(EntityFieldManagerInterface $entity_field_manager, AliasTypeManager $alias_type_manager) {
$this->entityFieldManager = $entity_field_manager;
$this->aliasTypeManager = $alias_type_manager;
}
/**
* Invalidate the 'rendered' cache tag whenever the settings are modified.
*
* @param \Drupal\Core\Config\ConfigCrudEvent $event
* The Event to process.
*/
public function onSave(ConfigCrudEvent $event) {
if ($event->getConfig()->getName() === 'pathauto.settings') {
$config = $event->getConfig();
$original_entity_types = $config->getOriginal('enabled_entity_types');
// Clear cached field definitions if the values are changed.
if ($original_entity_types != $config->get('enabled_entity_types')) {
$this->entityFieldManager->clearCachedFieldDefinitions();
$this->aliasTypeManager->clearCachedDefinitions();
}
}
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
$events[ConfigEvents::SAVE][] = ['onSave'];
return $events;
}
}

View file

@ -0,0 +1,185 @@
<?php
namespace Drupal\pathauto\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\pathauto\AliasTypeManager;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* 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}
*/
public function getFormId() {
return 'pathauto_admin_delete';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form['delete'] = [
'#type' => 'fieldset',
'#title' => $this->t('Choose aliases to delete'),
'#tree' => TRUE,
];
// First we do the "all" case.
$storage_helper = \Drupal::service('pathauto.alias_storage_helper');
$total_count = $storage_helper->countAll();
$form['delete']['all_aliases'] = [
'#type' => 'checkbox',
'#title' => $this->t('All aliases'),
'#default_value' => FALSE,
'#description' => $this->t('Delete all aliases. Number of aliases which will be deleted: %count.', ['%count' => $total_count]),
];
// Next, iterate over all visible alias types.
$definitions = $this->aliasTypeManager->getVisibleDefinitions();
foreach ($definitions as $id => $definition) {
/** @var \Drupal\pathauto\AliasTypeInterface $alias_type */
$alias_type = $this->aliasTypeManager->createInstance($id);
$count = $storage_helper->countBySourcePrefix($alias_type->getSourcePrefix());
$form['delete']['plugins'][$id] = [
'#type' => 'checkbox',
'#title' => (string) $definition['label'],
'#default_value' => FALSE,
'#description' => $this->t('Delete aliases for all @label. Number of aliases which will be deleted: %count.', ['@label' => (string) $definition['label'], '%count' => $count]),
];
}
$form['options'] = [
'#type' => 'fieldset',
'#title' => $this->t('Delete options'),
'#tree' => TRUE,
];
// Provide checkbox for not deleting custom aliases.
$form['options']['keep_custom_aliases'] = [
'#type' => 'checkbox',
'#title' => $this->t('Only delete automatically generated aliases'),
'#default_value' => TRUE,
'#description' => $this->t('When checked, aliases which have been manually set are not affected by this mass-deletion.'),
];
// Warn them and give a button that shows we mean business.
$form['warning'] = ['#value' => '<p>' . $this->t('<strong>Note:</strong> there is no confirmation. Be sure of your action before clicking the "Delete aliases now!" button.<br />You may want to make a backup of the database and/or the url_alias table prior to using this feature.') . '</p>'];
$form['buttons']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Delete aliases now!'),
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$delete_all = $form_state->getValue(['delete', 'all_aliases']);
// Keeping custom aliases forces us to go the slow way to correctly check
// the automatic/manual flag.
if ($form_state->getValue(['options', 'keep_custom_aliases'])) {
$batch = [
'title' => $this->t('Bulk deleting URL aliases'),
'operations' => [['Drupal\pathauto\Form\PathautoAdminDelete::batchStart', [$delete_all]]],
'finished' => 'Drupal\pathauto\Form\PathautoAdminDelete::batchFinished',
];
if ($delete_all) {
foreach (array_keys($form_state->getValue(['delete', 'plugins'])) as $id) {
$batch['operations'][] = ['Drupal\pathauto\Form\PathautoAdminDelete::batchProcess', [$id]];
}
}
else {
foreach (array_keys(array_filter($form_state->getValue(['delete', 'plugins']))) as $id) {
$batch['operations'][] = ['Drupal\pathauto\Form\PathautoAdminDelete::batchProcess', [$id]];
}
}
batch_set($batch);
}
else if ($delete_all) {
\Drupal::service('pathauto.alias_storage_helper')->deleteAll();
drupal_set_message($this->t('All of your path aliases have been deleted.'));
}
else {
$storage_helper = \Drupal::service('pathauto.alias_storage_helper');
foreach (array_keys(array_filter($form_state->getValue(['delete', 'plugins']))) as $id) {
$alias_type = $this->aliasTypeManager->createInstance($id);
$storage_helper->deleteBySourcePrefix((string) $alias_type->getSourcePrefix());
drupal_set_message($this->t('All of your %label path aliases have been deleted.', ['%label' => $alias_type->getLabel()]));
}
}
}
/**
* Batch callback; record if aliases of all types must be deleted.
*/
public static function batchStart($delete_all, &$context) {
$context['results']['delete_all'] = $delete_all;
$context['results']['deletions'] = [];
}
/**
* Common batch processing callback for all operations.
*/
public static function batchProcess($id, &$context) {
/** @var \Drupal\pathauto\AliasTypeBatchUpdateInterface $alias_type */
$alias_type = \Drupal::service('plugin.manager.alias_type')->createInstance($id);
$alias_type->batchDelete($context);
}
/**
* Batch finished callback.
*/
public static function batchFinished($success, $results, $operations) {
if ($success) {
if ($results['delete_all']) {
drupal_set_message(t('All of your automatically generated path aliases have been deleted.'));
}
else if (isset($results['deletions'])) {
foreach (array_values($results['deletions']) as $label) {
drupal_set_message(t('All of your automatically generated %label path aliases have been deleted.', ['%label' => $label]));
}
}
}
else {
$error_operation = reset($operations);
drupal_set_message(t('An error occurred while processing @operation with arguments : @args', array('@operation' => $error_operation[0], '@args' => print_r($error_operation[0], TRUE))));
}
}
}

View file

@ -0,0 +1,164 @@
<?php
namespace Drupal\pathauto\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Drupal\pathauto\AliasTypeBatchUpdateInterface;
use Drupal\pathauto\AliasTypeManager;
use Drupal\pathauto\PathautoGeneratorInterface;
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}
*/
public function getFormId() {
return 'pathauto_bulk_update_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form = array();
$form['#update_callbacks'] = array();
$form['update'] = array(
'#type' => 'checkboxes',
'#title' => $this->t('Select the types of paths for which to generate URL aliases'),
'#options' => array(),
'#default_value' => array(),
);
$definitions = $this->aliasTypeManager->getVisibleDefinitions();
foreach ($definitions as $id => $definition) {
$alias_type = $this->aliasTypeManager->createInstance($id);
if ($alias_type instanceof AliasTypeBatchUpdateInterface) {
$form['update']['#options'][$id] = $alias_type->getLabel();
}
}
$form['action'] = array(
'#type' => 'radios',
'#title' => $this->t('Select which URL aliases to generate'),
'#options' => ['create' => $this->t('Generate a URL alias for un-aliased paths only')],
'#default_value' => 'create',
);
$config = $this->config('pathauto.settings');
if ($config->get('update_action') == PathautoGeneratorInterface::UPDATE_ACTION_NO_NEW) {
// Existing aliases should not be updated.
$form['warning'] = array(
'#markup' => $this->t('<a href=":url">Pathauto settings</a> are set to ignore paths which already have a URL alias. You can only create URL aliases for paths having none.', [':url' => Url::fromRoute('pathauto.settings.form')->toString()]),
);
}
else {
$form['action']['#options']['update'] = $this->t('Update the URL alias for paths having an old URL alias');
$form['action']['#options']['all'] = $this->t('Regenerate URL aliases for all paths');
}
$form['actions']['#type'] = 'actions';
$form['actions']['submit'] = array(
'#type' => 'submit',
'#value' => $this->t('Update'),
);
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$batch = array(
'title' => $this->t('Bulk updating URL aliases'),
'operations' => array(
array('Drupal\pathauto\Form\PathautoBulkUpdateForm::batchStart', array()),
),
'finished' => 'Drupal\pathauto\Form\PathautoBulkUpdateForm::batchFinished',
);
$action = $form_state->getValue('action');
foreach ($form_state->getValue('update') as $id) {
if (!empty($id)) {
$batch['operations'][] = array('Drupal\pathauto\Form\PathautoBulkUpdateForm::batchProcess', [$id, $action]);
}
}
batch_set($batch);
}
/**
* Batch callback; initialize the number of updated aliases.
*/
public static function batchStart(&$context) {
$context['results']['updates'] = 0;
}
/**
* Common batch processing callback for all operations.
*
* Required to load our include the proper batch file.
*/
public static function batchProcess($id, $action, &$context) {
/** @var \Drupal\pathauto\AliasTypeBatchUpdateInterface $alias_type */
$alias_type = \Drupal::service('plugin.manager.alias_type')->createInstance($id);
$alias_type->batchUpdate($action, $context);
}
/**
* Batch finished callback.
*/
public static function batchFinished($success, $results, $operations) {
if ($success) {
if ($results['updates']) {
drupal_set_message(\Drupal::translation()->formatPlural($results['updates'], 'Generated 1 URL alias.', 'Generated @count URL aliases.'));
}
else {
drupal_set_message(t('No new URL aliases to generate.'));
}
}
else {
$error_operation = reset($operations);
drupal_set_message(t('An error occurred while processing @operation with arguments : @args', array('@operation' => $error_operation[0], '@args' => print_r($error_operation[0], TRUE))));
}
}
}

View file

@ -0,0 +1,266 @@
<?php
namespace Drupal\pathauto\Form;
use Drupal\Component\Utility\Html;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Url;
use Drupal\pathauto\AliasTypeManager;
use Drupal\pathauto\PathautoGeneratorInterface;
use Drupal\Core\Form\FormStateInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Configure file system settings for this site.
*/
class PathautoSettingsForm extends ConfigFormBase {
/**
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
*/
protected $entityFieldManager;
/**
* @var \Drupal\pathauto\AliasTypeManager
*/
protected $aliasTypeManager;
/**
* {@inheritDoc}
*/
public function __construct(ConfigFactoryInterface $config_factory, EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager, AliasTypeManager $alias_type_manager) {
parent::__construct($config_factory);
$this->entityTypeManager = $entity_type_manager;
$this->entityFieldManager = $entity_field_manager;
$this->aliasTypeManager = $alias_type_manager;
}
/**
* {@inheritDoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('config.factory'),
$container->get('entity_type.manager'),
$container->get('entity_field.manager'),
$container->get('plugin.manager.alias_type')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'pathauto_settings_form';
}
/**
* {@inheritdoc}
*/
protected function getEditableConfigNames() {
return ['pathauto.settings'];
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$config = $this->config('pathauto.settings');
$form['enabled_entity_types'] = [
'#type' => 'details',
'#open' => TRUE,
'#title' => $this->t('Enabled entity types'),
'#description' => $this->t('Enable to add a path field and allow to define alias patterns for the given type. Disabled types already define a path field themselves or currently have a pattern.'),
'#tree' => TRUE,
];
// Get all applicable entity types.
foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) {
// Disable a checkbox if it already exists and if the entity type has
// patterns currently defined or if it isn't defined by us.
$patterns_count = \Drupal::entityQuery('pathauto_pattern')
->condition('type', 'canonical_entities:' . $entity_type_id)
->count()
->execute();
if (is_subclass_of($entity_type->getClass(), FieldableEntityInterface::class) && $entity_type->hasLinkTemplate('canonical')) {
$field_definitions = $this->entityFieldManager->getBaseFieldDefinitions($entity_type_id);
$form['enabled_entity_types'][$entity_type_id] = [
'#type' => 'checkbox',
'#title' => $entity_type->getLabel(),
'#default_value' => isset($field_definitions['path']) || in_array($entity_type_id, $config->get('enabled_entity_types')),
'#disabled' => isset($field_definitions['path']) && ($field_definitions['path']->getProvider() != 'pathauto' || $patterns_count),
];
}
}
$form['verbose'] = array(
'#type' => 'checkbox',
'#title' => $this->t('Verbose'),
'#default_value' => $config->get('verbose'),
'#description' => $this->t('Display alias changes (except during bulk updates).'),
);
$form['separator'] = array(
'#type' => 'textfield',
'#title' => $this->t('Separator'),
'#size' => 1,
'#maxlength' => 1,
'#default_value' => $config->get('separator'),
'#description' => $this->t('Character used to separate words in titles. This will replace any spaces and punctuation characters. Using a space or + character can cause unexpected results.'),
);
$form['case'] = array(
'#type' => 'checkbox',
'#title' => $this->t('Character case'),
'#default_value' => $config->get('case'),
'#description' => $this->t('Convert token values to lowercase.'),
);
$max_length = \Drupal::service('pathauto.alias_storage_helper')->getAliasSchemaMaxlength();
$help_link = '';
if (\Drupal::moduleHandler()->moduleExists('help')) {
$help_link = ' ' . $this->t('See <a href=":pathauto-help">Pathauto help</a> for details.', [':pathauto-help' => Url::fromRoute('help.page', ['name' => 'pathauto'])->toString()]);
}
$form['max_length'] = array(
'#type' => 'number',
'#title' => $this->t('Maximum alias length'),
'#size' => 3,
'#maxlength' => 3,
'#default_value' => $config->get('max_length'),
'#min' => 1,
'#max' => $max_length,
'#description' => $this->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(
'#type' => 'number',
'#title' => $this->t('Maximum component length'),
'#size' => 3,
'#maxlength' => 3,
'#default_value' => $config->get('max_component_length'),
'#min' => 1,
'#max' => $max_length,
'#description' => $this->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 = $this->t('What should Pathauto do when updating an existing content item which already has an alias?');
if (\Drupal::moduleHandler()->moduleExists('redirect')) {
$description .= ' ' . $this->t('The <a href=":url">Redirect module settings</a> affect whether a redirect is created when an alias is deleted.', array(':url' => Url::fromRoute('redirect.settings')->toString()));
}
else {
$description .= ' ' . $this->t('Considering installing the <a href=":url">Redirect module</a> to get redirects when your aliases change.', array(':url' => 'http://drupal.org/project/redirect'));
}
$form['update_action'] = array(
'#type' => 'radios',
'#title' => $this->t('Update action'),
'#default_value' => $config->get('update_action'),
'#options' => array(
PathautoGeneratorInterface::UPDATE_ACTION_NO_NEW => $this->t('Do nothing. Leave the old alias intact.'),
PathautoGeneratorInterface::UPDATE_ACTION_LEAVE => $this->t('Create a new alias. Leave the existing alias functioning.'),
PathautoGeneratorInterface::UPDATE_ACTION_DELETE => $this->t('Create a new alias. Delete the old alias.'),
),
'#description' => $description,
);
$form['transliterate'] = array(
'#type' => 'checkbox',
'#title' => $this->t('Transliterate prior to creating alias'),
'#default_value' => $config->get('transliterate'),
'#description' => $this->t('When a pattern includes certain characters (such as those with accents) should Pathauto attempt to transliterate them into the US-ASCII alphabet?'),
);
$form['reduce_ascii'] = array(
'#type' => 'checkbox',
'#title' => $this->t('Reduce strings to letters and numbers'),
'#default_value' => $config->get('reduce_ascii'),
'#description' => $this->t('Filters the new alias to only letters and numbers found in the ASCII-96 set.'),
);
$form['ignore_words'] = array(
'#type' => 'textarea',
'#title' => $this->t('Strings to Remove'),
'#default_value' => $config->get('ignore_words'),
'#description' => $this->t('Words to strip out of the URL alias, separated by commas. Do not use this to remove punctuation.'),
'#wysiwyg' => FALSE,
);
$form['punctuation'] = array(
'#type' => 'fieldset',
'#title' => $this->t('Punctuation'),
'#collapsible' => TRUE,
'#collapsed' => TRUE,
'#tree' => TRUE,
);
$punctuation = \Drupal::service('pathauto.alias_cleaner')->getPunctuationCharacters();
foreach ($punctuation as $name => $details) {
// Use the value from config if it exists.
if ($config->get('punctuation.' . $name) !== NULL) {
$details['default'] = $config->get('punctuation.' . $name);
}
else {
// Otherwise use the correct default.
$details['default'] = $details['value'] == $config->get('separator') ? PathautoGeneratorInterface::PUNCTUATION_REPLACE : PathautoGeneratorInterface::PUNCTUATION_REMOVE;
}
$form['punctuation'][$name] = array(
'#type' => 'select',
'#title' => $details['name'] . ' (<code>' . Html::escape($details['value']) . '</code>)',
'#default_value' => $details['default'],
'#options' => array(
PathautoGeneratorInterface::PUNCTUATION_REMOVE => $this->t('Remove'),
PathautoGeneratorInterface::PUNCTUATION_REPLACE => $this->t('Replace by separator'),
PathautoGeneratorInterface::PUNCTUATION_DO_NOTHING => $this->t('No action (do not replace)'),
),
);
}
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$config = $this->config('pathauto.settings');
$form_state->cleanValues();
$original_entity_types = $config->get('enabled_entity_types');
foreach ($form_state->getValues() as $key => $value) {
if ($key == 'enabled_entity_types') {
$enabled_entity_types = [];
foreach ($value as $entity_type_id => $enabled) {
$field_definitions = $this->entityFieldManager->getBaseFieldDefinitions($entity_type_id);
// Verify that the entity type is enabled and that it is not defined
// or defined by us before adding it to the configuration, so that
// we do not store an entity type that cannot be enabled or disabled.
if ($enabled && (!isset($field_definitions['path']) || ($field_definitions['path']->getProvider() === 'pathauto'))) {
$enabled_entity_types[] = $entity_type_id;
}
}
$value = $enabled_entity_types;
}
$config->set($key, $value);
}
$config->save();
parent::submitForm($form, $form_state);
}
}

View file

@ -0,0 +1,52 @@
<?php
namespace Drupal\pathauto\Form;
use Drupal\Core\Entity\EntityConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
/**
* Provides the pathauto pattern disable disable form.
*/
class PatternDisableForm extends EntityConfirmFormBase {
/**
* {@inheritdoc}
*/
public function getQuestion() {
return $this->t('Are you sure you want to disable the pattern %label?', array('%label' => $this->entity->label()));
}
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
return new Url('entity.pathauto_pattern.collection');
}
/**
* {@inheritdoc}
*/
public function getConfirmText() {
return $this->t('Disable');
}
/**
* {@inheritdoc}
*/
public function getDescription() {
return $this->t('Disabled patterns are ignored when generating aliases.');
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->entity->disable()->save();
drupal_set_message($this->t('Disabled pattern %label.', array('%label' => $this->entity->label())));
$form_state->setRedirectUrl($this->getCancelUrl());
}
}

View file

@ -0,0 +1,285 @@
<?php
namespace Drupal\pathauto\Form;
use Drupal\Core\Entity\EntityForm;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\pathauto\AliasTypeManager;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Edit form for pathauto patterns.
*/
class PatternEditForm extends EntityForm {
/**
* @var \Drupal\pathauto\AliasTypeManager
*/
protected $manager;
/**
* @var \Drupal\pathauto\PathautoPatternInterface
*/
protected $entity;
/**
* The entity type bundle info service.
*
* @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
*/
protected $entityTypeBundleInfo;
/**
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected $languageManager;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->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->getVisibleDefinitions() 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' => '<div id="pathauto-pattern">',
'#suffix' => '</div>',
];
// 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', 'pathauto_pattern_validate'),
'#after_build' => array('token_element_validate'),
'#token_types' => $alias_type->getTokenTypes(),
'#min_tokens' => 1,
'#required' => TRUE,
);
// 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 (in_array($condition->getPluginId(), ['entity_bundle:' . $entity_type->id(), 'node_type'])) {
$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' => $this->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' => $this->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,
'#description' => $this->t('A short name to help you identify this pattern in the patterns list.'),
);
$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',
),
);
$form['status'] = [
'#title' => $this->t('Enabled'),
'#type' => 'checkbox',
'#default_value' => $this->entity->status(),
];
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);
// Will only be used for new patterns.
$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 (in_array($condition->getPluginId(), ['entity_bundle:' . $entity_type, 'node_type', 'language'])) {
$entity->removeSelectionCondition($condition_id);
}
}
if ($bundles = array_filter((array) $form_state->getValue('bundles'))) {
$default_weight -= 5;
$plugin_id = $entity_type == 'node' ? 'node_type' : 'entity_bundle:' . $entity_type;
$entity->addSelectionCondition(
[
'id' => $plugin_id,
'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,
]
]
);
$entity->addRelationship($language_mapping, t('Language'));
}
}
if ($entity->isNew()) {
$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();
}
}

View file

@ -0,0 +1,52 @@
<?php
namespace Drupal\pathauto\Form;
use Drupal\Core\Entity\EntityConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
/**
* Provides the pathauto pattern disable disable form.
*/
class PatternEnableForm extends EntityConfirmFormBase {
/**
* {@inheritdoc}
*/
public function getQuestion() {
return $this->t('Are you sure you want to enable the pattern %label?', array('%label' => $this->entity->label()));
}
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
return new Url('entity.pathauto_pattern.collection');
}
/**
* {@inheritdoc}
*/
public function getConfirmText() {
return $this->t('Enable');
}
/**
* {@inheritdoc}
*/
public function getDescription() {
return '';
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->entity->enable()->save();
drupal_set_message($this->t('Enabled pattern %label.', array('%label' => $this->entity->label())));
$form_state->setRedirectUrl($this->getCancelUrl());
}
}

View file

@ -0,0 +1,20 @@
<?php
namespace Drupal\pathauto;
/**
* Provides an interface for Messengers.
*/
interface MessengerInterface {
/**
* Adds a message.
*
* @param string $message
* The message to add.
* @param string $op
* (optional) The operation being performed.
*/
public function addMessage($message, $op = NULL);
}

View file

@ -0,0 +1,365 @@
<?php
namespace Drupal\pathauto;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\RevisionableInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\Core\Utility\Token;
use Drupal\token\TokenEntityMapperInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
/**
* Provides methods for generating path aliases.
*/
class PathautoGenerator implements PathautoGeneratorInterface {
use StringTranslationTrait;
/**
* Config factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* Module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* Token service.
*
* @var \Drupal\Core\Utility\Token
*/
protected $token;
/**
* Calculated pattern for a specific entity.
*
* @var array
*/
protected $patterns = array();
/**
* Available patterns per entity type ID.
*
* @var array
*/
protected $patternsByEntityType = array();
/**
* The alias cleaner.
*
* @var \Drupal\pathauto\AliasCleanerInterface
*/
protected $aliasCleaner;
/**
* The alias storage helper.
*
* @var \Drupal\pathauto\AliasStorageHelperInterface
*/
protected $aliasStorageHelper;
/**
* The alias uniquifier.
*
* @var \Drupal\pathauto\AliasUniquifierInterface
*/
protected $aliasUniquifier;
/**
* The messenger service.
*
* @var \Drupal\pathauto\MessengerInterface
*/
protected $messenger;
/**
* @var \Drupal\token\TokenEntityMapperInterface
*/
protected $tokenEntityMapper;
/**
* @var Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Creates a new Pathauto manager.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\Utility\Token $token
* The token utility.
* @param \Drupal\pathauto\AliasCleanerInterface $alias_cleaner
* The alias cleaner.
* @param \Drupal\pathauto\AliasStorageHelperInterface $alias_storage_helper
* The alias storage helper.
* @param AliasUniquifierInterface $alias_uniquifier
* The alias uniquifier.
* @param MessengerInterface $messenger
* The messenger service.
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* The string translation service.
* @param Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager
*/
public function __construct(ConfigFactoryInterface $config_factory, ModuleHandlerInterface $module_handler, Token $token, AliasCleanerInterface $alias_cleaner, AliasStorageHelperInterface $alias_storage_helper, AliasUniquifierInterface $alias_uniquifier, MessengerInterface $messenger, TranslationInterface $string_translation, TokenEntityMapperInterface $token_entity_mappper, EntityTypeManagerInterface $entity_type_manager) {
$this->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;
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@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();
// Core does not handle aliases with language Not Applicable.
if ($langcode == LanguageInterface::LANGCODE_NOT_APPLICABLE) {
$langcode = LanguageInterface::LANGCODE_NOT_SPECIFIED;
}
// 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))))
->condition('status', 1)
->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;
}
// Only act if this is the default revision.
if ($entity instanceof RevisionableInterface && !$entity->isDefaultRevision()) {
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';
}
}
try {
$result = $this->createEntityAlias($entity, $op);
}
catch (\InvalidArgumentException $e) {
drupal_set_message($e->getMessage(), 'error');
return NULL;
}
// @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 $this->entityTypeManager->getStorage('taxonomy_term')->loadChildren($tid);
}
}

View file

@ -0,0 +1,89 @@
<?php
namespace Drupal\pathauto;
use Drupal\Core\Entity\EntityInterface;
/**
* Provides and interface for PathautoGenerator.
*/
interface PathautoGeneratorInterface {
/**
* "Do nothing. Leave the old alias intact."
*/
const UPDATE_ACTION_NO_NEW = 0;
/**
* "Create a new alias. Leave the existing alias functioning."
*/
const UPDATE_ACTION_LEAVE = 1;
/**
* "Create a new alias. Delete the old alias."
*/
const UPDATE_ACTION_DELETE = 2;
/**
* Remove the punctuation from the alias.
*/
const PUNCTUATION_REMOVE = 0;
/**
* Replace the punctuation with the separator in the alias.
*/
const PUNCTUATION_REPLACE = 1;
/**
* Leave the punctuation as it is in the alias.
*/
const PUNCTUATION_DO_NOTHING = 2;
/**
* Resets internal caches.
*/
public function resetCaches();
/**
* Load an alias pattern entity by entity, bundle, and language.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* An entity.
* @return \Drupal\pathauto\PathautoPatternInterface|null
*/
public function getPatternByEntity(EntityInterface $entity);
/**
* Apply patterns to create an alias.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity.
* @param string $op
* Operation being performed on the content being aliased
* ('insert', 'update', 'return', or 'bulkupdate').
*
* @return array|string
* The alias that was created.
*
* @see _pathauto_set_alias()
*/
public function createEntityAlias(EntityInterface $entity, $op);
/**
* 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 updateEntityAlias(EntityInterface $entity, $op, array $options = array());
}

View file

@ -0,0 +1,58 @@
<?php
namespace Drupal\pathauto;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\TypedData\DataDefinition;
use Drupal\path\Plugin\Field\FieldType\PathItem;
/**
* Extends the default PathItem implementation to generate aliases.
*/
class PathautoItem extends PathItem {
/**
* {@inheritdoc}
*/
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 postSave($update) {
// Only allow the parent implementation to act if pathauto will not create
// an alias.
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;
}
}

View file

@ -0,0 +1,181 @@
<?php
namespace Drupal\pathauto;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
/**
* Provides an interface for defining Pathauto pattern entities.
*/
interface PathautoPatternInterface extends ConfigEntityInterface {
/**
* Get the tokenized pattern used during alias generation.
*
* @return string
*/
public function getPattern();
/**
* Set the tokenized pattern to use during alias generation.
*
* @param string $pattern
*
* @return $this
*/
public function setPattern($pattern);
/**
* Gets the type of this pattern.
*
* @return string
*/
public function getType();
/**
* @return \Drupal\pathauto\AliasTypeInterface
*/
public function getAliasType();
/**
* Gets the weight of this pattern (compared to other patterns of this type).
*
* @return int
*/
public function getWeight();
/**
* Sets the weight of this pattern (compared to other patterns of this type).
*
* @param int $weight
* The weight of the variant.
*
* @return $this
*/
public function setWeight($weight);
/**
* Returns the contexts of this pattern.
*
* @return \Drupal\Core\Plugin\Context\ContextInterface[]
*/
public function getContexts();
/**
* Returns whether a relationship exists.
*
* @param string $token
* Relationship identifier.
*
* @return bool
* TRUE if the relationship exists, FALSE otherwise.
*/
public function hasRelationship($token);
/**
* Adds a relationship.
*
* The relationship will not be changed if it already exists.
*
* @param string $token
* Relationship identifier.
* @param string|null $label
* (optional) A label, will use the label of the referenced context if not
* provided.
*
* @return $this
*/
public function addRelationship($token, $label = NULL);
/**
* Replaces a relationship.
*
* Only already existing relationships are updated.
*
* @param string $token
* Relationship identifier.
* @param string|null $label
* (optional) A label, will use the label of the referenced context if not
* provided.
*
* @return $this
*/
public function replaceRelationship($token, $label);
/**
* Removes a relationship.
*
* @param string $token
* Relationship identifier.
*
* @return $this
*/
public function removeRelationship($token);
/**
* Returns a list of relationships.
*
* @return array[]
* Keys are context tokens, and values are arrays with the following keys:
* - label (string|null, optional): The human-readable label of this
* relationship.
*/
public function getRelationships();
/**
* Gets the selection condition collection.
*
* @return \Drupal\Core\Condition\ConditionInterface[]|\Drupal\Core\Condition\ConditionPluginCollection
*/
public function getSelectionConditions();
/**
* Adds selection criteria.
*
* @param array $configuration
* Configuration of the selection criteria.
*
* @return string
* The condition id of the new criteria.
*/
public function addSelectionCondition(array $configuration);
/**
* Gets selection criteria by condition id.
*
* @param string $condition_id
* The id of the condition.
*
* @return \Drupal\Core\Condition\ConditionInterface
*/
public function getSelectionCondition($condition_id);
/**
* Removes selection criteria by condition id.
*
* @param string $condition_id
* The id of the condition.
*
* @return $this
*/
public function removeSelectionCondition($condition_id);
/**
* Gets the selection logic used by the criteria (ie. "and" or "or").
*
* @return string
* Either "and" or "or"; represents how the selection criteria are combined.
*/
public function getSelectionLogic();
/**
* Determines if this pattern can apply a given object.
*
* @param $object
* The object used to determine if this plugin can apply.
*
* @return bool
*/
public function applies($object);
}

View file

@ -0,0 +1,51 @@
<?php
namespace Drupal\pathauto;
use Drupal\Core\Config\Entity\DraggableListBuilder;
use Drupal\Core\Entity\EntityInterface;
/**
* Provides a listing of Pathauto pattern entities.
*/
class PathautoPatternListBuilder extends DraggableListBuilder {
/**
* {@inheritdoc}
*/
protected $limit = FALSE;
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'pathauto_pattern_list';
}
/**
* {@inheritdoc}
*/
public function buildHeader() {
$header['label'] = $this->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);
}
}

View file

@ -0,0 +1,97 @@
<?php
namespace Drupal\pathauto;
use Drupal\Core\TypedData\TypedData;
/**
* A property that stores in keyvalue whether an entity should receive an alias.
*/
class PathautoState extends TypedData {
/**
* An automatic alias should not be created.
*/
const SKIP = 0;
/**
* An automatic alias should be created.
*/
const CREATE = 1;
/**
* Pathauto state.
*
* @var int
*/
protected $value;
/**
* @var \Drupal\Core\Field\FieldItemInterface
*/
protected $parent;
/**
* {@inheritdoc}
*/
public function getValue() {
if ($this->value === NULL) {
// 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, then set the flag to
// create the alias if there is a matching pattern.
if ($this->value === NULL) {
$entity = $this->parent->getEntity();
$pattern = \Drupal::service('pathauto.generator')->getPatternByEntity($entity);
$this->value = !empty($pattern) ? static::CREATE : 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();
}
}

View file

@ -0,0 +1,71 @@
<?php
namespace Drupal\pathauto;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\path\Plugin\Field\FieldWidget\PathWidget;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
/**
* Extends the core path widget.
*/
class PathautoWidget extends PathWidget {
/**
* {@inheritdoc}
*/
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
$element = parent::formElement($items, $delta, $element, $form, $form_state);
$entity = $items->getEntity();
// Taxonomy terms do not have an actual fieldset for path settings.
// Merge in the defaults.
// @todo Impossible to do this in widget, use another solution
/*
$form['path'] += array(
'#type' => 'fieldset',
'#title' => $this->t('URL path settings'),
'#collapsible' => TRUE,
'#collapsed' => empty($form['path']['alias']),
'#group' => 'additional_settings',
'#attributes' => array(
'class' => array('path-form'),
),
'#access' => \Drupal::currentUser()->hasPermission('create url aliases') || \Drupal::currentUser()->hasPermission('administer url aliases'),
'#weight' => 30,
'#tree' => TRUE,
'#element_validate' => array('path_form_element_validate'),
);*/
$pattern = \Drupal::service('pathauto.generator')->getPatternByEntity($entity);
if (empty($pattern)) {
return $element;
}
if (\Drupal::currentUser()->hasPermission('administer pathauto')) {
$description = $this->t('Uncheck this to create a custom alias below. <a href="@admin_link">Configure URL alias patterns.</a>', ['@admin_link' => Url::fromRoute('entity.pathauto_pattern.collection')->toString()]);
}
else {
$description = $this->t('Uncheck this to create a custom alias below.');
}
$element['pathauto'] = array(
'#type' => 'checkbox',
'#title' => $this->t('Generate automatic URL alias'),
'#default_value' => $entity->path->pathauto,
'#description' => $description,
'#weight' => -1,
);
// Add JavaScript that will disable the path textfield when the automatic
// alias checkbox is checked.
$element['alias']['#states']['disabled']['input[name="path[' . $delta . '][pathauto]"]'] = array('checked' => TRUE);
// Override path.module's vertical tabs summary.
$element['alias']['#attached']['library'] = ['pathauto/widget'];
return $element;
}
}

View file

@ -0,0 +1,36 @@
<?php
namespace Drupal\pathauto\Plugin\Action;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Action\ActionBase;
use Drupal\Core\Session\AccountInterface;
use Drupal\pathauto\PathautoState;
/**
* Pathauto entity update action.
*
* @Action(
* id = "pathauto_update_alias",
* label = @Translation("Update URL alias of an entity"),
* )
*/
class UpdateAction extends ActionBase {
/**
* {@inheritdoc}
*/
public function execute($entity = NULL) {
$entity->path->pathauto = PathautoState::CREATE;
\Drupal::service('pathauto.generator')->updateEntityAlias($entity, 'bulkupdate', array('message' => TRUE));
}
/**
* {@inheritdoc}
*/
public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
$result = AccessResult::allowedIfHasPermission($account, 'create url aliases');
return $return_as_object ? $result : $result->isAllowed();
}
}

View file

@ -0,0 +1,94 @@
<?php
namespace Drupal\pathauto\Plugin\Deriver;
use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Plugin\Context\ContextDefinition;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\token\TokenEntityMapperInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Deriver that exposes content entities as alias type plugins.
*/
class EntityAliasTypeDeriver extends DeriverBase implements ContainerDeriverInterface {
use StringTranslationTrait;
/**
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
*/
protected $entityFieldManager;
/**
* @var \Drupal\token\TokenEntityMapperInterface
*/
protected $tokenEntityMapper;
/**
* Constructs new EntityAliasTypeDeriver.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
* The entity field manager.
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* The string translation service.
* @apram \Drupal\Token\TokenEntityMapperInterface $token_entity_mapper
* The token entity mapper.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager, TranslationInterface $string_translation, TokenEntityMapperInterface $token_entity_mapper) {
$this->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.
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;
}
}

View file

@ -0,0 +1,24 @@
<?php
namespace Drupal\pathauto\Plugin\pathauto\AliasType;
/**
* Defines a fallback plugin for missing block plugins.
*
* @AliasType(
* id = "broken",
* label = @Translation("Broken"),
* admin_label = @Translation("Broken/Missing"),
* category = @Translation("AliasType"),
* )
*/
class Broken extends EntityAliasTypeBase {
/**
* {@inheritdoc}
*/
public function getLabel() {
return $this->t('Broken type');
}
}

View file

@ -0,0 +1,342 @@
<?php
namespace Drupal\pathauto\Plugin\pathauto\AliasType;
use Drupal\Core\Database\Connection;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\KeyValueStore\KeyValueFactoryInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Plugin\Context\Context;
use Drupal\Core\Plugin\ContextAwarePluginBase;
use Drupal\pathauto\AliasTypeBatchUpdateInterface;
use Drupal\pathauto\AliasTypeInterface;
use Drupal\pathauto\PathautoState;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* A pathauto alias type plugin for entities with canonical links.
*
* @AliasType(
* id = "canonical_entities",
* deriver = "\Drupal\pathauto\Plugin\Deriver\EntityAliasTypeDeriver"
* )
*/
class EntityAliasTypeBase extends ContextAwarePluginBase implements AliasTypeInterface, AliasTypeBatchUpdateInterface, ContainerFactoryPluginInterface {
/**
* The module handler service.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The language manager service.
*
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected $languageManager;
/**
* The entity manager service.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The key/value manager service.
*
* @var \Drupal\Core\KeyValueStore\KeyValueFactoryInterface
*/
protected $keyValue;
/**
* The database connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected $database;
/**
* The path prefix for this entity type.
*
* @var string
*/
protected $prefix;
/**
* Constructs a EntityAliasTypeBase instance.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler service.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager service.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity manager service.
* @param \Drupal\Core\KeyValueStore\KeyValueFactoryInterface $key_value
* The key/value manager service.
* @param \Drupal\Core\Database\Connection $database
* The database connection.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, ModuleHandlerInterface $module_handler, LanguageManagerInterface $language_manager, EntityTypeManagerInterface $entity_type_manager, KeyValueFactoryInterface $key_value, Connection $database) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->moduleHandler = $module_handler;
$this->languageManager = $language_manager;
$this->entityTypeManager = $entity_type_manager;
$this->keyValue = $key_value;
$this->database = $database;
}
/**
* {@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'),
$container->get('keyvalue'),
$container->get('database')
);
}
/**
* {@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($action, &$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 = $this->database->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');
switch ($action) {
case 'create':
$query->isNull('ua.source');
break;
case 'update':
$query->isNotNull('ua.source');
break;
case 'all':
// Nothing to do. We want all paths.
break;
default:
// Unknown action. Abort!
return;
}
$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();
$updates = $this->bulkUpdate($ids);
$context['sandbox']['count'] += count($ids);
$context['sandbox']['current'] = max($ids);
$context['results']['updates'] += $updates;
$context['message'] = $this->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'];
}
}
/**
* {@inheritdoc}
*/
public function batchDelete(&$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 = $this->database->select($entity_type->get('base_table'), 'base_table');
$query->innerJoin('url_alias', 'ua', "CONCAT('" . $this->getSourcePrefix() . "' , base_table.$id_key) = ua.source");
$query->addField('base_table', $id_key, 'id');
$query->addField('ua', 'pid');
$query->condition('ua.pid', $context['sandbox']['current'], '>');
$query->orderBy('ua.pid');
$query->addTag('pathauto_bulk_delete');
$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 delete, then stop immediately.
if (!$context['sandbox']['total']) {
$context['finished'] = 1;
return;
}
}
$query->range(0, 100);
$pids_by_id = $query->execute()->fetchAllKeyed();
$this->bulkDelete($pids_by_id);
$context['sandbox']['count'] += count($pids_by_id);
$context['sandbox']['current'] = max($pids_by_id);
$context['results']['deletions'][] = $this->getLabel();
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.
* @param array $options
* An optional array of additional options.
*
* @return int
* The number of updated URL aliases.
*/
protected function bulkUpdate(array $ids, array $options = array()) {
$options += array('message' => FALSE);
$updates = 0;
$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);
$result = \Drupal::service('pathauto.generator')->updateEntityAlias($translated_entity, 'bulkupdate', $options);
if ($result) {
$updates++;
}
}
}
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()));
}
return $updates;
}
/**
* Deletes the URL aliases for multiple entities.
*
* @param int[] $pids_by_id
* A list of path IDs keyed by entity ID.
*/
protected function bulkDelete(array $pids_by_id) {
$collection = 'pathauto_state.' . $this->getEntityTypeId();
$states = $this->keyValue->get($collection)->getMultiple(array_keys($pids_by_id));
$pids = [];
foreach ($pids_by_id as $id => $pid) {
// Only delete aliases that were created by this module.
if (isset($states[$id]) && $states[$id] == PathautoState::CREATE) {
$pids[] = $pid;
}
}
\Drupal::service('pathauto.alias_storage_helper')->deleteMultiple($pids);
}
/**
* {@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;
}
/**
* {@inheritdoc}
*/
public function setContextValue($name, $value) {
// Overridden to avoid merging existing cacheability metadata, which is not
// relevant for alias type plugins.
$this->context[$name] = new Context($this->getContextDefinition($name), $value);
return $this;
}
}

View file

@ -0,0 +1,106 @@
<?php
namespace Drupal\pathauto\Plugin\pathauto\AliasType;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Database\Connection;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\KeyValueStore\KeyValueFactoryInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* A pathauto alias type plugin for forum terms.
*
* @AliasType(
* id = "forum",
* label = @Translation("Forum"),
* types = {"term"},
* provider = "forum",
* context = {
* "taxonomy_term" = @ContextDefinition("entity:taxonomy_term")
* }
* )
*/
class ForumAliasType extends EntityAliasTypeBase implements ContainerFactoryPluginInterface {
/**
* The config factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* Constructs a ForumAliasType instance.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler service.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager service.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity manager service.
* @param \Drupal\Core\KeyValueStore\KeyValueFactoryInterface $key_value
* The key/value manager service.
* @param \Drupal\Core\Database\Connection $database
* The database service.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, ModuleHandlerInterface $module_handler, LanguageManagerInterface $language_manager, EntityTypeManagerInterface $entity_type_manager, KeyValueFactoryInterface $key_value, Connection $database, ConfigFactoryInterface $config_factory) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $module_handler, $language_manager, $entity_type_manager, $key_value, $database);
$this->configFactory = $config_factory;
}
/**
* {@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'),
$container->get('keyvalue'),
$container->get('database'),
$container->get('config.factory')
);
}
/**
* {@inheritdoc}
*/
protected function getEntityTypeId() {
return 'taxonomy_term';
}
/**
* {@inheritdoc}
*/
public function getSourcePrefix() {
return '/forum/';
}
/**
* {@inheritdoc}
*/
public function applies($object) {
if (parent::applies($object)) {
/** @var \Drupal\taxonomy\TermInterface $object */
$vid = $this->configFactory->get('forum.settings')->get('vocabulary');
return $object->getVocabularyId() == $vid;
}
return FALSE;
}
}

View file

@ -0,0 +1,156 @@
<?php
namespace Drupal\pathauto\Tests;
use Drupal\pathauto\PathautoGeneratorInterface;
use Drupal\pathauto\PathautoState;
use Drupal\simpletest\WebTestBase;
/**
* Bulk update functionality tests.
*
* @group pathauto
*/
class PathautoBulkUpdateTest extends WebTestBase {
use PathautoTestHelperTrait;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('node', 'pathauto', 'forum');
/**
* Admin user.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* The created nodes.
*
* @var \Drupal\node\NodeInterface
*/
protected $nodes;
/**
* The created patterns.
*
* @var \Drupal\pathauto\PathautoPatternInterface
*/
protected $patterns;
/**
* {inheritdoc}
*/
function setUp() {
parent::setUp();
// Allow other modules to add additional permissions for the admin user.
$permissions = array(
'administer pathauto',
'administer url aliases',
'create url aliases',
'administer forums',
);
$this->adminUser = $this->drupalCreateUser($permissions);
$this->drupalLogin($this->adminUser);
$this->patterns = array();
$this->patterns['node'] = $this->createPattern('node', '/content/[node:title]');
$this->patterns['user'] = $this->createPattern('user', '/users/[user:name]');
$this->patterns['forum'] = $this->createPattern('forum', '/forums/[term:name]');
}
function testBulkUpdate() {
// Create some nodes.
$this->nodes = array();
for ($i = 1; $i <= 5; $i++) {
$node = $this->drupalCreateNode();
$this->nodes[$node->id()] = $node;
}
// Clear out all aliases.
$this->deleteAllAliases();
// Bulk create aliases.
$edit = array(
'update[canonical_entities:node]' => TRUE,
'update[canonical_entities:user]' => TRUE,
'update[forum]' => TRUE,
);
$this->drupalPostForm('admin/config/search/path/update_bulk', $edit, t('Update'));
// This has generated 8 aliases: 5 nodes, 2 users and 1 forum.
$this->assertText('Generated 8 URL aliases.');
// Check that aliases have actually been created.
foreach ($this->nodes as $node) {
$this->assertEntityAliasExists($node);
}
$this->assertEntityAliasExists($this->adminUser);
// This is the default "General discussion" forum.
$this->assertAliasExists(['source' => '/taxonomy/term/1']);
// Add a new node.
$new_node = $this->drupalCreateNode(array('path' => array('alias' => '', 'pathauto' => PathautoState::SKIP)));
// Run the update again which should not run against any nodes.
$this->drupalPostForm('admin/config/search/path/update_bulk', $edit, t('Update'));
$this->assertText('No new URL aliases to generate.');
$this->assertNoEntityAliasExists($new_node);
// Make sure existing aliases can be overriden.
$this->drupalPostForm('admin/config/search/path/settings', ['update_action' => PathautoGeneratorInterface::UPDATE_ACTION_DELETE], t('Save configuration'));
// Patterns did not change, so no aliases should be regenerated.
$edit['action'] = 'all';
$this->drupalPostForm('admin/config/search/path/update_bulk', $edit, t('Update'));
$this->assertText('No new URL aliases to generate.');
// Update the node pattern, and leave other patterns alone. Existing nodes should get a new alias,
// except the node above whose alias is manually set. Other aliases must be left alone.
$this->patterns['node']->delete();
$this->patterns['node'] = $this->createPattern('node', '/archive/node-[node:nid]');
$this->drupalPostForm('admin/config/search/path/update_bulk', $edit, t('Update'));
$this->assertText('Generated 5 URL aliases.');
// Prevent existing aliases to be overriden. The bulk generate page should only offer
// to create an alias for paths which have none.
$this->drupalPostForm('admin/config/search/path/settings', ['update_action' => PathautoGeneratorInterface::UPDATE_ACTION_NO_NEW], t('Save configuration'));
$this->drupalGet('admin/config/search/path/update_bulk');
$this->assertFieldByName('action', 'create');
$this->assertText('Pathauto settings are set to ignore paths which already have a URL alias.');
$this->assertNoFieldByName('action', 'update');
$this->assertNoFieldByName('action', 'all');
}
/**
* Tests alias generation for nodes that existed before installing Pathauto.
*/
function testBulkUpdateExistingContent() {
// Create a node.
$node = $this->drupalCreateNode();
// Delete its alias and Pathauto metadata.
\Drupal::service('pathauto.alias_storage_helper')->deleteEntityPathAll($node);
$node->path->first()->get('pathauto')->purge();
\Drupal::entityTypeManager()->getStorage('node')->resetCache(array($node->id()));
// Execute bulk generation.
// Bulk create aliases.
$edit = array(
'update[canonical_entities:node]' => TRUE,
);
$this->drupalPostForm('admin/config/search/path/update_bulk', $edit, t('Update'));
// Verify that the alias was created for the node.
$this->assertText('Generated 1 URL alias.');
}
}

View file

@ -0,0 +1,87 @@
<?php
namespace Drupal\pathauto\Tests;
use Drupal\simpletest\WebTestBase;
use Drupal\comment\Tests\CommentTestTrait;
/**
* Tests pathauto settings form.
*
* @group pathauto
*/
class PathautoEnablingEntityTypesTest extends WebTestBase {
use PathautoTestHelperTrait;
use CommentTestTrait;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('node', 'pathauto', 'comment');
/**
* Admin user.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* {inheritdoc}
*/
function setUp() {
parent::setUp();
$this->drupalCreateContentType(array('type' => 'article'));
$this->addDefaultCommentField('node', 'article');
$permissions = array(
'administer pathauto',
'administer url aliases',
'create url aliases',
'administer nodes',
'post comments',
);
$this->adminUser = $this->drupalCreateUser($permissions);
$this->drupalLogin($this->adminUser);
}
/**
* A suite of tests to verify if the feature to enable and disable the
* ability to define alias patterns for a given entity type works. Test with
* the comment module, as it is not enabled by default.
*/
function testEnablingEntityTypes() {
// Verify that the comment entity type is not available when trying to add
// a new pattern, nor "broken".
$this->drupalGet('/admin/config/search/path/patterns/add');
$this->assertEqual(count($this->cssSelect('option[value = "canonical_entities:comment"]:contains(Comment)')), 0);
$this->assertEqual(count($this->cssSelect('option:contains(Broken)')), 0);
// Enable the entity type and create a pattern for it.
$this->drupalGet('/admin/config/search/path/settings');
$edit = [
'enabled_entity_types[comment]' => TRUE,
];
$this->drupalPostForm(NULL, $edit, 'Save configuration');
$this->createPattern('comment', '/comment/[comment:body]');
// Create a node, a comment type and a comment entity.
$node = $this->drupalCreateNode(['type' => 'article']);
$this->drupalGet('/node/' . $node->id());
$edit = [
'comment_body[0][value]' => 'test-body',
];
$this->drupalPostForm(NULL, $edit, 'Save');
// Verify that an alias has been generated and that the type can no longer
// be disabled.
$this->assertAliasExists(['alias' => '/comment/test-body']);
$this->drupalGet('/admin/config/search/path/settings');
$this->assertEqual(count($this->cssSelect('input[name = "enabled_entity_types[comment]"][disabled = "disabled"]')), 1);
}
}

View file

@ -0,0 +1,201 @@
<?php
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;
/**
* Test pathauto functionality with localization and translation.
*
* @group pathauto
*/
class PathautoLocaleTest extends WebTestBase {
use PathautoTestHelperTrait;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('node', 'pathauto', 'locale', 'content_translation');
/**
* {@inheritdoc}
*/
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
* updated and its newer French alias is left intact.
*/
function testLanguageAliases() {
$this->createPattern('node', '/content/[node:title]');
// Add predefined French language.
ConfigurableLanguage::createFromLangcode('fr')->save();
$node = array(
'title' => 'English node',
'langcode' => 'en',
'path' => array(array(
'alias' => '/english-node',
'pathauto' => FALSE,
)),
);
$node = $this->drupalCreateNode($node);
$english_alias = \Drupal::service('path.alias_storage')->load(array('alias' => '/english-node', 'langcode' => 'en'));
$this->assertTrue($english_alias, 'Alias created with proper language.');
// Also save a French alias that should not be left alone, even though
// it is the newer alias.
$this->saveEntityAlias($node, '/french-node', 'fr');
// Add an alias with the soon-to-be generated alias, causing the upcoming
// alias update to generate a unique alias with the '-0' suffix.
$this->saveAlias('/node/invalid', '/content/english-node', Language::LANGCODE_NOT_SPECIFIED);
// Update the node, triggering a change in the English alias.
$node->path->pathauto = 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'));
// 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 '-0'
// suffix.
$this->assertEntityAlias($node, '/content/english-node-0', 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'));
$this->enableArticleTranslation();
// 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');
}
/**
* Tests the alias created for a node with language Not Applicable.
*/
public function testLanguageNotApplicable() {
$this->drupalLogin($this->rootUser);
$this->enableArticleTranslation();
// Create a pattern for nodes.
$pattern = $this->createPattern('node', '/content/[node:title]', -1);
$pattern->save();
// Create a node with language Not Applicable.
$node = $this->createNode(['type' => 'article', 'title' => 'Test node', 'langcode' => LanguageInterface::LANGCODE_NOT_APPLICABLE]);
// Check that the generated alias has language Not Specified.
$alias = \Drupal::service('pathauto.alias_storage_helper')->loadBySource('/node/' . $node->id());
$this->assertEqual($alias['langcode'], LanguageInterface::LANGCODE_NOT_SPECIFIED, 'PathautoGenerator::createEntityAlias() adjusts the alias langcode from Not Applicable to Not Specified.');
// Check that the alias works.
$this->drupalGet('content/test-node');
$this->assertResponse(200);
}
/**
* Enables content translation on articles.
*/
protected function enableArticleTranslation() {
// 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'));
}
}

View file

@ -0,0 +1,200 @@
<?php
namespace Drupal\pathauto\Tests;
use Drupal\pathauto\PathautoState;
use Drupal\simpletest\WebTestBase;
/**
* Mass delete functionality tests.
*
* @group pathauto
*/
class PathautoMassDeleteTest extends WebTestBase {
use PathautoTestHelperTrait;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('node', 'taxonomy', 'pathauto');
/**
* Admin user.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* The test nodes.
*
* @var \Drupal\node\NodeInterface
*/
protected $nodes;
/**
* The test accounts.
*
* @var \Drupal\user\UserInterface
*/
protected $accounts;
/**
* The test terms.
*
* @var \Drupal\taxonomy\TermInterface
*/
protected $terms;
/**
* {inheritdoc}
*/
function setUp() {
parent::setUp();
$permissions = array(
'administer pathauto',
'administer url aliases',
'create url aliases',
);
$this->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,
'options[keep_custom_aliases]' => FALSE,
);
$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('admin/config/search/path/delete_bulk');
// Make sure that all of them are actually deleted.
$aliases = \Drupal::database()->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,
'options[keep_custom_aliases]' => FALSE,
);
$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);
}
}
}
// 3. Test deleting automatically generated aliases only.
$this->generateAliases();
$edit = array(
'delete[all_aliases]' => TRUE,
'options[keep_custom_aliases]' => TRUE,
);
$this->drupalPostForm('admin/config/search/path/delete_bulk', $edit, t('Delete aliases now!'));
$this->assertText(t('All of your automatically generated path aliases have been deleted.'));
$this->assertUrl('admin/config/search/path/delete_bulk');
// Make sure that only custom aliases and aliases with no information about
// their state still exist.
$aliases = \Drupal::database()->select('url_alias', 'ua')->fields('ua', ['source'])->execute()->fetchCol();
$this->assertEqual($aliases, ['/node/101', '/node/104', '/node/105'], 'Custom aliases still exist.');
}
/**
* Helper function to generate aliases.
*/
function generateAliases() {
// Delete all aliases to avoid duplicated aliases. They will be recreated below.
$this->deleteAllAliases();
// 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)) {
// Create a large number of nodes (100+) to make sure that the batch code works.
for ($i = 1; $i <= 105; $i++) {
// Set the alias of two nodes manually.
$settings = ($i > 103) ? ['path' => ['alias' => "/custom_alias_$i", 'pathauto' => PathautoState::SKIP]] : [];
$node = $this->drupalCreateNode($settings);
$this->nodes[$node->id()] = $node;
}
}
else {
foreach ($this->nodes as $node) {
if ($node->id() > 103) {
// The alias is set manually.
$node->set('path', ['alias' => '/custom_alias_' . $node->id()]);
}
$node->save();
}
}
// Delete information about the state of an alias to make sure that aliases
// with no such data are left alone by default.
\Drupal::keyValue('pathauto_state.node')->delete(101);
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);
}
}
}
}

View file

@ -0,0 +1,292 @@
<?php
namespace Drupal\pathauto\Tests;
use Drupal\pathauto\Entity\PathautoPattern;
use Drupal\node\Entity\Node;
use Drupal\pathauto\PathautoState;
use Drupal\simpletest\WebTestBase;
/**
* Tests pathauto node UI integration.
*
* @group pathauto
*/
class PathautoNodeWebTest extends WebTestBase {
use PathautoTestHelperTrait;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('node', 'pathauto', 'views', 'taxonomy', 'pathauto_views_test');
/**
* Admin user.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* {inheritdoc}
*/
function setUp() {
parent::setUp();
$this->drupalCreateContentType(array('type' => 'page', 'name' => 'Basic page'));
$this->drupalCreateContentType(array('type' => 'article'));
// Allow other modules to add additional permissions for the admin user.
$permissions = array(
'administer pathauto',
'administer url aliases',
'create url aliases',
'administer nodes',
'bypass node access',
'access content overview',
);
$this->adminUser = $this->drupalCreateUser($permissions);
$this->drupalLogin($this->adminUser);
$this->createPattern('node', '/content/[node:title]');
}
/**
* Tests editing nodes with different settings.
*/
function testNodeEditing() {
// Ensure that the Pathauto checkbox is checked by default on the node add form.
$this->drupalGet('node/add/page');
$this->assertFieldChecked('edit-path-0-pathauto');
// Create a node by saving the node form.
$title = ' Testing: node title [';
$automatic_alias = '/content/testing-node-title';
$this->drupalPostForm(NULL, array('title[0][value]' => $title), t('Save and publish'));
$node = $this->drupalGetNodeByTitle($title);
// Look for alias generated in the form.
$this->drupalGet("node/{$node->id()}/edit");
$this->assertFieldChecked('edit-path-0-pathauto');
$this->assertFieldByName('path[0][alias]', $automatic_alias, 'Generated alias visible in the path alias field.');
// Check whether the alias actually works.
$this->drupalGet($automatic_alias);
$this->assertText($title, 'Node accessible through automatic alias.');
// Manually set the node's alias.
$manual_alias = '/content/' . $node->id();
$edit = array(
'path[0][pathauto]' => FALSE,
'path[0][alias]' => $manual_alias,
);
$this->drupalPostForm($node->toUrl('edit-form'), $edit, t('Save and keep published'));
$this->assertText(t('@type @title has been updated.', array('@type' => 'page', '@title' => $title)));
// Check that the automatic alias checkbox is now unchecked by default.
$this->drupalGet("node/{$node->id()}/edit");
$this->assertNoFieldChecked('edit-path-0-pathauto');
$this->assertFieldByName('path[0][alias]', $manual_alias);
// Submit the node form with the default values.
$this->drupalPostForm(NULL, array('path[0][pathauto]' => FALSE), t('Save and keep published'));
$this->assertText(t('@type @title has been updated.', array('@type' => 'page', '@title' => $title)));
// Test that the old (automatic) alias has been deleted and only accessible
// through the new (manual) alias.
$this->drupalGet($automatic_alias);
$this->assertResponse(404, 'Node not accessible through automatic alias.');
$this->drupalGet($manual_alias);
$this->assertText($title, 'Node accessible through manual alias.');
// Test that the manual alias is not kept for new nodes when the pathauto
// checkbox is ticked.
$title = 'Automatic Title';
$edit = array(
'title[0][value]' => $title,
'path[0][pathauto]' => TRUE,
'path[0][alias]' => '/should-not-get-created',
);
$this->drupalPostForm('node/add/page', $edit, t('Save and publish'));
$this->assertNoAliasExists(array('alias' => 'should-not-get-created'));
$node = $this->drupalGetNodeByTitle($title);
$this->assertEntityAlias($node, '/content/automatic-title');
// Remove the pattern for nodes, the pathauto checkbox should not be
// displayed.
$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');
$this->assertFieldByName('path[0][alias]', '');
$edit = array();
$edit['title'] = 'My test article';
$this->drupalCreateNode($edit);
//$this->drupalPostForm(NULL, $edit, t('Save and keep published'));
$node = $this->drupalGetNodeByTitle($edit['title']);
// Pathauto checkbox should still not exist.
$this->drupalGet($node->toUrl('edit-form'));
$this->assertNoFieldById('edit-path-0-pathauto');
$this->assertFieldByName('path[0][alias]', '');
$this->assertNoEntityAlias($node);
}
/**
* Test node operations.
*/
function testNodeOperations() {
$node1 = $this->drupalCreateNode(array('title' => 'node1'));
$node2 = $this->drupalCreateNode(array('title' => 'node2'));
// Delete all current URL aliases.
$this->deleteAllAliases();
$this->drupalGet('admin/content');
// Check which of the two nodes is first.
if (strpos($this->getTextContent(), 'node1') < strpos($this->getTextContent(), 'node2')) {
$index = 0;
}
else {
$index = 1;
}
$edit = array(
'action' => 'pathauto_update_alias_node',
'node_bulk_form[' . $index . ']' => TRUE,
);
$this->drupalPostForm(NULL, $edit, t('Apply to selected items'));
$this->assertText('Update URL alias was applied to 1 item.');
$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::entityTypeManager()->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::entityTypeManager()->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::entityTypeManager()->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');
}
/**
* Tests that nodes without a Pathauto pattern can set custom aliases.
*/
public function testCustomAliasWithoutPattern() {
// First, delete all patterns to be sure that there will be no match.
$entity_ids = \Drupal::entityQuery('pathauto_pattern')->execute();
$entities = PathautoPattern::loadMultiple($entity_ids);
foreach ($entities as $entity) {
$entity->delete();
}
// Next, create a node with a custom alias.
$edit = [
'title[0][value]' => 'Sample article',
'path[0][alias]' => '/sample-article',
];
$this->drupalPostForm('node/add/article', $edit, t('Save and publish'));
$this->assertText(t('article Sample article has been created.'));
// Test the alias.
$this->assertAliasExists(array('alias' => '/sample-article'));
$this->drupalGet('sample-article');
$this->assertResponse(200, 'A node without a pattern can have a custom alias.');
// Now create a node through the API.
$node = Node::create(['type' => 'article', 'title' => 'Sample article API', 'path' => ['alias' => '/sample-article-api']]);
$node->save();
// Test the alias.
$this->assertAliasExists(['alias' => '/sample-article-api']);
$this->drupalGet('sample-article-api');
$this->assertResponse(200);
}
}

View file

@ -0,0 +1,240 @@
<?php
namespace Drupal\pathauto\Tests;
use Drupal\pathauto\PathautoGeneratorInterface;
use Drupal\simpletest\WebTestBase;
/**
* Tests pathauto settings form.
*
* @group pathauto
*/
class PathautoSettingsFormWebTest extends WebTestBase {
use PathautoTestHelperTrait;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('node', 'pathauto');
/**
* Admin user.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* Form values that are set by default.
*
* @var array
*/
protected $defaultFormValues = array(
'verbose' => FALSE,
'separator' => '-',
'case' => '1',
'max_length' => '100',
'max_component_length' => '100',
'update_action' => '2',
'transliterate' => '1',
'reduce_ascii' => FALSE,
'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',
);
/**
* Punctuation form items with default values.
*
* @var array
*/
protected $defaultPunctuations = array(
'punctuation[double_quotes]' => '0',
'punctuation[quotes]' => '0',
'punctuation[backtick]' => '0',
'punctuation[comma]' => '0',
'punctuation[period]' => '0',
'punctuation[hyphen]' => '1',
'punctuation[underscore]' => '0',
'punctuation[colon]' => '0',
'punctuation[semicolon]' => '0',
'punctuation[pipe]' => '0',
'punctuation[left_curly]' => '0',
'punctuation[left_square]' => '0',
'punctuation[right_curly]' => '0',
'punctuation[right_square]' => '0',
'punctuation[plus]' => '0',
'punctuation[equal]' => '0',
'punctuation[asterisk]' => '0',
'punctuation[ampersand]' => '0',
'punctuation[percent]' => '0',
'punctuation[caret]' => '0',
'punctuation[dollar]' => '0',
'punctuation[hash]' => '0',
'punctuation[exclamation]' => '0',
'punctuation[tilde]' => '0',
'punctuation[left_parenthesis]' => '0',
'punctuation[right_parenthesis]' => '0',
'punctuation[question_mark]' => '0',
'punctuation[less_than]' => '0',
'punctuation[greater_than]' => '0',
'punctuation[slash]' => '0',
'punctuation[back_slash]' => '0',
);
/**
* {inheritdoc}
*/
function setUp() {
parent::setUp();
$this->drupalCreateContentType(array('type' => 'article'));
$permissions = array(
'administer pathauto',
'notify of path changes',
'administer url aliases',
'create url aliases',
'administer nodes',
'bypass node access',
);
$this->adminUser = $this->drupalCreateUser($permissions);
$this->drupalLogin($this->adminUser);
$this->createPattern('node', '/content/[node:title]');
}
/**
* Test if the default values are shown correctly in the form.
*/
function testDefaultFormValues() {
$this->drupalGet('/admin/config/search/path/settings');
$this->assertNoFieldChecked('edit-verbose');
$this->assertField('edit-separator', $this->defaultFormValues['separator']);
$this->assertFieldChecked('edit-case');
$this->assertField('edit-max-length', $this->defaultFormValues['max_length']);
$this->assertField('edit-max-component-length', $this->defaultFormValues['max_component_length']);
$this->assertFieldChecked('edit-update-action-2');
$this->assertFieldChecked('edit-transliterate');
$this->assertNoFieldChecked('edit-reduce-ascii');
$this->assertField('edit-ignore-words', $this->defaultFormValues['ignore_words']);
}
/**
* Test the verbose option.
*/
function testVerboseOption() {
$edit = array('verbose' => '1');
$this->drupalPostForm('/admin/config/search/path/settings', $edit, t('Save configuration'));
$this->assertText(t('The configuration options have been saved.'));
$this->assertFieldChecked('edit-verbose');
$title = 'Verbose settings test';
$this->drupalGet('/node/add/article');
$this->assertFieldChecked('edit-path-0-pathauto');
$this->drupalPostForm(NULL, array('title[0][value]' => $title), t('Save and publish'));
$this->assertText('Created new alias /content/verbose-settings-test for');
$node = $this->drupalGetNodeByTitle($title);
$this->drupalPostForm('/node/' . $node->id() . '/edit', array('title[0][value]' => 'Updated title'), t('Save and keep published'));
$this->assertText('Created new alias /content/updated-title for');
$this->assertText('replacing /content/verbose-settings-test.');
}
/**
* Tests generating aliases with different settings.
*/
function testSettingsForm() {
// Ensure the separator settings apply correctly.
$this->checkAlias('My awesome content', '/content/my.awesome.content', array('separator' => '.'));
// Ensure the character case setting works correctly.
// Leave case the same as source token values.
$this->checkAlias('My awesome Content', '/content/My-awesome-Content', array('case' => FALSE));
$this->checkAlias('Change Lower', '/content/change-lower', array('case' => '1'));
// Ensure the maximum alias length is working.
$this->checkAlias('My awesome Content', '/content/my-awesome', array('max_length' => '23'));
// Ensure the maximum component length is working.
$this->checkAlias('My awesome Content', '/content/my', array('max_component_length' => '2'));
// Ensure transliteration option is working.
$this->checkAlias('è é àl ö äl ü', '/content/e-e-al-o-al-u', array('transliterate' => '1'));
$this->checkAlias('è é àl äl ö ü', '/content/è-é-àl-äl-ö-ü', array('transliterate' => FALSE));
$ignore_words = 'a, new, very, should';
$this->checkAlias('a very new alias to test', '/content/alias-to-test', array('ignore_words' => $ignore_words));
}
/**
* Test the punctuation setting form items.
*/
function testPunctuationSettings() {
// Test the replacement of punctuations.
$settings = [];
foreach ($this->defaultPunctuations as $key => $punctuation) {
$settings[$key] = PathautoGeneratorInterface::PUNCTUATION_REPLACE;
}
$title = 'aa"b`c,d.e-f_g:h;i|j{k[l}m]n+o=p*q%r^s$t#u!v~w(x)y?z>1/2\3';
$alias = '/content/aa-b-c-d-e-f-g-h-i-j-k-l-m-n-o-p-q-r-s-t-u-v-w-x-y-z-1-2-3';
$this->checkAlias($title, $alias, $settings);
// Test the removal of punctuations.
$settings = [];
foreach ($this->defaultPunctuations as $key => $punctuation) {
$settings[$key] = PathautoGeneratorInterface::PUNCTUATION_REMOVE;
}
$title = 'a"b`c,d.e-f_g:h;i|j{k[l}m]n+o=p*q%r^s$t#u!v~w(x)y?z>1/2\3';
$alias = '/content/abcdefghijklmnopqrstuvwxyz123';
$this->checkAlias($title, $alias, $settings);
// Keep all punctuations in alias.
$settings = [];
foreach ($this->defaultPunctuations as $key => $punctuation) {
$settings[$key] = PathautoGeneratorInterface::PUNCTUATION_DO_NOTHING;
}
$title = 'al"b`c,d.e-f_g:h;i|j{k[l}m]n+o=p*q%r^s$t#u!v~w(x)y?z>1/2\3';
$alias = '/content/al"b`c,d.e-f_g:h;i|j{k[l}m]n+o=p*q%r^s$t#u!v~w(x)y?z>1/2\3';
$this->checkAlias($title, $alias, $settings);
}
/**
* Helper method to check the an aliases.
*
* @param string $title
* The node title to build the aliases from.
* @param string $alias
* The expected alias.
* @param array $settings
* The form values the alias should be generated with.
*/
protected function checkAlias($title, $alias, $settings = array()) {
// Submit the settings form.
$edit = array_merge($this->defaultFormValues + $this->defaultPunctuations, $settings);
$this->drupalPostForm('/admin/config/search/path/settings', $edit, t('Save configuration'));
$this->assertText(t('The configuration options have been saved.'));
// If we do not clear the caches here, AliasCleaner will use its
// cleanStringCache instance variable. Due to that the creation of aliases
// with $this->createNode() will only work correctly on the first call.
\Drupal::service('pathauto.generator')->resetCaches();
// Create a node and check if the settings applied.
$node = $this->createNode(
array(
'title' => $title,
'type' => 'article',
)
);
$this->drupalGet($alias);
$this->assertResponse(200);
$this->assertEntityAlias($node, $alias);
}
}

View file

@ -0,0 +1,102 @@
<?php
namespace Drupal\pathauto\Tests;
use Drupal\simpletest\WebTestBase;
/**
* Tests pathauto taxonomy UI integration.
*
* @group pathauto
*/
class PathautoTaxonomyWebTest extends WebTestBase {
use PathautoTestHelperTrait;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('taxonomy', 'pathauto', 'views');
/**
* Admin user.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* {inheritdoc}
*/
function setUp() {
parent::setUp();
// Allow other modules to add additional permissions for the admin user.
$permissions = array(
'administer pathauto',
'administer url aliases',
'create url aliases',
'administer taxonomy',
);
$this->adminUser = $this->drupalCreateUser($permissions);
$this->drupalLogin($this->adminUser);
$this->createPattern('taxonomy_term', '/[term:vocabulary]/[term:name]');
}
/**
* Basic functional testing of Pathauto with taxonomy terms.
*/
function testTermEditing() {
$this->drupalGet('admin/structure');
$this->drupalGet('admin/structure/taxonomy');
// Add vocabulary "tags".
$vocabulary = $this->addVocabulary(array('name' => 'tags', 'vid' => 'tags'));
// Create term for testing.
$name = 'Testing: term name [';
$automatic_alias = '/tags/testing-term-name';
$this->drupalPostForm('admin/structure/taxonomy/manage/tags/add', array('name[0][value]' => $name), 'Save');
$name = trim($name);
$this->assertText("Created new term $name.");
$term = $this->drupalGetTermByName($name);
// Look for alias generated in the form.
$this->drupalGet("taxonomy/term/{$term->id()}/edit");
$this->assertFieldChecked('edit-path-0-pathauto');
$this->assertFieldByName('path[0][alias]', $automatic_alias, 'Generated alias visible in the path alias field.');
// Check whether the alias actually works.
$this->drupalGet($automatic_alias);
$this->assertText($name, 'Term accessible through automatic alias.');
// Manually set the term's alias.
$manual_alias = '/tags/' . $term->id();
$edit = array(
'path[0][pathauto]' => FALSE,
'path[0][alias]' => $manual_alias,
);
$this->drupalPostForm("taxonomy/term/{$term->id()}/edit", $edit, t('Save'));
$this->assertText("Updated term $name.");
// Check that the automatic alias checkbox is now unchecked by default.
$this->drupalGet("taxonomy/term/{$term->id()}/edit");
$this->assertNoFieldChecked('edit-path-0-pathauto');
$this->assertFieldByName('path[0][alias]', $manual_alias);
// Submit the term form with the default values.
$this->drupalPostForm(NULL, array('path[0][pathauto]' => FALSE), t('Save'));
$this->assertText("Updated term $name.");
// Test that the old (automatic) alias has been deleted and only accessible
// through the new (manual) alias.
$this->drupalGet($automatic_alias);
$this->assertResponse(404, 'Term not accessible through automatic alias.');
$this->drupalGet($manual_alias);
$this->assertText($name, 'Term accessible through manual alias.');
}
}

View file

@ -0,0 +1,190 @@
<?php
namespace Drupal\pathauto\Tests;
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;
use Drupal\taxonomy\Entity\Vocabulary;
use Drupal\taxonomy\Entity\Term;
/**
* Helper test class with some added functions for testing.
*/
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) {
$type = ($entity_type_id == 'forum') ? 'forum' : 'canonical_entities:' . $entity_type_id;
$pattern = PathautoPattern::create([
'id' => Unicode::strtolower($this->randomMachineName()),
'type' => $type,
'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) {
$plugin_id = $entity_type == 'node' ? 'node_type' : 'entity_bundle:' . $entity_type;
$pattern->addSelectionCondition(
[
'id' => $plugin_id,
'bundles' => [
$bundle => $bundle,
],
'negate' => FALSE,
'context_mapping' => [
$entity_type => $entity_type,
]
]
);
}
public function assertToken($type, $object, $token, $expected) {
$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)));
}
public function saveAlias($source, $alias, $langcode = Language::LANGCODE_NOT_SPECIFIED) {
\Drupal::service('path.alias_storage')->delete(array('source' => $source, 'language', 'langcode' => $langcode));
return \Drupal::service('path.alias_storage')->save($source, $alias, $langcode);
}
public function saveEntityAlias(EntityInterface $entity, $alias, $langcode = NULL) {
// By default, use the entity language.
if (!$langcode) {
$langcode = $entity->language()->getId();
}
return $this->saveAlias('/' . $entity->toUrl()->getInternalPath(), $alias, $langcode);
}
public function assertEntityAlias(EntityInterface $entity, $expected_alias, $langcode = NULL) {
// By default, use the entity language.
if (!$langcode) {
$langcode = $entity->language()->getId();
}
$this->assertAlias('/' . $entity->toUrl()->getInternalPath(), $expected_alias, $langcode);
}
public function assertEntityAliasExists(EntityInterface $entity) {
return $this->assertAliasExists(array('source' => '/' . $entity->toUrl()->getInternalPath()));
}
public function assertNoEntityAlias(EntityInterface $entity, $langcode = NULL) {
// By default, use the entity language.
if (!$langcode) {
$langcode = $entity->language()->getId();
}
$this->assertEntityAlias($entity, '/' . $entity->toUrl()->getInternalPath(), $langcode);
}
public function assertNoEntityAliasExists(EntityInterface $entity, $alias = NULL) {
$path = array('source' => '/' . $entity->toUrl()->getInternalPath());
if (!empty($alias)) {
$path['alias'] = $alias;
}
$this->assertNoAliasExists($path);
}
public function assertAlias($source, $expected_alias, $langcode = Language::LANGCODE_NOT_SPECIFIED) {
\Drupal::service('path.alias_manager')->cacheClear($source);
$this->assertEqual($expected_alias, \Drupal::service('path.alias_manager')->getAliasByPath($source, $langcode), t("Alias for %source with language '@language' is correct.",
array('%source' => $source, '@language' => $langcode)));
}
public function assertAliasExists($conditions) {
$path = \Drupal::service('path.alias_storage')->load($conditions);
$this->assertTrue($path, t('Alias with conditions @conditions found.', array('@conditions' => var_export($conditions, TRUE))));
return $path;
}
public function assertNoAliasExists($conditions) {
$alias = \Drupal::service('path.alias_storage')->load($conditions);
$this->assertFalse($alias, t('Alias with conditions @conditions not found.', array('@conditions' => var_export($conditions, TRUE))));
}
public function deleteAllAliases() {
\Drupal::database()->delete('url_alias')->execute();
\Drupal::service('path.alias_manager')->cacheClear();
}
/**
* @param array $values
* @return \Drupal\taxonomy\VocabularyInterface
*/
public function addVocabulary(array $values = array()) {
$name = Unicode::strtolower($this->randomMachineName(5));
$values += array(
'name' => $name,
'vid' => $name,
);
$vocabulary = Vocabulary::create($values);
$vocabulary->save();
return $vocabulary;
}
public function addTerm(VocabularyInterface $vocabulary, array $values = array()) {
$values += array(
'name' => Unicode::strtolower($this->randomMachineName(5)),
'vid' => $vocabulary->id(),
);
$term = Term::create($values);
$term->save();
return $term;
}
public function assertEntityPattern($entity_type, $bundle, $langcode = Language::LANGCODE_NOT_SPECIFIED, $expected) {
$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) {
if ($reset) {
// @todo - implement cache reset.
}
$terms = \Drupal::entityTypeManager()->getStorage('taxonomy_term')->loadByProperties(array('name' => $name));
return !empty($terms) ? reset($terms) : FALSE;
}
}

View file

@ -0,0 +1,189 @@
<?php
namespace Drupal\pathauto\Tests;
use Drupal\simpletest\WebTestBase;
use Drupal\pathauto\Entity\PathautoPattern;
/**
* Test basic pathauto functionality.
*
* @group pathauto
*/
class PathautoUiTest extends WebTestBase {
use PathautoTestHelperTrait;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('pathauto', 'node');
/**
* Admin user.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* {inheritdoc}
*/
function setUp() {
parent::setUp();
$this->drupalCreateContentType(array('type' => 'page', 'name' => 'Basic page'));
$this->drupalCreateContentType(array('type' => 'article'));
// Allow other modules to add additional permissions for the admin user.
$permissions = array(
'administer pathauto',
'administer url aliases',
'create url aliases',
'administer nodes',
'bypass node access',
'access content overview',
);
$this->adminUser = $this->drupalCreateUser($permissions);
$this->drupalLogin($this->adminUser);
}
function testSettingsValidation() {
$edit = array();
$edit['max_length'] = 'abc';
$edit['max_component_length'] = 'abc';
$this->drupalPostForm('admin/config/search/path/settings', $edit, 'Save configuration');
/*$this->assertText('The field Maximum alias length is not a valid number.');
$this->assertText('The field Maximum component length is not a valid number.');*/
$this->assertNoText('The configuration options have been saved.');
$edit['max_length'] = '0';
$edit['max_component_length'] = '0';
$this->drupalPostForm('admin/config/search/path/settings', $edit, 'Save configuration');
/*$this->assertText('The field Maximum alias length cannot be less than 1.');
$this->assertText('The field Maximum component length cannot be less than 1.');*/
$this->assertNoText('The configuration options have been saved.');
$edit['max_length'] = '999';
$edit['max_component_length'] = '999';
$this->drupalPostForm('admin/config/search/path/settings', $edit, 'Save configuration');
/*$this->assertText('The field Maximum alias length cannot be greater than 255.');
$this->assertText('The field Maximum component length cannot be greater than 255.');*/
$this->assertNoText('The configuration options have been saved.');
$edit['max_length'] = '50';
$edit['max_component_length'] = '50';
$this->drupalPostForm('admin/config/search/path/settings', $edit, 'Save configuration');
$this->assertText('The configuration options have been saved.');
}
function testPatternsWorkflow() {
// Try to save an empty pattern, should not be allowed.
$this->drupalGet('admin/config/search/path/patterns/add');
$edit = array(
'type' => 'canonical_entities:node',
);
$this->drupalPostAjaxForm(NULL, $edit, 'type');
$edit += array(
'bundles[page]' => TRUE,
'label' => 'Page pattern',
'id' => 'page_pattern',
);
$this->drupalPostForm(NULL, $edit, 'Save');
$this->assertText('Path pattern field is required.');
$this->assertNoText('The configuration options have been saved.');
// Try to save an invalid pattern.
$edit += array(
'pattern' => '[node:title]/[user:name]/[term:name]',
);
$this->drupalPostForm(NULL, $edit, 'Save');
$this->assertText('Path pattern is using the following invalid tokens: [user:name], [term:name].');
$this->assertNoText('The configuration options have been saved.');
$edit['pattern'] = '#[node:title]';
$this->drupalPostForm(NULL, $edit, 'Save');
$this->assertText('The Path pattern is using the following invalid characters: #.');
$this->assertNoText('The configuration options have been saved.');
// Checking whitespace ending of the string.
$edit['pattern'] = '[node:title] ';
$this->drupalPostForm(NULL, $edit, 'Save');
$this->assertText('The Path pattern doesn\'t allow the patterns ending with whitespace.');
$this->assertNoText('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.');
\Drupal::service('pathauto.generator')->resetCaches();
// Create a node with pattern enabled and check if the pattern applies.
$title = 'Page Pattern enabled';
$alias = '/page-pattern-enabled';
$node = $this->createNode(['title' => $title, 'type' => 'page']);
$this->drupalGet($alias);
$this->assertResponse(200);
$this->assertEntityAlias($node, $alias);
// Edit workflow, set a new label and weight for the pattern.
$this->drupalPostForm('/admin/config/search/path/patterns', ['entities[page_pattern][weight]' => '4'], t('Save'));
$this->clickLink(t('Edit'));
$this->assertUrl('/admin/config/search/path/patterns/page_pattern');
$this->assertFieldByName('pattern', '[node:title]');
$this->assertFieldByName('label', 'Page pattern');
$this->assertFieldChecked('edit-status');
$this->assertLink(t('Delete'));
$edit = array('label' => 'Test');
$this->drupalPostForm('/admin/config/search/path/patterns/page_pattern', $edit, t('Save'));
$this->assertText('Pattern Test saved.');
// Check that the pattern weight did not change.
$this->assertOptionSelected('edit-entities-page-pattern-weight', '4');
// Disable workflow.
$this->drupalGet('/admin/config/search/path/patterns');
$this->assertNoLink(t('Enable'));
$this->clickLink(t('Disable'));
$this->assertUrl('/admin/config/search/path/patterns/page_pattern/disable');
$this->drupalPostForm(NULL, [], t('Disable'));
$this->assertText('Disabled pattern Test.');
// Load the pattern from storage and check if its disabled.
$pattern = PathautoPattern::load('page_pattern');
$this->assertFalse($pattern->status());
\Drupal::service('pathauto.generator')->resetCaches();
// Create a node with pattern disabled and check that we have no new alias.
$title = 'Page Pattern disabled';
$node = $this->createNode(['title' => $title, 'type' => 'page']);
$this->assertNoEntityAlias($node);
// Enable workflow.
$this->drupalGet('/admin/config/search/path/patterns');
$this->assertNoLink(t('Disable'));
$this->clickLink(t('Enable'));
$this->assertUrl('/admin/config/search/path/patterns/page_pattern/enable');
$this->drupalPostForm(NULL, [], t('Enable'));
$this->assertText('Enabled pattern Test.');
// Reload pattern from storage and check if its enabled.
$pattern = PathautoPattern::load('page_pattern');
$this->assertTrue($pattern->status());
// Delete workflow.
$this->drupalGet('/admin/config/search/path/patterns');
$this->clickLink(t('Delete'));
$this->assertUrl('/admin/config/search/path/patterns/page_pattern/delete');
$this->assertText(t('This action cannot be undone.'));
$this->drupalPostForm(NULL, [], t('Delete'));
$this->assertText('The pathauto pattern Test has been deleted.');
$this->assertFalse(PathautoPattern::load('page_pattern'));
}
}

View file

@ -0,0 +1,92 @@
<?php
namespace Drupal\pathauto\Tests;
use Drupal\Component\Utility\Unicode;
use Drupal\simpletest\WebTestBase;
use Drupal\views\Views;
/**
* Tests pathauto user UI integration.
*
* @group pathauto
*/
class PathautoUserWebTest extends WebTestBase {
use PathautoTestHelperTrait;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('pathauto', 'views');
/**
* Admin user.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* {inheritdoc}
*/
function setUp() {
parent::setUp();
// Allow other modules to add additional permissions for the admin user.
$permissions = array(
'administer pathauto',
'administer url aliases',
'create url aliases',
'administer users',
);
$this->adminUser = $this->drupalCreateUser($permissions);
$this->drupalLogin($this->adminUser);
$this->createPattern('user', '/users/[user:name]');
}
/**
* Basic functional testing of Pathauto with users.
*/
function testUserEditing() {
// There should be no Pathauto checkbox on user forms.
$this->drupalGet('user/' . $this->adminUser->id() . '/edit');
$this->assertNoFieldById('path[0][pathauto]');
}
/**
* Test user operations.
*/
function testUserOperations() {
$account = $this->drupalCreateUser();
// Delete all current URL aliases.
$this->deleteAllAliases();
// Find the position of just created account in the user_admin_people view.
$view = Views::getView('user_admin_people');
$view->initDisplay();
$view->preview('page_1');
foreach ($view->result as $key => $row) {
if ($view->field['name']->getValue($row) == $account->getUsername()) {
break;
}
}
$edit = array(
'action' => 'pathauto_update_alias_user',
"user_bulk_form[$key]" => TRUE,
);
$this->drupalPostForm('admin/people', $edit, t('Apply to selected items'));
$this->assertText('Update URL alias was applied to 1 item.');
$this->assertEntityAlias($account, '/users/' . Unicode::strtolower($account->getUsername()));
$this->assertEntityAlias($this->adminUser, '/user/' . $this->adminUser->id());
}
}

View file

@ -0,0 +1,63 @@
<?php
namespace Drupal\pathauto;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Session\AccountInterface;
/**
* Provides a verbose messenger.
*/
class VerboseMessenger implements MessengerInterface {
/**
* The verbose flag.
*
* @var bool
*/
protected $isVerbose;
/**
* The config factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* The current user account.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $account;
/**
* Creates a verbose messenger.
*/
public function __construct(ConfigFactoryInterface $config_factory, AccountInterface $account) {
$this->configFactory = $config_factory;
$this->account = $account;
}
/**
* {@inheritdoc}
*/
public function addMessage($message, $op = NULL) {
if (!isset($this->isVerbose)) {
$config = $this->configFactory->get('pathauto.settings');
$this->isVerbose = $config->get('verbose') && $this->account->hasPermission('notify of path changes');
}
if (!$this->isVerbose || (isset($op) && in_array($op, array('bulkupdate', 'return')))) {
return FALSE;
}
if ($message) {
drupal_set_message($message);
}
return TRUE;
}
}

View file

@ -0,0 +1,231 @@
langcode: en
status: true
dependencies:
module:
- node
- taxonomy
- user
id: articles
label: articles
module: pathauto_views_test
description: ''
tag: ''
base_table: node_field_data
base_field: nid
core: 8.x
display:
default:
display_plugin: default
id: default
display_title: Master
position: 0
display_options:
access:
type: perm
options:
perm: 'access content'
cache:
type: tag
options: { }
query:
type: views_query
options:
disable_sql_rewrite: false
distinct: false
replica: false
query_comment: ''
query_tags: { }
exposed_form:
type: basic
options:
submit_button: Apply
reset_button: false
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
pager:
type: full
options:
items_per_page: 10
offset: 0
id: 0
total_pages: null
expose:
items_per_page: false
items_per_page_label: 'Items per page'
items_per_page_options: '5, 10, 25, 50'
items_per_page_options_all: false
items_per_page_options_all_label: '- All -'
offset: false
offset_label: Offset
tags:
previous: ' Previous'
next: 'Next '
first: '« First'
last: 'Last »'
quantity: 9
style:
type: default
row:
type: 'entity:node'
options:
view_mode: teaser
fields:
title:
id: title
table: node_field_data
field: title
entity_type: node
entity_field: title
label: ''
alter:
alter_text: false
make_link: false
absolute: false
trim: false
word_boundary: false
ellipsis: false
strip_tags: false
html: false
hide_empty: false
empty_zero: false
settings:
link_to_entity: true
plugin_id: field
relationship: none
group_type: group
admin_label: ''
exclude: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_alter_empty: true
click_sort_column: value
type: string
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
filters:
status:
value: '1'
table: node_field_data
field: status
plugin_id: boolean
entity_type: node
entity_field: status
id: status
expose:
operator: ''
group: 1
type:
id: type
table: node_field_data
field: type
value:
article: article
entity_type: node
entity_field: type
plugin_id: bundle
sorts:
created:
id: created
table: node_field_data
field: created
order: DESC
entity_type: node
entity_field: created
plugin_id: date
relationship: none
group_type: group
admin_label: ''
exposed: false
expose:
label: ''
granularity: second
title: articles
header: { }
footer: { }
empty: { }
relationships: { }
arguments:
tid:
id: tid
table: taxonomy_index
field: tid
relationship: none
group_type: group
admin_label: ''
default_action: ignore
exception:
value: all
title_enable: false
title: All
title_enable: false
title: ''
default_argument_type: fixed
default_argument_options:
argument: ''
default_argument_skip_url: false
summary_options:
base_path: ''
count: true
items_per_page: 25
override: false
summary:
sort_order: asc
number_of_records: 0
format: default_summary
specify_validation: false
validate:
type: none
fail: 'not found'
validate_options: { }
break_phrase: false
add_table: false
require_value: false
reduce_duplicates: false
plugin_id: taxonomy_index_tid
display_extenders: { }
cache_metadata:
max-age: -1
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url
- url.query_args
- 'user.node_grants:view'
- user.permissions
tags: { }
page_1:
display_plugin: page
id: page_1
display_title: Page
position: 1
display_options:
display_extenders: { }
path: articles
cache_metadata:
max-age: -1
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url
- url.query_args
- 'user.node_grants:view'
- user.permissions
tags: { }

View file

@ -0,0 +1,14 @@
name: 'Views Test Config'
type: module
description: 'Provides default views for tests.'
package: Testing
# version: VERSION
# core: 8.x
dependencies:
- views
# Information added by Drupal.org packaging script on 2017-04-29
version: '8.x-1.0'
core: '8.x'
project: 'pathauto'
datestamp: 1493468049

View file

@ -0,0 +1,572 @@
<?php
namespace Drupal\Tests\pathauto\Kernel;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Language\Language;
use Drupal\Core\Language\LanguageInterface;
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\pathauto\Tests\PathautoTestHelperTrait;
use Drupal\KernelTests\KernelTestBase;
use Drupal\taxonomy\Entity\Term;
use Drupal\taxonomy\Entity\Vocabulary;
use Drupal\node\Entity\Node;
use Drupal\user\Entity\User;
/**
* Unit tests for Pathauto functions.
*
* @group pathauto
*/
class PathautoKernelTest extends KernelTestBase {
use PathautoTestHelperTrait;
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', '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 = User::create(array('name' => $this->randomMachineName()));
$this->currentUser->save();
}
/**
* Test _pathauto_get_schema_alias_maxlength().
*/
public function testGetSchemaAliasMaxLength() {
$this->assertIdentical(\Drupal::service('pathauto.alias_storage_helper')->getAliasSchemaMaxlength(), 255);
}
/**
* Test pathauto_pattern_load_by_entity().
*/
public function testPatternLoadByEntity() {
$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',
]
]
);
$pattern->addRelationship('node:langcode:language');
$pattern->save();
$pattern = $this->createPattern('node', '/[node:title]', -1);
$this->addBundleCondition($pattern, 'node', 'page');
$pattern->save();
$tests = array(
array(
'entity' => 'node',
'values' => [
'title' => 'Article fr',
'type' => 'article',
'langcode' => 'fr',
],
'expected' => '/article/[node:title]',
),
array(
'entity' => 'node',
'values' => [
'title' => 'Article en',
'type' => 'article',
'langcode' => 'en',
],
'expected' => '/article/en/[node:title]',
),
array(
'entity' => 'node',
'values' => [
'title' => 'Article und',
'type' => 'article',
'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
],
'expected' => '/article/[node:title]',
),
array(
'entity' => 'node',
'values' => [
'title' => 'Page',
'type' => 'page',
],
'expected' => '/[node:title]',
),
array(
'entity' => 'user',
'values' => [
'name' => 'User',
],
'expected' => '/users/[user:name]',
),
);
foreach ($tests as $test) {
$entity = \Drupal::entityTypeManager()->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'],
'@values' => print_r($test['values'], TRUE),
)));
}
}
/**
* Test potential conflicts with the same alias in different languages.
*/
public function testSameTitleDifferentLanguages() {
// Create two English articles with the same title.
$edit = [
'title' => 'Sample page',
'type' => 'page',
'langcode' => 'en',
];
$node1 = $this->drupalCreateNode($edit);
$this->assertEntityAlias($node1, '/content/sample-page', 'en');
$node2 = $this->drupalCreateNode($edit);
$this->assertEntityAlias($node2, '/content/sample-page-0', 'en');
// Now, create a French article with the same title, and verify that it gets
// the basic alias with the correct langcode.
$edit['langcode'] = 'fr';
$node3 = $this->drupalCreateNode($edit);
$this->assertEntityAlias($node3, '/content/sample-page', 'fr');
}
/**
* Test pathauto_cleanstring().
*/
public function testCleanString() {
// Test with default settings defined in pathauto.settings.yml.
$this->installConfig(array('pathauto'));
\Drupal::service('pathauto.generator')->resetCaches();
$tests = array();
// Test the 'ignored words' removal.
$tests['this'] = 'this';
$tests['this with that'] = 'this-with-that';
$tests['this thing with that thing'] = 'thing-thing';
// Test 'ignored words' removal and duplicate separator removal.
$tests[' - Pathauto is the greatest - module ever - '] = 'pathauto-greatest-module-ever';
// Test length truncation and lowering of strings.
$long_string = $this->randomMachineName(120);
$tests[$long_string] = strtolower(substr($long_string, 0, 100));
// Test that HTML tags are removed.
$tests['This <span class="text">text</span> has <br /><a href="http://example.com"><strong>HTML tags</strong></a>.'] = 'text-has-html-tags';
$tests[Html::escape('This <span class="text">text</span> has <br /><a href="http://example.com"><strong>HTML tags</strong></a>.')] = 'text-has-html-tags';
// Transliteration.
$tests['ľščťžýáíéňô'] = 'lsctzyaieno';
foreach ($tests as $input => $expected) {
$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,
)));
}
}
/**
* Test pathauto_clean_alias().
*/
public function testCleanAlias() {
$tests = array();
$tests['one/two/three'] = '/one/two/three';
$tests['/one/two/three/'] = '/one/two/three';
$tests['one//two///three'] = '/one/two/three';
$tests['one/two--three/-/--/-/--/four---five'] = '/one/two-three/four-five';
$tests['one/-//three--/four'] = '/one/three/four';
foreach ($tests as $input => $expected) {
$output = \Drupal::service('pathauto.alias_cleaner')->cleanAlias($input);
$this->assertEqual($output, $expected, t("Drupal::service('pathauto.generator')->cleanAlias('@input') expected '@expected', actual '@output'", array(
'@input' => $input,
'@expected' => $expected,
'@output' => $output,
)));
}
}
/**
* Test pathauto_path_delete_multiple().
*/
public function testPathDeleteMultiple() {
$this->saveAlias('/node/1', '/node-1-alias');
$this->saveAlias('/node/1/view', '/node-1-alias/view');
$this->saveAlias('/node/1', '/node-1-alias-en', 'en');
$this->saveAlias('/node/1', '/node-1-alias-fr', 'fr');
$this->saveAlias('/node/2', '/node-2-alias');
$this->saveAlias('/node/10', '/node-10-alias');
\Drupal::service('pathauto.alias_storage_helper')->deleteBySourcePrefix('/node/1');
$this->assertNoAliasExists(array('source' => "/node/1"));
$this->assertNoAliasExists(array('source' => "/node/1/view"));
$this->assertAliasExists(array('source' => "/node/2"));
$this->assertAliasExists(array('source' => "/node/10"));
}
/**
* 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', PathautoGeneratorInterface::UPDATE_ACTION_NO_NEW);
$config->save();
$node = $this->drupalCreateNode(array('title' => 'First title'));
$this->assertEntityAlias($node, '/content/first-title');
$node->path->pathauto = PathautoState::CREATE;
// Default action is PATHAUTO_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'));
// Test PATHAUTO_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->toUrl()->getInternalPath(), 'alias' => '/content/second-title'));
$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'));
// The older second alias is not deleted yet.
$older_path = $this->assertAliasExists(array('source' => '/' . $node->toUrl()->getInternalPath(), 'alias' => '/content/second-title'));
\Drupal::service('path.alias_storage')->delete($older_path);
$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'));
// Test PATHAUTO_UPDATE_ACTION_NO_NEW with unaliased node and 'update'.
$this->deleteAllAliases();
$node->save();
$this->assertEntityAlias($node, '/content/fifth-title');
// Test PATHAUTO_UPDATE_ACTION_NO_NEW with unaliased node and 'bulkupdate'.
$this->deleteAllAliases();
$node->setTitle('Sixth title');
\Drupal::service('pathauto.generator')->updateEntityAlias($node, 'bulkupdate');
$this->assertEntityAlias($node, '/content/sixth-title');
}
/**
* 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() {
$this->installConfig(['filter']);
$this->nodePattern
->setPattern('/content/[node:body]')
->save();
$node = $this->drupalCreateNode();
$this->assertNoEntityAliasExists($node);
$node->body->value = 'hello';
$node->save();
$this->assertEntityAlias($node, '/content/hello');
}
/**
* Test the handling of path vs non-path tokens in pathauto_clean_token_values().
*/
public function testPathTokens() {
$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');
$term2 = $this->addTerm($vocab, array('name' => 'Child term', 'parent' => $term1->id()));
$this->assertEntityAlias($term2, '/parent-term/child-term');
$this->saveEntityAlias($term1, '/My Crazy/Alias/');
$term2->save();
$this->assertEntityAlias($term2, '/My Crazy/Alias/child-term');
}
/**
* 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.
$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();
$display = entity_get_display('taxonomy_term', 'tags', 'default');
$display->setComponent($fieldname, ['type' => 'string']);
$display->save();
// 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 . ']');
// 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));
}
/**
* 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', PathautoGeneratorInterface::PUNCTUATION_DO_NOTHING)
->save();
$this->nodePattern
->setPattern('[node:title]')
->save();
// Check that Pathauto does not create an alias of '/admin'.
$node = $this->drupalCreateNode(array('title' => 'Admin', 'type' => 'page'));
$this->assertEntityAlias($node, '/admin-0');
// Check that Pathauto does not create an alias of '/modules'.
$node->setTitle('Modules');
$node->save();
$this->assertEntityAlias($node, '/modules-0');
// Check that Pathauto does not create an alias of '/index.php'.
$node->setTitle('index.php');
$node->save();
$this->assertEntityAlias($node, '/index.php-0');
// Check that a safe value gets an automatic alias. This is also a control
// to ensure the above tests work properly.
$node->setTitle('Safe value');
$node->save();
$this->assertEntityAlias($node, '/safe-value');
}
/**
* Test programmatic entity creation for aliases.
*/
function testProgrammaticEntityCreation() {
$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');
$vocabulary = $this->addVocabulary(array('name' => 'Tags'));
$term = $this->addTerm($vocabulary, array('name' => 'Test term', 'path' => array('pathauto' => TRUE)));
$this->assertEntityAlias($term, '/tags/test-term');
$edit['name'] = 'Test user';
$edit['mail'] = 'test-user@example.com';
$edit['pass'] = user_password();
$edit['path'] = array('pathauto' => TRUE);
$edit['status'] = 1;
$account = User::create($edit);
$account->save();
$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');
}
/**
* Test if aliases are (not) generated with enabled/disabled patterns.
*/
function testPatternStatus() {
// Create a node to get an alias for.
$title = 'Pattern enabled';
$alias = '/content/pattern-enabled';
$node1 = $this->drupalCreateNode(['title' => $title, 'type' => 'page']);
$this->assertEntityAlias($node1, $alias);
// Disable the pattern, save the node again and make sure the alias is still
// working.
$this->nodePattern->setStatus(FALSE)->save();
$node1->save();
$this->assertEntityAlias($node1, $alias);
// Create a new node with disabled pattern and make sure there is no new
// alias created.
$title = 'Pattern disabled';
$node2 = $this->drupalCreateNode(['title' => $title, 'type' => 'page']);
$this->assertNoEntityAlias($node2);
}
/**
* Tests that enabled entity types genrates the necessary fields and plugins.
*/
public function testSettingChangeInvalidatesCache() {
$this->installConfig(['pathauto']);
$this->enableModules(['entity_test']);
$definitions = \Drupal::service('plugin.manager.alias_type')->getDefinitions();
$this->assertFalse(isset($definitions['canonical_entities:entity_test']));
$fields = \Drupal::service('entity_field.manager')->getBaseFieldDefinitions('entity_test');
$this->assertFalse(isset($fields['path']));
$this->config('pathauto.settings')
->set('enabled_entity_types', ['user', 'entity_test'])
->save();
$definitions = \Drupal::service('plugin.manager.alias_type')->getDefinitions();
$this->assertTrue(isset($definitions['canonical_entities:entity_test']));
$fields = \Drupal::service('entity_field.manager')->getBaseFieldDefinitions('entity_test');
$this->assertTrue(isset($fields['path']));
}
/**
* Tests that aliases are only generated for default revisions.
*/
public function testDefaultRevision() {
$node1 = $this->drupalCreateNode(['title' => 'Default revision', 'type' => 'page']);
$this->assertEntityAlias($node1, '/content/default-revision');
$node1->setNewRevision(TRUE);
$node1->isDefaultRevision(FALSE);
$node1->setTitle('New non-default-revision');
$node1->save();
$this->assertEntityAlias($node1, '/content/default-revision');
}
/**
* 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(
'title' => $this->randomMachineName(8),
'type' => 'page',
);
$node = Node::create($settings);
$node->save();
return $node;
}
}

View file

@ -0,0 +1,78 @@
<?php
namespace Drupal\Tests\pathauto\Kernel;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests tokens provided by Pathauto.
*
* @group pathauto
*/
class PathautoTokenTest extends KernelTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('system', 'token', 'pathauto');
public function testPathautoTokens() {
$this->installConfig(array('pathauto'));
$array = array(
'test first arg',
'The Array / value',
);
$tokens = array(
'join-path' => 'test-first-arg/array-value',
);
$data['array'] = $array;
$replacements = $this->assertTokens('array', $data, $tokens);
// Ensure that the cleanTokenValues() method does not alter this token value.
/* @var \Drupal\pathauto\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');
}
/**
* Function copied from TokenTestHelper::assertTokens().
*/
public function assertTokens($type, array $data, array $tokens, array $options = array()) {
$input = $this->mapTokenNames($type, array_keys($tokens));
$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)) {
$this->assertTrue(!isset($values[$token]), t("Token value for @token was not generated.", array('@type' => $type, '@token' => $token)));
}
elseif (!isset($replacements[$token])) {
$this->fail(t("Token value for @token was not generated.", array('@type' => $type, '@token' => $token)));
}
elseif (!empty($options['regex'])) {
$this->assertTrue(preg_match('/^' . $expected . '$/', $replacements[$token]), t("Token value for @token was '@actual', matching regular expression pattern '@expected'.", array('@type' => $type, '@token' => $token, '@actual' => $replacements[$token], '@expected' => $expected)));
}
else {
$this->assertIdentical($replacements[$token], $expected, t("Token value for @token was '@actual', expected value '@expected'.", array('@type' => $type, '@token' => $token, '@actual' => $replacements[$token], '@expected' => $expected)));
}
}
return $replacements;
}
public function mapTokenNames($type, array $tokens = array()) {
$return = array();
foreach ($tokens as $token) {
$return[$token] = "[$type:$token]";
}
return $return;
}
}

View file

@ -0,0 +1,59 @@
<?php
namespace Drupal\Tests\pathauto\Unit {
use Drupal\pathauto\VerboseMessenger;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\pathauto\VerboseMessenger
* @group pathauto
*/
class VerboseMessengerTest extends UnitTestCase {
/**
* The messenger under test.
*
* @var \Drupal\pathauto\VerboseMessenger
*/
protected $messenger;
/**
* {@inheritdoc}
*/
protected function setUp() {
$config_factory = $this->getConfigFactoryStub(array('pathauto.settings' => array('verbose' => TRUE)));
$account = $this->getMock('\Drupal\Core\Session\AccountInterface');
$account->expects($this->once())
->method('hasPermission')
->withAnyParameters()
->willReturn(TRUE);
$this->messenger = new VerboseMessenger($config_factory, $account);
}
/**
* Tests add messages.
* @covers ::addMessage
*/
public function testAddMessage() {
$this->assertTrue($this->messenger->addMessage("Test message"), "The message was added");
}
/**
* @covers ::addMessage
*/
public function testDoNotAddMessageWhileBulkupdate() {
$this->assertFalse($this->messenger->addMessage("Test message", "bulkupdate"), "The message was NOT added");
}
}
}
namespace {
// @todo Delete after https://drupal.org/node/1858196 is in.
if (!function_exists('drupal_set_message')) {
function drupal_set_message() {
}
}
}