first commit

This commit is contained in:
2024-07-15 12:33:27 +02:00
commit ce50ae282b
22084 changed files with 2623791 additions and 0 deletions

View File

@@ -0,0 +1,110 @@
################
# DrupalCI GitLabCI template
#
# Gitlab-ci.yml to replicate DrupalCI testing for Contrib
#
# With thanks to:
# * The GitLab Acceleration Initiative participants
# * DrupalSpoons
################
################
# Guidelines
#
# This template is designed to give any Contrib maintainer everything they need to test, without requiring modification. It is also designed to keep up to date with Core Development automatically through the use of include files that can be centrally maintained.
#
# However, you can modify this template if you have additional needs for your project.
################
################
# Includes
#
# Additional configuration can be provided through includes.
# One advantage of include files is that if they are updated upstream, the changes affect all pipelines using that include.
#
# Includes can be overridden by re-declaring anything provided in an include, here in gitlab-ci.yml
# https://docs.gitlab.com/ee/ci/yaml/includes.html#override-included-configuration-values
################
include:
################
# DrupalCI includes:
# As long as you include this, any future includes added by the Drupal Association will be accessible to your pipelines automatically.
# View these include files at https://git.drupalcode.org/project/gitlab_templates/
################
- project: $_GITLAB_TEMPLATES_REPO
ref: $_GITLAB_TEMPLATES_REF
file:
- '/includes/include.drupalci.main.yml'
- '/includes/include.drupalci.variables.yml'
- '/includes/include.drupalci.workflows.yml'
################
# Pipeline configuration variables
#
# These are the variables provided to the Run Pipeline form that a user may want to override.
#
# Docs at https://git.drupalcode.org/project/gitlab_templates/-/blob/1.0.x/includes/include.drupalci.variables.yml
################
# variables:
# SKIP_ESLINT: '1'
variables:
OPT_IN_TEST_PREVIOUS_MINOR: '1'
_PHPUNIT_CONCURRENT: 1
composer (next major):
variables:
_LENIENT_ALLOW_LIST: "ctools"
PHP_VERSION: $CORE_PHP_MAX
DRUPAL_CORE: $CORE_MAJOR_DEVELOPMENT
IGNORE_PROJECT_DRUPAL_CORE_VERSION: 1
phpunit (next major):
variables:
SYMFONY_DEPRECATIONS_HELPER: "disabled"
###################################################################################
#
# *
# /(
# ((((,
# /(((((((
# ((((((((((*
# ,(((((((((((((((
# ,(((((((((((((((((((
# ((((((((((((((((((((((((*
# *(((((((((((((((((((((((((((((
# ((((((((((((((((((((((((((((((((((*
# *(((((((((((((((((( .((((((((((((((((((
# ((((((((((((((((((. /(((((((((((((((((*
# /((((((((((((((((( .(((((((((((((((((,
# ,(((((((((((((((((( ((((((((((((((((((
# .(((((((((((((((((((( .(((((((((((((((((
# ((((((((((((((((((((((( ((((((((((((((((/
# (((((((((((((((((((((((((((/ ,(((((((((((((((*
# .((((((((((((((/ /(((((((((((((. ,(((((((((((((((
# *(((((((((((((( ,(((((((((((((/ *((((((((((((((.
# ((((((((((((((, /(((((((((((((. ((((((((((((((,
# (((((((((((((/ ,(((((((((((((* ,(((((((((((((,
# *((((((((((((( .((((((((((((((( ,(((((((((((((
# ((((((((((((/ /((((((((((((((((((. ,((((((((((((/
# ((((((((((((( *(((((((((((((((((((((((* *((((((((((((
# ((((((((((((( ,(((((((((((((..((((((((((((( *((((((((((((
# ((((((((((((, /((((((((((((* /((((((((((((/ ((((((((((((
# ((((((((((((( /((((((((((((/ (((((((((((((* ((((((((((((
# (((((((((((((/ /(((((((((((( ,((((((((((((, *((((((((((((
# (((((((((((((( *(((((((((((/ *((((((((((((. ((((((((((((/
# *((((((((((((((((((((((((((, /(((((((((((((((((((((((((
# ((((((((((((((((((((((((( ((((((((((((((((((((((((,
# .(((((((((((((((((((((((/ ,(((((((((((((((((((((((
# ((((((((((((((((((((((/ ,(((((((((((((((((((((/
# *((((((((((((((((((((( (((((((((((((((((((((,
# ,(((((((((((((((((((((, ((((((((((((((((((((/
# ,(((((((((((((((((((((* /((((((((((((((((((((
# ((((((((((((((((((((((, ,/((((((((((((((((((((,
# ,(((((((((((((((((((((((((((((((((((((((((((((((((((
# .(((((((((((((((((((((((((((((((((((((((((((((
# .((((((((((((((((((((((((((((((((((((,.
# .,(((((((((((((((((((((((((.
#
###################################################################################

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.

121
modules/pathauto/README.md Normal file
View File

@@ -0,0 +1,121 @@
CONTENTS OF THIS FILE
---------------------
* Introduction
* Requirements
* Recommended Modules
* Installation
* Configuration
* Notices
* Troubleshooting
* Maintainers
INTRODUCTION
------------
The Pathauto module provides support functions for other modules to
automatically generate aliases based on appropriate criteria and tokens, 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.
Pathauto is beneficial for search engine optimization (SEO) and for ease-of-use
for visitors.
* For a full description of the module, visit the project page:
https://www.drupal.org/project/pathauto
* To submit bug reports and feature suggestions, or track changes:
https://www.drupal.org/project/issues/pathauto
REQUIREMENTS
------------
This module requires the following module:
* Token - https://www.drupal.org/project/token
RECOMMENDED MODULES
-------------------
* Redirect - https://www.drupal.org/project/redirect
* Sub-pathauto (Sub-path URL Aliases) -
https://www.drupal.org/project/subpathauto
INSTALLATION
------------
* Install as you would normally install a contributed Drupal module. Visit
https://www.drupal.org/node/1897420 for further information.
CONFIGURATION
-------------
1. Configure the module at Administration > Configuration > Search and metadata
> URL aliases > Patterns (admin/config/search/path/patterns). Add a new
pattern by clicking "Add Pathauto pattern".
2. Select the entity type for "Pattern Type", and provide an administrative
label.
2. Fill out "Path pattern" with a token replacement pattern, such as
[node:title]. Use the "Browse available tokens" link to view available
variables to construct a URL alias pattern.
3. Click "Save" to save your pattern. When you save new content from now on, it
will automatically be assigned the pathauto-configured URL alias.
NOTICES
-------
Pathauto adds URL aliases to content, users, and taxonomy terms. Because the
patterns are 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, 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, Drupal
will replace it with the URL alias.
TROUBLESHOOTING
---------------
Q: Why are URLs not getting replaced with aliases?
A: Only URLs passed through the Drupal URL and Link APIs will be replaced
with their aliases during page output. If a module or a template contains
hardcoded links (such as 'href="node/$node->nid"'), those will not get
replaced with their corresponding aliases.
Q: How do you disable Pathauto for a specific content type (or taxonomy)?
A: When the pattern for a content type is left blank, the default pattern will
be used. If the default pattern is also blank, Pathauto will be disabled
for that content type.
MAINTAINERS
-----------
Current maintainers:
* Dave Reid - http://www.davereid.net
* Sascha Grossenbacher - https://www.drupal.org/u/berdir
The original Pathauto release 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 (now deprecated) tracker
support. Other suggestions and patches have been contributed by the Drupal
community.

View File

@@ -0,0 +1,29 @@
{
"name": "drupal/pathauto",
"description": "Provides a mechanism for modules to automatically generate aliases for the content they manage.",
"type": "drupal-module",
"license": "GPL-2.0-or-later",
"homepage": "https://www.drupal.org/project/pathauto",
"support": {
"issues": "https://www.drupal.org/project/issues/pathauto",
"documentation": "https://www.drupal.org/docs/8/modules/pathauto",
"source": "https://cgit.drupalcode.org/pathauto"
},
"require": {
"drupal/token": "*",
"drupal/ctools": "*"
},
"require-dev": {
"drupal/forum": "*"
},
"suggest": {
"drupal/redirect": "When installed Pathauto will provide a new \"Update Action\" in case your URLs change. This is the recommended update action and is considered the best practice for SEO and usability."
},
"extra": {
"drush": {
"services": {
"drush.services.yml": "^9 || ^10"
}
}
}
}

View File

@@ -0,0 +1,9 @@
services:
pathauto.commands:
class: \Drupal\pathauto\Commands\PathautoCommands
arguments:
- '@config.factory'
- '@plugin.manager.alias_type'
- '@pathauto.alias_storage_helper'
tags:
- { name: drush.command }

BIN
modules/pathauto/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -0,0 +1,23 @@
id: d7_pathauto_patterns
label: Pathauto patterns
migration_tags:
- Drupal 7
- Configuration
source:
plugin: pathauto_pattern
constants:
status: true
selection_logic: 'and'
process:
status: constants/status
id: id
label: label
type: type
pattern: pattern
selection_criteria: selection_criteria
selection_logic: constants/selection_logic
destination:
plugin: 'entity:pathauto_pattern'
migration_dependencies:
optional:
- d7_node_type

View File

@@ -0,0 +1,95 @@
id: d7_pathauto_settings
label: Pathauto configuration
migration_tags:
- Drupal 7
- Configuration
source:
plugin: variable
variables:
- pathauto_punctuation_ampersand
- pathauto_punctuation_asterisk
- pathauto_punctuation_at
- pathauto_punctuation_backtick
- pathauto_punctuation_back_slash
- pathauto_punctuation_caret
- pathauto_punctuation_colon
- pathauto_punctuation_comma
- pathauto_punctuation_dollar
- pathauto_punctuation_double_quotes
- pathauto_punctuation_equal
- pathauto_punctuation_exclamation
- pathauto_punctuation_greater_than
- pathauto_punctuation_hash
- pathauto_punctuation_hyphen
- pathauto_punctuation_left_curly
- pathauto_punctuation_left_parenthesis
- pathauto_punctuation_left_square
- pathauto_punctuation_less_than
- pathauto_punctuation_percent
- pathauto_punctuation_period
- pathauto_punctuation_pipe
- pathauto_punctuation_plus
- pathauto_punctuation_question_mark
- pathauto_punctuation_quotes
- pathauto_punctuation_right_curly
- pathauto_punctuation_right_parenthesis
- pathauto_punctuation_right_square
- pathauto_punctuation_semicolon
- pathauto_punctuation_slash
- pathauto_punctuation_tilde
- pathauto_punctuation_underscore
- pathauto_verbose
- pathauto_separator
- pathauto_max_length
- pathauto_max_component_length
- pathauto_transliterate
- pathauto_reduce_ascii
- pathauto_ignore_words
- pathauto_case
- pathauto_update_action
source_module: pathauto
process:
punctuation/ampersand: pathauto_punctuation_ampersand
punctuation/asterisk: pathauto_punctuation_asterisk
punctuation/at: pathauto_punctuation_at
punctuation/backtick: pathauto_punctuation_backtick
punctuation/back_slash: pathauto_punctuation_back_slash
punctuation/caret: pathauto_punctuation_caret
punctuation/colon: pathauto_punctuation_colon
punctuation/comma: pathauto_punctuation_comma
punctuation/dollar: pathauto_punctuation_dollar
punctuation/double_quotes: pathauto_punctuation_double_quotes
punctuation/equal: pathauto_punctuation_equal
punctuation/exclamation: pathauto_punctuation_exclamation
punctuation/greater_than: pathauto_punctuation_greater_than
punctuation/hash: pathauto_punctuation_hash
punctuation/hyphen: pathauto_punctuation_hyphen
punctuation/left_curly: pathauto_punctuation_left_curly
punctuation/left_parenthesis: pathauto_punctuation_left_parenthesis
punctuation/left_square: pathauto_punctuation_left_square
punctuation/less_than: pathauto_punctuation_less_than
punctuation/percent: pathauto_punctuation_percent
punctuation/period: pathauto_punctuation_period
punctuation/pipe: pathauto_punctuation_pipe
punctuation/plus: pathauto_punctuation_plus
punctuation/question_mark: pathauto_punctuation_question_mark
punctuation/quotes: pathauto_punctuation_quotes
punctuation/right_curly: pathauto_punctuation_right_curly
punctuation/right_parenthesis: pathauto_punctuation_right_parenthesis
punctuation/right_square: pathauto_punctuation_right_square
punctuation/semicolon: pathauto_punctuation_semicolon
punctuation/slash: pathauto_punctuation_slash
punctuation/tilde: pathauto_punctuation_tilde
punctuation/underscore: pathauto_punctuation_underscore
verbose: pathauto_verbose
separator: pathauto_separator
max_length: pathauto_max_length
max_component_length: pathauto_max_component_length
transliterate: pathauto_transliterate
reduce_ascii: pathauto_reduce_ascii
ignore_words: pathauto_ignore_words
case: pathauto_case
update_action: pathauto_update_action
destination:
plugin: config
config_name: pathauto.settings

View File

@@ -0,0 +1,159 @@
<?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 \Drupal\pathauto\PathautoPatternInterface $pattern
* The Pathauto pattern to be used.
* @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().
* - 'bundle': 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(\Drupal\pathauto\PathautoPatternInterface $pattern, array $context) {
// Switch out any [node:created:*] tokens with [node:updated:*] on update.
if ($context['module'] == 'node' && ($context['op'] == 'update')) {
$pattern->setPattern(preg_replace('/\[node:created(\:[^]]*)?\]/', '[node:updated$1]', $pattern->getPattern()));
}
}
/**
* 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 array $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'] = ['value' => '™', 'name' => t('Trademark symbol')];
// Remove the dollar sign.
unset($punctuation['dollar']);
}

View File

@@ -0,0 +1,18 @@
name : 'Pathauto'
description : 'Provides a mechanism for modules to automatically generate aliases for the content they manage.'
core_version_requirement: ^9.3 || ^10 || ^11
type: module
dependencies:
- drupal:path
- token:token
configure: entity.pathauto_pattern.collection
recommends:
- redirect:redirect
# Information added by Drupal.org packaging script on 2024-05-18
version: '8.x-1.12+6-dev'
project: 'pathauto'
datestamp: 1716065615

View File

@@ -0,0 +1,322 @@
<?php
/**
* @file
* Install, update, and uninstall functions for Pathauto.
*
* @ingroup pathauto
*/
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\pathauto\Entity\PathautoPattern;
/**
* Implements hook_install().
*/
function pathauto_install() {
// Set the weight to 1.
module_set_weight('pathauto', 1);
}
/**
* 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 = [];
/** @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', [
'@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' => [$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) {
[, $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', [
'@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' => [$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() {
// This update function is not needed anymore.
}
/**
* 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();
}
/**
* Initialize the new safe tokens setting.
*/
function pathauto_update_8107() {
$safe_tokens = [
'alias',
'alias',
'path',
'join-path',
'login-url',
'url',
'url-brief',
];
\Drupal::configFactory()->getEditable('pathauto.settings')
->set('safe_tokens', $safe_tokens)
->save();
}
/**
* Update node type conditions from node_type to entity_bundle.
*/
function pathauto_update_8108() {
// 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 node_type plugin by the entity_bundle:node
// 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'] === 'node_type') {
$pattern_config->set("selection_criteria.$uuid.id", 'entity_bundle:node');
$pattern_config->save();
break;
}
}
}
}
}

View File

@@ -0,0 +1,21 @@
(function ($) {
'use strict';
Drupal.behaviors.pathFieldsetSummaries = {
attach: function (context) {
$(context).find('.path-form').drupalSetSummary(function (context) {
var path = $('.js-form-item-path-0-alias input', context).val();
var automatic = $('.js-form-item-path-0-pathauto input', context).prop('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,5 @@
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: entity.path_alias.collection
title: 'Patterns'
weight: 10
pathauto.settings.form:
route_name: pathauto.settings.form
base_route: entity.path_alias.collection
title: 'Settings'
weight: 20
pathauto.bulk.update.form:
route_name: pathauto.bulk.update.form
base_route: entity.path_alias.collection
title: 'Bulk generate'
weight: 30
pathauto.admin.delete:
route_name: pathauto.admin.delete
base_route: entity.path_alias.collection
title: 'Delete aliases'
weight: 40

View File

@@ -0,0 +1,180 @@
<?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\PathautoFieldItemList;
use Drupal\pathauto\PathautoItem;
/**
* Implements hook_hook_info().
*/
function pathauto_hook_info() {
$hooks = [
'pathauto_pattern_alter',
'pathauto_alias_alter',
'pathauto_is_alias_reserved',
];
return array_fill_keys($hooks, ['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('entity.path_alias.collection')->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 path_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.', ['@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_delete().
*/
function pathauto_entity_delete(EntityInterface $entity) {
if ($entity->hasLinkTemplate('canonical') && $entity instanceof ContentEntityInterface && $entity->hasField('path') && $entity->getFieldDefinition('path')->getType() == 'path') {
\Drupal::service('pathauto.alias_storage_helper')->deleteEntityPathAll($entity);
$entity->get('path')->first()->get('pathauto')->purge();
}
}
/**
* Implements hook_field_info_alter().
*/
function pathauto_field_info_alter(&$info) {
$info['path']['class'] = PathautoItem::class;
$info['path']['list_class'] = PathautoFieldItemList::class;
}
/**
* 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', [
'type' => 'path',
'weight' => 30,
])
->setDisplayConfigurable('form', TRUE);
return $fields;
}
}
}
/**
* 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.', [
'%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.", ['%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,63 @@
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'
entity.pathauto_pattern.duplicate_form:
path: '/admin/config/search/path/patterns/{pathauto_pattern}/duplicate'
defaults:
_entity_form: 'pathauto_pattern.duplicate'
_title: 'Duplicate 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', '@plugin.manager.alias_type']
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.repository', '@database','@pathauto.verbose_messenger', '@string_translation', '@entity_type.manager']
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', '@messenger']
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 = [];
$info['tokens']['array']['join-path'] = [
'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 = [];
if ($type == 'array' && !empty($data['array'])) {
$array = $data['array'];
foreach ($tokens as $name => $original) {
switch ($name) {
case 'join-path':
$values = [];
foreach (token_element_children($array) as $key) {
$value = is_array($array[$key]) ? \Drupal::service('renderer')->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,367 @@
<?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;
use Drupal\Core\StringTranslation\StringTranslationTrait;
/**
* Provides an alias cleaner.
*/
class AliasCleaner implements AliasCleanerInterface {
use StringTranslationTrait;
/**
* 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.
*
* @var array
*
* @todo Split this up into separate properties.
*/
protected $cleanStringCache = [];
/**
* Transliteration service.
*
* @var \Drupal\Component\Transliteration\TransliterationInterface
*/
protected $transliteration;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* An array of arrays for punctuation values.
*
* @var array
*/
protected $punctuationCharacters = [];
/**
* 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 backslash.
$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 = []) {
if (empty($this->cleanStringCache)) {
// Generate and cache variables used in this method.
$config = $this->configFactory->get('pathauto.settings');
$this->cleanStringCache = [
'separator' => $config->get('separator'),
'strings' => [],
'transliterate' => $config->get('transliterate'),
'punctuation' => [],
'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(['/^[,\s]+|[,\s]+$/', '/[,\s]+/'], ['', '\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 = 'en';
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);
// Replace or drop punctuation based on user settings.
$output = strtr($output, $this->cleanStringCache['punctuation']);
// 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 again as the transliteration process can
// convert special characters to punctuation.
$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 (mb_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 = mb_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 = [];
$punctuation['double_quotes'] = ['value' => '"', 'name' => $this->t('Double quotation marks')];
$punctuation['quotes'] = ['value' => '\'', 'name' => $this->t("Single quotation marks (apostrophe)")];
$punctuation['backtick'] = ['value' => '`', 'name' => $this->t('Back tick')];
$punctuation['comma'] = ['value' => ',', 'name' => $this->t('Comma')];
$punctuation['period'] = ['value' => '.', 'name' => $this->t('Period')];
$punctuation['hyphen'] = ['value' => '-', 'name' => $this->t('Hyphen')];
$punctuation['underscore'] = ['value' => '_', 'name' => $this->t('Underscore')];
$punctuation['colon'] = ['value' => ':', 'name' => $this->t('Colon')];
$punctuation['semicolon'] = ['value' => ';', 'name' => $this->t('Semicolon')];
$punctuation['pipe'] = ['value' => '|', 'name' => $this->t('Vertical bar (pipe)')];
$punctuation['left_curly'] = ['value' => '{', 'name' => $this->t('Left curly bracket')];
$punctuation['left_square'] = ['value' => '[', 'name' => $this->t('Left square bracket')];
$punctuation['right_curly'] = ['value' => '}', 'name' => $this->t('Right curly bracket')];
$punctuation['right_square'] = ['value' => ']', 'name' => $this->t('Right square bracket')];
$punctuation['plus'] = ['value' => '+', 'name' => $this->t('Plus sign')];
$punctuation['equal'] = ['value' => '=', 'name' => $this->t('Equal sign')];
$punctuation['asterisk'] = ['value' => '*', 'name' => $this->t('Asterisk')];
$punctuation['ampersand'] = ['value' => '&', 'name' => $this->t('Ampersand')];
$punctuation['percent'] = ['value' => '%', 'name' => $this->t('Percent sign')];
$punctuation['caret'] = ['value' => '^', 'name' => $this->t('Caret')];
$punctuation['dollar'] = ['value' => '$', 'name' => $this->t('Dollar sign')];
$punctuation['hash'] = ['value' => '#', 'name' => $this->t('Number sign (pound sign, hash)')];
$punctuation['at'] = ['value' => '@', 'name' => $this->t('At sign')];
$punctuation['exclamation'] = ['value' => '!', 'name' => $this->t('Exclamation mark')];
$punctuation['tilde'] = ['value' => '~', 'name' => $this->t('Tilde')];
$punctuation['left_parenthesis'] = ['value' => '(', 'name' => $this->t('Left parenthesis')];
$punctuation['right_parenthesis'] = ['value' => ')', 'name' => $this->t('Right parenthesis')];
$punctuation['question_mark'] = ['value' => '?', 'name' => $this->t('Question mark')];
$punctuation['less_than'] = ['value' => '<', 'name' => $this->t('Less-than sign')];
$punctuation['greater_than'] = ['value' => '>', 'name' => $this->t('Greater-than sign')];
$punctuation['slash'] = ['value' => '/', 'name' => $this->t('Slash')];
$punctuation['back_slash'] = ['value' => '\\', 'name' => $this->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 = [], $options = []) {
foreach ($replacements as $token => $value) {
// Only clean non-path tokens.
$config = $this->configFactory->get('pathauto.settings');
$safe_tokens = implode('|', (array) $config->get('safe_tokens'));
if (!preg_match('/(\[|\:)(' . $safe_tokens . ')(:|\]$)/', $token)) {
$replacements[$token] = $this->cleanString($value, $options);
}
}
}
/**
* {@inheritdoc}
*/
public function resetCaches() {
$this->cleanStringCache = [];
}
}

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 = []);
/**
* 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 = [], $options = []);
/**
* Resets internal caches.
*/
public function resetCaches();
}

View File

@@ -0,0 +1,248 @@
<?php
namespace Drupal\pathauto;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Database\Connection;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\path_alias\AliasRepositoryInterface;
/**
* 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 repository.
*
* @var \Drupal\path_alias\AliasRepositoryInterface
*/
protected $aliasRepository;
/**
* The database connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected $database;
/**
* The messenger.
*
* @var \Drupal\pathauto\MessengerInterface
*/
protected $messenger;
/**
* The entity type manager service.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The config factory.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
* @param \Drupal\path_alias\AliasRepositoryInterface $alias_repository
* The alias repository.
* @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.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manger.
*/
public function __construct(ConfigFactoryInterface $config_factory, AliasRepositoryInterface $alias_repository, Connection $database, MessengerInterface $messenger, TranslationInterface $string_translation, EntityTypeManagerInterface $entity_type_manager = NULL) {
$this->configFactory = $config_factory;
$this->aliasRepository = $alias_repository;
$this->database = $database;
$this->messenger = $messenger;
$this->stringTranslation = $string_translation;
$this->entityTypeManager = $entity_type_manager ?: \Drupal::service('entity_type.manager');
}
/**
* {@inheritdoc}
*/
public function getAliasSchemaMaxLength() {
return $this->aliasSchemaMaxLength;
}
/**
* {@inheritdoc}
*/
public function save(array $path, $existing_alias = NULL, $op = NULL) {
$config = $this->configFactory->get('pathauto.settings');
// Set up all the variables needed to simplify the code below.
$source = $path['source'];
$alias = $path['alias'];
$langcode = $path['language'];
if ($existing_alias) {
/** @var \Drupal\path_alias\PathAliasInterface $existing_alias */
$existing_alias = $this->entityTypeManager->getStorage('path_alias')->load($existing_alias['pid']);
}
// Alert users if they are trying to create an alias that is the same as the
// internal system path.
if ($source == $alias) {
$this->messenger->addMessage($this->t('Ignoring alias %alias because it is the same as the internal path.', ['%alias' => $alias]));
return NULL;
}
// Don't create a new alias if it is identical to the current alias.
if ($existing_alias && $existing_alias->getAlias() == $alias) {
return NULL;
}
// Update the existing alias if there is one and the configuration is set to
// replace it.
if ($existing_alias && $config->get('update_action') == PathautoGeneratorInterface::UPDATE_ACTION_DELETE) {
$old_alias = $existing_alias->getAlias();
$existing_alias->setAlias($alias)->save();
$this->messenger->addMessage($this->t('Created new alias %alias for %source, replacing %old_alias.', [
'%alias' => $alias,
'%source' => $source,
'%old_alias' => $old_alias,
]));
$return = $existing_alias;
}
else {
// Otherwise, create a new alias.
$path_alias = $this->entityTypeManager->getStorage('path_alias')->create([
'path' => $source,
'alias' => $alias,
'langcode' => $langcode,
]);
$path_alias->save();
$this->messenger->addMessage($this->t('Created new alias %alias for %source.', [
'%alias' => $path_alias->getAlias(),
'%source' => $path_alias->getPath(),
]));
$return = $path_alias;
}
return [
'source' => $return->getPath(),
'alias' => $return->getAlias(),
'pid' => $return->id(),
'langcode' => $return->language()->getId(),
];
}
/**
* {@inheritdoc}
*/
public function loadBySource($source, $language = LanguageInterface::LANGCODE_NOT_SPECIFIED) {
$alias = $this->aliasRepository->lookupBySystemPath($source, $language);
if ($alias) {
return [
'pid' => $alias['id'],
'alias' => $alias['alias'],
'source' => $alias['path'],
'langcode' => $alias['langcode'],
];
}
}
/**
* {@inheritdoc}
*/
public function deleteBySourcePrefix($source) {
$pids = $this->loadBySourcePrefix($source);
if ($pids) {
$this->deleteMultiple($pids);
}
}
/**
* {@inheritdoc}
*/
public function deleteAll() {
/** @var \Drupal\Core\Entity\Sql\TableMappingInterface $table_mapping */
$table_mapping = $this->entityTypeManager->getStorage('path_alias')->getTableMapping();
foreach ($table_mapping->getTableNames() as $table_name) {
$this->database->truncate($table_name)->execute();
}
$this->entityTypeManager->getStorage('path_alias')->resetCache();
}
/**
* {@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) {
return $this->entityTypeManager->getStorage('path_alias')->getQuery('OR')
->condition('path', $source, '=')
->condition('path', rtrim($source, '/') . '/', 'STARTS_WITH')
->accessCheck(FALSE)
->execute();
}
/**
* {@inheritdoc}
*/
public function countBySourcePrefix($source) {
return $this->entityTypeManager->getStorage('path_alias')->getQuery('OR')
->condition('path', $source, '=')
->condition('path', rtrim($source, '/') . '/', 'STARTS_WITH')
->accessCheck(FALSE)
->count()
->execute();
}
/**
* {@inheritdoc}
*/
public function countAll() {
return $this->entityTypeManager->getStorage('path_alias')->getQuery()
->accessCheck(FALSE)
->count()
->execute();
}
/**
* {@inheritdoc}
*/
public function deleteMultiple($pids) {
$this->entityTypeManager->getStorage('path_alias')->delete($this->entityTypeManager->getStorage('path_alias')->loadMultiple($pids));
}
}

View File

@@ -0,0 +1,132 @@
<?php
namespace Drupal\pathauto;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Language\LanguageInterface;
/**
* Provides helper methods for accessing alias storage.
*
* @internal This interface is internal to pathauto, it abstracts direct alias
* storage access as well as backwards-compatibility for 8.7 and will be
* refactored.
*/
interface AliasStorageHelperInterface {
/**
* Fetch the maximum length of the {path_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 path alias entity tables).
*/
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();
/**
* 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);
}

View File

@@ -0,0 +1,32 @@
<?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\Component\Plugin\FallbackPluginManagerInterface;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
/**
* 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 = []) {
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,181 @@
<?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\Routing\RouteProviderInterface;
use Drupal\path_alias\AliasManagerInterface;
/**
* 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\path_alias\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.
* @param \Drupal\path_alias\AliasManagerInterface $alias_manager
* The alias manager.
*/
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 - mb_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 = [
$alias,
$source,
$langcode,
];
if (method_exists($this->moduleHandler, 'invokeAllWith')) {
$implementations = [];
$this->moduleHandler->invokeAllWith(
'pathauto_is_alias_reserved',
function (callable $hook, string $module) use (&$implementations) {
$implementations[] = $module;
}
);
}
else {
// Use the deprecated getImplementations() for Drupal < 9.4.
$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,43 @@
<?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.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $label;
/**
* The token types.
*
* @var string[]
*/
public $types = [];
}

View File

@@ -0,0 +1,300 @@
<?php
namespace Drupal\pathauto\Commands;
use Consolidation\AnnotatedCommand\CommandData;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\pathauto\AliasStorageHelperInterface;
use Drupal\pathauto\AliasTypeBatchUpdateInterface;
use Drupal\pathauto\AliasTypeManager;
use Drupal\pathauto\Form\PathautoBulkUpdateForm;
use Drupal\pathauto\PathautoGeneratorInterface;
use Drush\Commands\DrushCommands;
use Symfony\Component\Console\Input\Input;
use Symfony\Component\Console\Output\Output;
use Symfony\Component\Console\Question\ChoiceQuestion;
/**
* Drush commands allowing to perform Pathauto tasks from the command line.
*/
class PathautoCommands extends DrushCommands {
/**
* The argument option for generating URL aliases of all possible types.
*/
const TYPE_ALL = 'all';
/**
* The configuration object factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* The alias type manager.
*
* @var \Drupal\pathauto\AliasTypeManager
*/
protected $aliasTypeManager;
/**
* The alias storage helper.
*
* @var \Drupal\pathauto\AliasStorageHelperInterface
*/
protected $aliasStorageHelper;
/**
* Constructs a new PathautoCommands object.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
* The configuration object factory.
* @param \Drupal\pathauto\AliasTypeManager $aliasTypeManager
* The alias type manager.
* @param \Drupal\pathauto\AliasStorageHelperInterface $aliasStorageHelper
* The alias storage helper.
*/
public function __construct(ConfigFactoryInterface $configFactory, AliasTypeManager $aliasTypeManager, AliasStorageHelperInterface $aliasStorageHelper) {
$this->configFactory = $configFactory;
$this->aliasTypeManager = $aliasTypeManager;
$this->aliasStorageHelper = $aliasStorageHelper;
}
/**
* (Re)generate URL aliases.
*
* @command pathauto:aliases-generate
*
* @param string $action
* The action to take. Possible actions are "create" (generate aliases for
* un-aliased paths only), "update" (update aliases for paths that have an
* existing alias) or "all" (generate aliases for all paths).
* @param array $types
* Comma-separated list of aliase typess to generate. Pass "all" to generate
* aliases for all types.
*
* @throws \Exception
*
* @usage drush pathauto:aliases-generate all all
* Generate all URL aliases.
* @usage drush pathauto:aliases-generate create canonical_entities:node
* Generate URL aliases for un-aliased node paths only.
* @usage drush pathauto:aliases-generate
* When the arguments are omitted they can be chosen from an interactive
* menu.
*
* @aliases pag
*/
public function generateAliases($action = NULL, array $types = NULL) {
$batch = [
'title' => dt('Bulk updating URL aliases'),
'operations' => [
['Drupal\pathauto\Form\PathautoBulkUpdateForm::batchStart', []],
],
'finished' => 'Drupal\pathauto\Form\PathautoBulkUpdateForm::batchFinished',
'progressive' => FALSE,
];
foreach ($types as $type) {
$batch['operations'][] = ['Drupal\pathauto\Form\PathautoBulkUpdateForm::batchProcess', [$type, $action]];
}
batch_set($batch);
drush_backend_batch_process();
}
/**
* Delete URL aliases
*
* @command pathauto:aliases-delete
*
* @param array $types
* Comma-separated list of alias types to delete. Pass "all" to delete
* aliases for all types.
*
* @option purge
* Deletes all URL aliases, including manually created ones.
*
* @throws \Exception
*
* @usage drush pathauto:aliases-delete canonical_entities:node
* Delete all automatically generated URL aliases for node entities,
* preserving manually created aliases.
* @usage drush pathauto:aliases-delete all
* Delete all automatically generated URL aliases, preserving manually
* created ones.
* @usage drush pathauto:aliases-delete all --purge
* Delete all URL aliases, including manually created ones.
* @usage drush pathauto:aliases-delete
* When the alias types are omitted they can be chosen from an interactive
* menu.
*
* @aliases pad
*/
public function deleteAliases(array $types = NULL, $options = ['purge' => FALSE]) {
$delete_all = count($types) === count($this->getAliasTypes());
// Keeping custom aliases forces us to go the slow way to correctly check
// the automatic/manual flag.
if (!$options['purge']) {
$batch = [
'title' => dt('Bulk deleting URL aliases'),
'operations' => [['Drupal\pathauto\Form\PathautoAdminDelete::batchStart', [$delete_all]]],
'finished' => 'Drupal\pathauto\Form\PathautoAdminDelete::batchFinished',
];
foreach ($types as $type) {
$batch['operations'][] = ['Drupal\pathauto\Form\PathautoAdminDelete::batchProcess', [$type]];
}
batch_set($batch);
drush_backend_batch_process();
}
elseif ($delete_all) {
$this->aliasStorageHelper->deleteAll();
$this->logger()->success(dt('All of your path aliases have been deleted.'));
}
else {
foreach ($types as $type) {
/** @var \Drupal\pathauto\AliasTypeInterface $alias_type */
$alias_type = $this->aliasTypeManager->createInstance($type);
$this->aliasStorageHelper->deleteBySourcePrefix($alias_type->getSourcePrefix());
$this->logger()->success(dt('All of your %label path aliases have been deleted.', [
'%label' => $alias_type->getLabel(),
]));
}
}
}
/**
* @hook interact pathauto:aliases-generate
*
* @throws \Drush\Exceptions\UserAbortException
* Thrown when the user cancels the operation during CLI interaction.
*/
public function interactGenerateAliases(Input $input, Output $output) {
if (!$input->getArgument('action')) {
$action = $this->io()->choice(dt('Choose the action to perform.'), $this->getAllowedGenerateActions());
$input->setArgument('action', $action);
}
}
/**
* @hook interact
*/
public function interactAliasTypes(Input $input, Output $output) {
if (!$input->getArgument('types')) {
$available_types = $this->getAliasTypes();
$question = new ChoiceQuestion(dt('Choose the alias type(s). Separate multiple choices with commas, e.g. "1,2,4", or choose "0" for all types.'), array_merge([static::TYPE_ALL], $available_types), NULL);
$question->setMultiselect(TRUE);
$types = $this->io()->askQuestion($question);
$input->setArgument('types', implode(',', $types));
}
}
/**
* @hook validate pathauto:aliases-generate
*
* @throws \InvalidArgumentException
* Thrown when one of the passed arguments is invalid
*/
public function validateGenerateAliases(CommandData $commandData) {
$input = $commandData->input();
$action = $input->getArgument('action');
$valid_actions = array_keys($this->getAllowedGenerateActions());
if (!in_array($action, $valid_actions)) {
$message = dt('Invalid action argument "@invalid_action". Please use one of: @valid_actions', [
'@invalid_action' => $action,
'@valid_actions' => '"' . implode('", "', $valid_actions) . '"',
]);
throw new \InvalidArgumentException($message);
}
}
/**
* @hook validate
*
* @throws \InvalidArgumentException
* Thrown when one of the passed arguments is invalid
* @throws \Drupal\Component\Plugin\Exception\PluginException
* Thrown when an alias type can not be instantiated.
*/
public function validateAliaseTypes(CommandData $commandData) {
$input = $commandData->input();
// Convert the comma-separated list of types to an array with no duplicates.
$types = explode(',', $input->getArgument('types') ?? '');
$types = array_map('trim', $types);
sort($types);
$types = array_unique($types);
// Set all available types if the user chooses this option.
if (in_array(static::TYPE_ALL, $types)) {
$types = $this->getAliasTypes();
}
// Check for invalid types.
$available_types = $this->getAliasTypes();
$unsupported_types = array_diff($types, $available_types);
if (!empty($unsupported_types)) {
$message = dt('Invalid type argument "@invalid_types". Please choose from the following: @valid_types', [
'@invalid_types' => '"' . implode('", "', $unsupported_types) . '"',
'@valid_types' => '"' . implode('", "', [static::TYPE_ALL] + $available_types) . '"',
]);
throw new \InvalidArgumentException($message);
}
// Pass the array of types to the command, rather than the comma-separated
// string.
$input->setArgument('types', $types);
}
/**
* Returns the allowed actions according to the site configuration.
*
* @return array
* An associative array of allowed option descriptions, keyed by option
* name.
*/
protected function getAllowedGenerateActions() {
$actions = [
PathautoBulkUpdateForm::ACTION_CREATE => dt('Generate a URL alias for un-aliased paths only.'),
];
// The options that affect existing URL aliases are allowed unless the
// site is configured to preserve existing aliases.
$config = $this->configFactory->get('pathauto.settings');
if ($config->get('update_action') !== PathautoGeneratorInterface::UPDATE_ACTION_NO_NEW) {
$actions[PathautoBulkUpdateForm::ACTION_UPDATE] = dt('Update the URL alias for paths having an old URL alias.');
$actions[PathautoBulkUpdateForm::ACTION_ALL] = dt('Regenerate URL aliases for all paths.');
}
return $actions;
}
/**
* Returns the available alias types for which aliases can be generated.
*
* @return array
* An indexed array of alias types.
*
* @throws \Drupal\Component\Plugin\Exception\PluginException
* Thrown when an alias type can not be instantiated.
*/
public function getAliasTypes() {
$types = [];
foreach ($this->aliasTypeManager->getVisibleDefinitions() as $id => $definition) {
/** @var \Drupal\pathauto\AliasTypeInterface $aliasType */
$aliasType = $this->aliasTypeManager->createInstance($id);
if ($aliasType instanceof AliasTypeBatchUpdateInterface) {
$types[] = $aliasType->getPluginId();
}
}
return $types;
}
}

View File

@@ -0,0 +1,528 @@
<?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\Context\Context;
use Drupal\Core\Plugin\Context\ContextDefinition;
use Drupal\Core\Plugin\Context\ContextInterface;
use Drupal\Core\Plugin\Context\EntityContextDefinition;
use Drupal\Core\Plugin\ContextAwarePluginInterface;
use Drupal\Core\Plugin\DefaultSingleLazyPluginCollection;
use Drupal\Core\TypedData\DataReferenceDefinitionInterface;
use Drupal\Core\TypedData\DataReferenceInterface;
use Drupal\Core\TypedData\ListDataDefinitionInterface;
use Drupal\Core\TypedData\ListInterface;
use Drupal\Core\Utility\Error;
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",
* "duplicate" = "Drupal\pathauto\Form\PatternDuplicateForm",
* "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",
* "duplicate-form" = "/admin/config/search/path/patterns/{pathauto_pattern}/duplicate"
* }
* )
*/
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) {
$context = $this->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) {
// As the context object is kept and only the value is switched out,
// it can over time grow to a huge number of cache contexts. Reset it
// if there are 100 cache tags to prevent cache tag merging getting too
// slow.
foreach ($condition->getContextDefinitions() as $name => $context_definition) {
if (count($condition->getContext($name)->getCacheTags()) > 100) {
$condition->setContext($name, new Context($context_definition));
}
}
if ($condition instanceof ContextAwarePluginInterface) {
try {
$context_handler->applyContextMapping($condition, $contexts);
}
catch (ContextException $e) {
if (method_exists(Error::class, 'logException')) {
Error::logException(\Drupal::logger('pathauto'), $e);
}
else {
/* @phpstan-ignore-next-line */
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;
}
/**
* Extracts a context from an array of contexts by a tokenized pattern.
*
* This is more than simple isset/empty checks on the contexts array. The
* pattern could be node:uid:name which will iterate over all provided
* contexts in the array for one named 'node', it will then load the data
* definition of 'node' and check for a property named 'uid'. This will then
* set a new (temporary) context on the array and recursively call itself to
* navigate through related properties all the way down until the request
* property is located. At that point the property is passed to a
* TypedDataResolver which will convert it to an appropriate ContextInterface
* object.
*
* @param string $token
* A ":" delimited set of tokens representing
* @param \Drupal\Core\Plugin\Context\ContextInterface[] $contexts
* The array of available contexts.
*
* @return \Drupal\Core\Plugin\Context\ContextInterface
* The requested token as a full Context object.
*
* @throws \Exception
*/
public function convertTokenToContext(string $token, array $contexts) {
// If the requested token is already a context, just return it.
if (isset($contexts[$token])) {
return $contexts[$token];
}
else {
[$base, $property_path] = explode(':', $token, 2);
// A base must always be set. This method recursively calls itself
// setting bases for this reason.
if (!empty($contexts[$base])) {
return $this->getContextFromProperty($property_path, $contexts[$base]);
}
// @todo improve this exception message.
throw new ContextNotFoundException("The requested context was not found in the supplied array of contexts.");
}
}
/**
* Convert a property to a context.
*
* This method will respect the value of contexts as well, so if a context
* object is pass that contains a value, the appropriate value will be
* extracted and injected into the resulting context object if available.
*
* @param string $property_path
* The name of the property.
* @param \Drupal\Core\Plugin\Context\ContextInterface $context
* The context from which we will extract values if available.
*
* @return \Drupal\Core\Plugin\Context\Context
* A context object that represents the definition & value of the property.
*
* @throws \Exception
*/
public function getContextFromProperty($property_path, ContextInterface $context) {
$value = NULL;
$data_definition = NULL;
if ($context->hasContextValue()) {
/** @var \Drupal\Core\TypedData\ComplexDataInterface $data */
$data = $context->getContextData();
foreach (explode(':', $property_path) as $name) {
if ($data instanceof ListInterface) {
if (!is_numeric($name)) {
// Implicitly default to delta 0 for lists when not specified.
$data = $data->first();
}
else {
// If we have a delta, fetch it and continue with the next part.
$data = $data->get($name);
continue;
}
}
// Forward to the target value if this is a data reference.
if ($data instanceof DataReferenceInterface) {
$data = $data->getTarget();
}
if (!$data->getDataDefinition()->getPropertyDefinition($name)) {
throw new \Exception("Unknown property $name in property path $property_path");
}
$data = $data->get($name);
}
$value = $data->getValue();
$data_definition = $data instanceof DataReferenceInterface ? $data->getDataDefinition()->getTargetDefinition() : $data->getDataDefinition();
}
else {
/** @var \Drupal\Core\TypedData\ComplexDataDefinitionInterface $data_definition */
$data_definition = $context->getContextDefinition()->getDataDefinition();
foreach (explode(':', $property_path) as $name) {
if ($data_definition instanceof ListDataDefinitionInterface) {
$data_definition = $data_definition->getItemDefinition();
// If the delta was specified explicitly, continue with the next part.
if (is_numeric($name)) {
continue;
}
}
// Forward to the target definition if this is a data reference
// definition.
if ($data_definition instanceof DataReferenceDefinitionInterface) {
$data_definition = $data_definition->getTargetDefinition();
}
if (!$data_definition->getPropertyDefinition($name)) {
throw new \Exception("Unknown property $name in property path $property_path");
}
$data_definition = $data_definition->getPropertyDefinition($name);
}
// Forward to the target definition if this is a data reference
// definition.
if ($data_definition instanceof DataReferenceDefinitionInterface) {
$data_definition = $data_definition->getTargetDefinition();
}
}
if (strpos($data_definition->getDataType(), 'entity:') === 0) {
$context_definition = new EntityContextDefinition($data_definition->getDataType(), $data_definition->getLabel(), $data_definition->isRequired(), FALSE, $data_definition->getDescription());
}
else {
$context_definition = new ContextDefinition($data_definition->getDataType(), $data_definition->getLabel(), $data_definition->isRequired(), FALSE, $data_definition->getDescription());
}
return new Context($context_definition, $value);
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace Drupal\pathauto\EventSubscriber;
use Drupal\Core\Config\ConfigCrudEvent;
use Drupal\Core\Config\ConfigEvents;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\pathauto\AliasTypeManager;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* A subscriber to clear fielddefinition cache when saving pathauto settings.
*/
class PathautoSettingsCacheTag implements EventSubscriberInterface {
/**
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
*/
protected $entityFieldManager;
/**
* The alias type manager.
*
* @var \Drupal\pathauto\AliasTypeManager
*/
protected $aliasTypeManager;
/**
* Constructs a PathautoSettingsCacheTag object.
*
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
* The entity field manager.
* @param \Drupal\pathauto\AliasTypeManager $alias_type_manager
* The alias type manager.
*/
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(): array {
$events[ConfigEvents::SAVE][] = ['onSave'];
return $events;
}
}

View File

@@ -0,0 +1,206 @@
<?php
namespace Drupal\pathauto\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\pathauto\AliasStorageHelperInterface;
use Drupal\pathauto\AliasTypeManager;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Alias mass delete form.
*/
class PathautoAdminDelete extends FormBase {
/**
* Provides helper methods for accessing alias storage.
*
* @var \Drupal\pathauto\AliasStorageHelperInterface
*/
protected $aliasStorageHelper;
/**
* The alias type manager.
*
* @var \Drupal\pathauto\AliasTypeManager
*/
protected $aliasTypeManager;
/**
* Constructs a PathautoAdminDelete object.
*
* @param \Drupal\pathauto\AliasStorageHelperInterface $alias_storage_helper
* Provides helper methods for accessing alias storage.
* @param \Drupal\pathauto\AliasTypeManager $alias_type_manager
* The alias type manager.
*/
public function __construct(AliasStorageHelperInterface $alias_storage_helper, AliasTypeManager $alias_type_manager) {
$this->aliasStorageHelper = $alias_storage_helper;
$this->aliasTypeManager = $alias_type_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('pathauto.alias_storage_helper'),
$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.
$total_count = $this->aliasStorageHelper->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 = $this->aliasStorageHelper->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'] = ['#markup' => '<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 path_alias and path_alias_revision tables 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);
}
elseif ($delete_all) {
$this->aliasStorageHelper->deleteAll();
$this->messenger()->addMessage($this->t('All of your path aliases have been deleted.'));
}
else {
foreach (array_keys(array_filter($form_state->getValue(['delete', 'plugins']))) as $id) {
$alias_type = $this->aliasTypeManager->createInstance($id);
$this->aliasStorageHelper->deleteBySourcePrefix((string) $alias_type->getSourcePrefix());
$this->messenger()->addMessage($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::service('messenger')
->addMessage(t('All of your automatically generated path aliases have been deleted.'));
}
elseif (isset($results['deletions'])) {
foreach (array_values($results['deletions']) as $label) {
\Drupal::service('messenger')
->addMessage(t('All of your automatically generated %label path aliases have been deleted.', [
'%label' => $label,
]));
}
}
}
else {
$error_operation = reset($operations);
\Drupal::service('messenger')
->addMessage(t('An error occurred while processing @operation with arguments : @args', [
'@operation' => $error_operation[0],
'@args' => print_r($error_operation[0]),
]));
}
}
}

View File

@@ -0,0 +1,185 @@
<?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 {
/**
* Generate URL aliases for un-aliased paths only.
*/
const ACTION_CREATE = 'create';
/**
* Update URL aliases for paths that have an existing alias.
*/
const ACTION_UPDATE = 'update';
/**
* Regenerate URL aliases for all paths.
*/
const ACTION_ALL = 'all';
/**
* 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 = [];
$form['#update_callbacks'] = [];
$form['update'] = [
'#type' => 'checkboxes',
'#title' => $this->t('Select the types of paths for which to generate URL aliases'),
'#options' => [],
'#default_value' => [],
];
$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'] = [
'#type' => 'radios',
'#title' => $this->t('Select which URL aliases to generate'),
'#options' => [static::ACTION_CREATE => $this->t('Generate a URL alias for un-aliased paths only')],
'#default_value' => static::ACTION_CREATE,
];
$config = $this->config('pathauto.settings');
if ($config->get('update_action') == PathautoGeneratorInterface::UPDATE_ACTION_NO_NEW) {
// Existing aliases should not be updated.
$form['warning'] = [
'#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'][static::ACTION_UPDATE] = $this->t('Update the URL alias for paths having an old URL alias');
$form['action']['#options'][static::ACTION_ALL] = $this->t('Regenerate URL aliases for all paths');
}
$form['actions']['#type'] = 'actions';
$form['actions']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Update'),
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$batch = [
'title' => $this->t('Bulk updating URL aliases'),
'operations' => [
['Drupal\pathauto\Form\PathautoBulkUpdateForm::batchStart', []],
],
'finished' => 'Drupal\pathauto\Form\PathautoBulkUpdateForm::batchFinished',
];
$action = $form_state->getValue('action');
foreach ($form_state->getValue('update') as $id) {
if (!empty($id)) {
$batch['operations'][] = ['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::service('messenger')->addMessage(\Drupal::translation()
->formatPlural($results['updates'], 'Generated 1 URL alias.', 'Generated @count URL aliases.'));
}
else {
\Drupal::service('messenger')
->addMessage(t('No new URL aliases to generate.'));
}
}
else {
$error_operation = reset($operations);
\Drupal::service('messenger')
->addMessage(t('An error occurred while processing @operation with arguments : @args'), [
'@operation' => $error_operation[0],
'@args' => print_r($error_operation[0]),
]);
}
}
}

View File

@@ -0,0 +1,296 @@
<?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\Extension\ModuleHandlerInterface;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Drupal\pathauto\AliasCleanerInterface;
use Drupal\pathauto\AliasStorageHelperInterface;
use Drupal\pathauto\AliasTypeManager;
use Drupal\pathauto\PathautoGeneratorInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Configure pathauto settings for this site.
*/
class PathautoSettingsForm extends ConfigFormBase {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The entity field manager.
*
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
*/
protected $entityFieldManager;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The alias cleaner.
*
* @var \Drupal\pathauto\AliasCleanerInterface
*/
protected $aliasCleaner;
/**
* Provides helper methods for accessing alias storage.
*
* @var \Drupal\pathauto\AliasStorageHelperInterface
*/
protected $aliasStorageHelper;
/**
* Manages pathauto alias type plugins.
*
* @var \Drupal\pathauto\AliasTypeManager
*/
protected $aliasTypeManager;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
$form = parent::create($container);
$form->entityTypeManager = $container->get('entity_type.manager');
$form->entityFieldManager = $container->get('entity_field.manager');
$form->moduleHandler = $container->get('module_handler');
$form->aliasCleaner = $container->get('pathauto.alias_cleaner');
$form->aliasStorageHelper = $container->get('pathauto.alias_storage_helper');
$form->aliasTypeManager = $container->get('plugin.manager.alias_type');
return $form;
}
/**
* {@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 = $this->entityTypeManager->getStorage('pathauto_pattern')->getQuery()
->condition('type', 'canonical_entities:' . $entity_type_id)
->accessCheck(TRUE)
->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'] = [
'#type' => 'checkbox',
'#title' => $this->t('Verbose'),
'#default_value' => $config->get('verbose'),
'#description' => $this->t('Display alias changes (except during bulk updates).'),
];
$form['separator'] = [
'#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'] = [
'#type' => 'checkbox',
'#title' => $this->t('Character case'),
'#default_value' => $config->get('case'),
'#description' => $this->t('Convert token values to lowercase.'),
];
$max_length = $this->aliasStorageHelper->getAliasSchemaMaxlength();
$help_link = '';
if ($this->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'] = [
'#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.', ['@max' => $max_length]) . $help_link,
];
$form['max_component_length'] = [
'#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 ($this->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.', [':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.', [':url' => 'http://drupal.org/project/redirect']);
}
$form['update_action'] = [
'#type' => 'radios',
'#title' => $this->t('Update action'),
'#default_value' => $config->get('update_action'),
'#options' => [
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'] = [
'#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'] = [
'#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'] = [
'#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.'),
];
$form['safe_tokens'] = [
'#type' => 'textarea',
'#title' => $this->t('Safe tokens'),
'#default_value' => implode(', ', $config->get('safe_tokens')),
'#description' => $this->t('List of tokens that are safe to use in alias patterns and do not need to be cleaned. For example urls, aliases, machine names. Separated with a comma.'),
];
$form['punctuation'] = [
'#type' => 'details',
'#title' => $this->t('Punctuation'),
'#open' => FALSE,
'#tree' => TRUE,
];
$punctuation = $this->aliasCleaner->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] = [
'#type' => 'select',
'#title' => $details['name'] . ' (<code>' . Html::escape($details['value']) . '</code>)',
'#default_value' => $details['default'],
'#options' => [
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();
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;
}
elseif ($key == 'safe_tokens') {
$value = array_filter(array_map('trim', explode(',', $value ?? '')));
}
$config->set($key, $value);
}
$config->save();
parent::submitForm($form, $form_state);
}
}

View File

@@ -0,0 +1,54 @@
<?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?', ['%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();
$this->messenger()->addMessage($this->t('Disabled pattern %label.', [
'%label' => $this->entity->label(),
]));
$form_state->setRedirectUrl($this->getCancelUrl());
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace Drupal\pathauto\Form;
use Drupal\Core\Entity\EntityInterface;
/**
* Provides the pathauto pattern duplicate form.
*/
class PatternDuplicateForm extends PatternEditForm {
/**
* {@inheritdoc}
*/
public function setEntity(EntityInterface $entity) {
$this->entity = $entity->createDuplicate();
return $this;
}
}

View File

@@ -0,0 +1,295 @@
<?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 {
/**
* The alias type manager.
*
* @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;
/**
* The entity manager service.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The language manager service.
*
* @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
* The alias type manager.
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
* The entity type bundle info service.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity manager service.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager service.
*/
public 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' => [['type']],
'#submit' => ['::submitSelectType'],
'#executes_submit_callback' => TRUE,
'#ajax' => [
'callback' => '::ajaxReplacePatternForm',
'wrapper' => 'pathauto-pattern',
],
];
$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'] = [
'#type' => 'textfield',
'#title' => $this->t('Path pattern'),
'#default_value' => $this->entity->getPattern(),
'#size' => 65,
'#maxlength' => 1280,
'#element_validate' => ['token_element_validate', 'pathauto_pattern_validate'],
'#after_build' => ['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'] = [
'#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) {
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'] = [
'#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'] = [
'#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'] = [
'#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'] = [
'#type' => 'machine_name',
'#title' => $this->t('ID'),
'#maxlength' => 255,
'#default_value' => $this->entity->id(),
'#required' => TRUE,
'#disabled' => !$this->entity->isNew(),
'#machine_name' => [
'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, 'language'])) {
$entity->removeSelectionCondition($condition_id);
}
}
if ($bundles = array_filter((array) $form_state->getValue('bundles'))) {
$default_weight -= 5;
$entity->addSelectionCondition(
[
'id' => 'entity_bundle:' . $entity_type,
'bundles' => $bundles,
'negate' => FALSE,
'context_mapping' => [
$entity_type => $entity_type,
],
]
);
}
if ($languages = array_filter((array) $form_state->getValue('languages'))) {
$default_weight -= 5;
$language_mapping = $entity_type . ':' . $this->entityTypeManager->getDefinition($entity_type)->getKey('langcode') . ':language';
$entity->addSelectionCondition(
[
'id' => 'language',
'langcodes' => array_combine($languages, $languages),
'negate' => FALSE,
'context_mapping' => [
'language' => $language_mapping,
],
]
);
$entity->addRelationship($language_mapping, $this->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);
$this->messenger()->addMessage($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,54 @@
<?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?', ['%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();
$this->messenger()->addMessage($this->t('Enabled pattern %label.', [
'%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,41 @@
<?php
namespace Drupal\pathauto;
use Drupal\path\Plugin\Field\FieldType\PathFieldItemList;
class PathautoFieldItemList extends PathFieldItemList {
/**
* @{inheritdoc}
*/
protected function delegateMethod($method) {
// @todo Workaround until this is fixed, see
// https://www.drupal.org/project/drupal/issues/2946289.
$this->ensureComputedValue();
// Duplicate the logic instead of calling the parent due to the dynamic
// arguments.
$result = [];
$args = array_slice(func_get_args(), 1);
foreach ($this->list as $delta => $item) {
// call_user_func_array() is way slower than a direct call so we avoid
// using it if have no parameters.
$result[$delta] = $args ? call_user_func_array([$item, $method], $args) : $item->{$method}();
}
return $result;
}
/**
* @{inheritdoc}
*/
protected function computeValue() {
parent::computeValue();
// For a new entity, default to creating a new alias.
if ($this->getEntity()->isNew()) {
$this->list[0]->set('pathauto', PathautoState::CREATE);
}
}
}

View File

@@ -0,0 +1,411 @@
<?php
namespace Drupal\pathauto;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityMalformedException;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\Exception\UndefinedLinkTemplateException;
use Drupal\Core\Entity\RevisionableInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Messenger\MessengerTrait;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\Core\Utility\Token;
use Drupal\token\TokenEntityMapperInterface;
/**
* Provides methods for generating path aliases.
*/
class PathautoGenerator implements PathautoGeneratorInterface {
use MessengerTrait;
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 = [];
/**
* Available patterns per entity type ID.
*
* @var array
*/
protected $patternsByEntityType = [];
/**
* 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 $pathautoMessenger;
/**
* The token entity mapper.
*
* @var \Drupal\token\TokenEntityMapperInterface
*/
protected $tokenEntityMapper;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Manages pathauto alias type plugins.
*
* @var \Drupal\pathauto\AliasTypeManager
*/
protected $aliasTypeManager;
/**
* 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 \Drupal\pathauto\MessengerInterface $pathauto_messenger
* The messenger service.
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* The string translation service.
* @param \Drupal\token\TokenEntityMapperInterface $token_entity_mapper
* The token entity mapper.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\pathauto\AliasTypeManager $alias_type_manager
* Manages pathauto alias type plugins.
*/
public function __construct(ConfigFactoryInterface $config_factory, ModuleHandlerInterface $module_handler, Token $token, AliasCleanerInterface $alias_cleaner, AliasStorageHelperInterface $alias_storage_helper, AliasUniquifierInterface $alias_uniquifier, MessengerInterface $pathauto_messenger, TranslationInterface $string_translation, TokenEntityMapperInterface $token_entity_mapper, EntityTypeManagerInterface $entity_type_manager, AliasTypeManager $alias_type_manager = NULL) {
$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->pathautoMessenger = $pathauto_messenger;
$this->stringTranslation = $string_translation;
$this->tokenEntityMapper = $token_entity_mapper;
$this->entityTypeManager = $entity_type_manager;
$this->aliasTypeManager = $alias_type_manager ?: \Drupal::service('plugin.manager.alias_type');
}
/**
* {@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;
}
try {
$internalPath = $entity->toUrl()->getInternalPath();
}
// @todo convert to multi-exception handling in PHP 7.1.
catch (EntityMalformedException $exception) {
return NULL;
}
catch (UndefinedLinkTemplateException $exception) {
return NULL;
}
catch (\UnexpectedValueException $exception) {
return NULL;
}
$source = '/' . $internalPath;
$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 = [
'module' => $entity->getEntityType()->getProvider(),
'op' => $op,
'source' => $source,
'data' => $data,
'bundle' => $entity->bundle(),
'language' => &$langcode,
];
$pattern_original = $pattern->getPattern();
$this->moduleHandler->alter('pathauto_pattern', $pattern, $context);
$pattern_altered = $pattern->getPattern();
// 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, [
'clear' => TRUE,
'callback' => [$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 (!mb_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->pathautoMessenger->addMessage($this->t('The automatically generated alias %original_alias conflicted with an existing alias. Alias changed to %alias.', [
'%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 = [
'source' => $source,
'alias' => $alias,
'language' => $langcode,
];
$return = $this->aliasStorageHelper->save($path, $existing_alias, $op);
// Because there is no way to set an altered pattern to not be cached,
// change it back to the original value.
if ($pattern_altered !== $pattern_original) {
$pattern->setPattern($pattern_original);
}
return $return;
}
/**
* 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 = $this->entityTypeManager->getStorage('pathauto_pattern')
->getQuery()
->condition('type', array_keys(
$this->aliasTypeManager
->getPluginDefinitionByType($this->tokenEntityMapper->getTokenTypeForEntityType($entity_type_id))))
->condition('status', 1)
->sort('weight')
->accessCheck(TRUE)
->execute();
$this->patternsByEntityType[$entity_type_id] = $this->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 = []) {
// 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 += ['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->bundle() == $config_forum->get('vocabulary')) {
$type = 'forum';
}
}
try {
$result = $this->createEntityAlias($entity, $op);
}
catch (\InvalidArgumentException $e) {
$this->messenger()->addError($e->getMessage());
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,90 @@
<?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 = []);
}

View File

@@ -0,0 +1,48 @@
<?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 parent::isEmpty() && !$this->get('pathauto')->hasValue();
}
}

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,71 @@
<?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['pattern']['#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);
}
/**
* {@inheritdoc}
*/
public function getDefaultOperations(EntityInterface $entity) {
/** @var \Drupal\Core\Config\Entity\ConfigEntityInterface $entity */
$operations = parent::getDefaultOperations($entity);
if (!$entity->hasLinkTemplate('duplicate-form')) {
return $operations;
}
$operations['duplicate'] = [
'title' => $this->t('Duplicate'),
'weight' => 0,
'url' => $this->ensureDestination($entity->toUrl('duplicate-form')),
];
return $operations;
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace Drupal\pathauto;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DependencyInjection\ServiceProviderBase;
/**
* Remove the drush commands until path_alias module is enabled.
*/
class PathautoServiceProvider extends ServiceProviderBase {
/**
* {@inheritdoc}
*/
public function register(ContainerBuilder $container) {
$definitions = array_keys($container->getDefinitions());
if (!in_array('path_alias.repository', $definitions)) {
$container->removeDefinition('pathauto.commands');
}
}
}

View File

@@ -0,0 +1,190 @@
<?php
namespace Drupal\pathauto;
use Drupal\Component\Utility\Crypt;
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;
/**
* Pathauto State Original value.
*
* @var int
*/
protected $originalValue;
/**
* {@inheritdoc}
*/
public function getValue() {
if ($this->value === NULL) {
$this->value = $this->getOriginalValue();
// 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;
}
/**
* Gets the data value currently stored in database.
*
* @return mixed
* The data value.
*/
protected function getOriginalValue() {
if ($this->originalValue === NULL) {
// If no value has been set or loaded yet, try to load a value if this
// entity has already been saved.
if ($this->parent->getEntity()->isNew()) {
return NULL;
}
$this->originalValue = \Drupal::keyValue($this->getCollection())
->get(static::getPathautoStateKey($this->parent->getEntity()->id()));
}
return $this->originalValue;
}
/**
* {@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() {
// Do nothing if current value is same as original value.
if ($this->getValue() === $this->getOriginalValue()) {
return;
}
\Drupal::keyValue($this->getCollection())
->set(static::getPathautoStateKey($this->parent->getEntity()->id()), $this->getValue());
}
/**
* Deletes the stored state.
*/
public function purge() {
\Drupal::keyValue($this->getCollection())
->delete(static::getPathautoStateKey($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();
}
/**
* Deletes the URL aliases for multiple entities of the same type.
*
* @param string $entity_type_id
* The entity type ID of entities being deleted.
* @param int[] $pids_by_id
* A list of path IDs keyed by entity ID.
*/
public static function bulkDelete($entity_type_id, array $pids_by_id) {
foreach ($pids_by_id as $id => $pid) {
// Some key-values store entries have computed keys.
$key = static::getPathautoStateKey($id);
if ($key !== $id) {
$pids_by_id[$key] = $pid;
unset($pids_by_id[$id]);
}
}
$states = \Drupal::keyValue("pathauto_state.$entity_type_id")
->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);
}
/**
* Gets the key-value store entry key for 'pathauto_state.*' collections.
*
* Normally we want to use the entity ID as key for 'pathauto_state.*'
* collection entries. But some entity types may use string IDs. When such IDs
* are exceeding 128 characters, which is the limit for the 'name' column in
* the {key_value} table, the insertion of the ID in {key_value} will fail.
* Thus we test if we can use the plain ID or we need to store a hashed
* version of the entity ID. Also, it is not possible to rely on the UUID as
* entity types might not have one or might use a non-standard format.
*
* The code is inspired by
* \Drupal\Core\Cache\DatabaseBackend::normalizeCid().
*
* @param int|string $entity_id
* The entity id for which to compute the key.
*
* @return int|string
* The key used to store the value in the key-value store.
*
* @see \Drupal\Core\Cache\DatabaseBackend::normalizeCid()
*/
public static function getPathautoStateKey($entity_id) {
$entity_id_is_ascii = mb_check_encoding($entity_id, 'ASCII');
if ($entity_id_is_ascii && strlen($entity_id) <= 128) {
// The original entity ID, if it's an ASCII of 128 characters or less.
return $entity_id;
}
return Crypt::hashBase64($entity_id);
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace Drupal\pathauto;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Drupal\path\Plugin\Field\FieldWidget\PathWidget;
/**
* 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)) {
// Explicitly turn off pathauto here.
$element['pathauto'] = [
'#type' => 'value',
'#value' => PathautoState::SKIP,
];
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'] = [
'#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]"]'] = ['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', ['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,96 @@
<?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\EntityContextDefinition;
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;
/**
* The entity field manager.
*
* @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.
* @param \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_definitions'] = [
$entity_type_id => new EntityContextDefinition("entity:$entity_type_id", $this->t('@label being aliased', ['@label' => $entity_type->getLabel()])),
];
}
}
return $this->derivatives;
}
}

View File

@@ -0,0 +1,133 @@
<?php
namespace Drupal\pathauto\Plugin\migrate\source;
use Drupal\Core\Entity\EntityTypeBundleInfo;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\State\StateInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Row;
use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Fetches pathauto patterns from the source database.
*
* @MigrateSource(
* id = "pathauto_pattern",
* source_module = "pathauto",
* )
*/
class PathautoPattern extends DrupalSqlBase {
/**
* The entity type bundle info.
*
* @var \Drupal\Core\Entity\EntityTypeBundleInfo
*/
protected $entityTypeBundleInfo;
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, StateInterface $state, EntityTypeManagerInterface $entity_type_manager, EntityTypeBundleInfo $entity_bundle_info) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $migration, $state, $entity_type_manager);
$this->entityTypeBundleInfo = $entity_bundle_info;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration = NULL) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$migration,
$container->get('state'),
$container->get('entity_type.manager'),
$container->get('entity_type.bundle.info')
);
}
/**
* {@inheritdoc}
*/
public function query() {
// Fetch all pattern variables whose value is not a serialized empty string.
return $this->select('variable', 'v')
->fields('v', ['name', 'value'])
->condition('name', 'pathauto_%_pattern', 'LIKE')
->condition('value', 's:0:"";', '<>');
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['name']['type'] = 'string';
return $ids;
}
/**
* {@inheritdoc}
*/
public function fields() {
return [
'name' => $this->t("The name of the pattern's variable."),
'value' => $this->t("The value of the pattern's variable."),
];
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
$entity_definitions = $this->entityTypeManager->getDefinitions();
$name = $row->getSourceProperty('name');
// Pattern variables are made of pathauto_[entity type]_[bundle]_pattern.
// First let's find a matching entity type from the variable name.
foreach ($entity_definitions as $entity_type => $definition) {
// Check if this is the default pattern for this entity type.
// Otherwise, check if this is a pattern for a specific bundle.
if ($name == 'pathauto_' . $entity_type . '_pattern') {
// Set process values.
$row->setSourceProperty('id', $entity_type);
$row->setSourceProperty('label', (string) $definition->getLabel() . ' - default');
$row->setSourceProperty('type', 'canonical_entities:' . $entity_type);
$row->setSourceProperty('pattern', unserialize($row->getSourceProperty('value'), ['allowed_classes' => FALSE]));
return parent::prepareRow($row);
}
elseif (strpos($name, 'pathauto_' . $entity_type . '_') === 0) {
// Extract the bundle out of the variable name.
preg_match('/^pathauto_' . $entity_type . '_([a-zA-z0-9_]+)_pattern$/', $name, $matches);
$bundle = $matches[1];
// Check that the bundle exists.
$bundles = $this->entityTypeBundleInfo->getBundleInfo($entity_type);
if (!in_array($bundle, array_keys($bundles))) {
// No matching bundle found in destination.
return FALSE;
}
// Set process values.
$row->setSourceProperty('id', $entity_type . '_' . $bundle);
$row->setSourceProperty('label', (string) $definition->getLabel() . ' - ' . $bundles[$bundle]['label']);
$row->setSourceProperty('type', 'canonical_entities:' . $entity_type);
$row->setSourceProperty('pattern', unserialize($row->getSourceProperty('value'), ['allowed_classes' => FALSE]));
$selection_criteria = [
'id' => 'entity_bundle:' . $entity_type,
'bundles' => [$bundle => $bundle],
'negate' => FALSE,
'context_mapping' => [$entity_type => $entity_type],
];
$row->setSourceProperty('selection_criteria', [$selection_criteria]);
return parent::prepareRow($row);
}
}
return FALSE;
}
}

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,378 @@
<?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\ContextAwarePluginTrait;
use Drupal\Core\Plugin\PluginBase;
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 PluginBase implements AliasTypeInterface, AliasTypeBatchUpdateInterface, ContainerFactoryPluginInterface {
use ContextAwarePluginTrait;
/**
* 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($this->getTableInfo()['table'], 'pa', "CONCAT('" . $this->getSourcePrefix() . "' , base_table.$id_key) = pa.{$this->getTableInfo()['fields']['path']}");
$query->addField('base_table', $id_key, 'id');
switch ($action) {
case 'create':
$query->isNull("pa.{$this->getTableInfo()['fields']['path']}");
break;
case 'update':
$query->isNotNull("pa.{$this->getTableInfo()['fields']['path']}");
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'] = !empty($ids) ? max($ids) : 0;
$context['results']['updates'] += $updates;
$context['message'] = $this->t('Updated alias for %label @id.', [
'%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($this->getTableInfo()['table'], 'pa', "CONCAT('" . $this->getSourcePrefix() . "' , base_table.$id_key) = pa.{$this->getTableInfo()['fields']['path']}");
$query->addField('base_table', $id_key, 'id');
$query->addField('pa', $this->getTableInfo()['fields']['id']);
$query->condition("pa.{$this->getTableInfo()['fields']['id']}}", $context['sandbox']['current'], '>');
$query->orderBy("pa.{$this->getTableInfo()['fields']['id']}}");
$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();
PathautoState::bulkDelete($this->getEntityTypeId(), $pids_by_id);
$context['sandbox']['count'] += count($pids_by_id);
$context['sandbox']['current'] = !empty($pids_by_id) ? max($pids_by_id) : 0;
$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 = []) {
$options += ['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'])) {
$this->messenger->addMessage($this->formatPlural(count($ids), 'Updated 1 %label URL alias.', 'Updated @count %label URL aliases.'), [
'%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.
*
* @deprecated Use \Drupal\pathauto\PathautoState::bulkDelete() instead.
*/
protected function bulkDelete(array $pids_by_id) {
PathautoState::bulkDelete($this->getEntityTypeId(), $pids_by_id);
}
/**
* {@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;
}
/**
* Returns information about the path alias table and field names.
*
* @return array
* An array with the following structure:
* - table: the name of the table where path aliases are stored;
* - fields: an array containing the mapping of path alias properties to
* their table column names.
*/
private function getTableInfo() {
if (version_compare(\Drupal::VERSION, '8.8', '<')) {
return [
'table' => 'url_alias',
'fields' => [
'id' => 'pid',
'path' => 'source',
'alias' => 'alias',
'langcode' => 'langcode',
],
];
}
else {
return [
'table' => 'path_alias',
'fields' => [
'id' => 'id',
'path' => 'path',
'alias' => 'alias',
'langcode' => 'langcode',
],
];
}
}
}

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_definitions = {
* "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->bundle() == $vid;
}
return FALSE;
}
}

View File

@@ -0,0 +1,79 @@
<?php
namespace Drupal\pathauto;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Messenger\MessengerInterface as CoreMessengerInterface;
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;
/**
* The messenger service.
*
* @var \Drupal\Core\Messenger\MessengerInterface
*/
protected $messenger;
/**
* Creates a verbose messenger.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
* @param \Drupal\Core\Session\AccountInterface $account
* The current user account.
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
* The messenger service.
*/
public function __construct(ConfigFactoryInterface $config_factory, AccountInterface $account, CoreMessengerInterface $messenger) {
$this->configFactory = $config_factory;
$this->account = $account;
$this->messenger = $messenger;
}
/**
* {@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, ['bulkupdate', 'return']))) {
return FALSE;
}
if ($message) {
$this->messenger->addMessage($message);
}
return TRUE;
}
}

View File

@@ -0,0 +1,9 @@
name: 'Pathauto custom punctuation testing module'
type: module
description: 'Add some uncommon punctuation to the replacement list.'
package: Testing
# Information added by Drupal.org packaging script on 2024-05-18
version: '8.x-1.12+6-dev'
project: 'pathauto'
datestamp: 1716065615

View File

@@ -0,0 +1,8 @@
<?php
/**
* Implements hook_pathauto_punctuation_chars_alter().
*/
function pathauto_custom_punctuation_test_pathauto_punctuation_chars_alter(array &$punctuation) {
$punctuation['copyright'] = ['value' => '©', 'name' => t('Copyright symbol')];
}

View File

@@ -0,0 +1,11 @@
name: 'Pathauto testing module'
type: module
description: 'Pathauto for Entity with string ID.'
package: Testing
dependencies:
- token:token
# Information added by Drupal.org packaging script on 2024-05-18
version: '8.x-1.12+6-dev'
project: 'pathauto'
datestamp: 1716065615

View File

@@ -0,0 +1,50 @@
<?php
namespace Drupal\pathauto_string_id_test\Entity;
use Drupal\Core\Entity\ContentEntityBase;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
/**
* Defines a test entity with a string ID.
*
* @ContentEntityType(
* id = "pathauto_string_id_test",
* label = @Translation("Test entity with string ID"),
* handlers = {
* "route_provider" = {
* "html" = "Drupal\Core\Entity\Routing\DefaultHtmlRouteProvider",
* },
* },
* base_table = "pathauto_string_id_test",
* entity_keys = {
* "id" = "id",
* "label" = "name",
* },
* links = {
* "canonical" = "/pathauto_string_id_test/{pathauto_string_id_test}",
* },
* )
*/
class PathautoStringIdTest extends ContentEntityBase {
/**
* {@inheritdoc}
*/
public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
$fields['id'] = BaseFieldDefinition::create('string')
->setLabel('ID')
->setReadOnly(TRUE)
// A bigger value will not be allowed to build the index.
->setSetting('max_length', 191);
$fields['name'] = BaseFieldDefinition::create('string')
->setLabel('Name');
$fields['path'] = BaseFieldDefinition::create('path')
->setLabel('Path')
->setComputed(TRUE);
return $fields;
}
}

View File

@@ -0,0 +1,11 @@
name: 'Views Test Config'
type: module
description: 'Provides default views for tests.'
package: Testing
dependencies:
- drupal:views
# Information added by Drupal.org packaging script on 2024-05-18
version: '8.x-1.12+6-dev'
project: 'pathauto'
datestamp: 1716065615

View File

@@ -0,0 +1,169 @@
<?php
namespace Drupal\Tests\pathauto\Functional;
use Drupal\pathauto\PathautoGeneratorInterface;
use Drupal\pathauto\PathautoState;
use Drupal\Tests\BrowserTestBase;
/**
* Bulk update functionality tests.
*
* @group pathauto
*/
class PathautoBulkUpdateTest extends BrowserTestBase {
use PathautoTestHelperTrait;
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['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}
*/
protected function setUp(): void {
parent::setUp();
// Allow other modules to add additional permissions for the admin user.
$permissions = [
'administer pathauto',
'administer url aliases',
'create url aliases',
'administer forums',
];
$this->adminUser = $this->drupalCreateUser($permissions);
$this->drupalLogin($this->adminUser);
$this->patterns = [];
$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]');
}
public function testBulkUpdate() {
// Create some nodes.
$this->nodes = [];
for ($i = 1; $i <= 5; $i++) {
$node = $this->drupalCreateNode();
$this->nodes[$node->id()] = $node;
}
// Clear out all aliases.
$this->deleteAllAliases();
// Bulk create aliases.
$edit = [
'update[canonical_entities:node]' => TRUE,
'update[canonical_entities:user]' => TRUE,
'update[forum]' => TRUE,
];
$this->drupalGet('admin/config/search/path/update_bulk');
$this->submitForm($edit, 'Update');
// This has generated 8 aliases: 5 nodes, 2 users and 1 forum.
$this->assertSession()->pageTextContains('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(['path' => '/taxonomy/term/1']);
// Add a new node.
$new_node = $this->drupalCreateNode(['path' => ['alias' => '', 'pathauto' => PathautoState::SKIP]]);
// Run the update again which should not run against any nodes.
$this->drupalGet('admin/config/search/path/update_bulk');
$this->submitForm($edit, 'Update');
$this->assertSession()->pageTextContains('No new URL aliases to generate.');
$this->assertNoEntityAliasExists($new_node);
// Make sure existing aliases can be overridden.
$this->drupalGet('admin/config/search/path/settings');
$this->submitForm(['update_action' => (string) PathautoGeneratorInterface::UPDATE_ACTION_DELETE], 'Save configuration');
// Patterns did not change, so no aliases should be regenerated.
$edit['action'] = 'all';
$this->drupalGet('admin/config/search/path/update_bulk');
$this->submitForm($edit, 'Update');
$this->assertSession()->pageTextContains('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->drupalGet('admin/config/search/path/update_bulk');
$this->submitForm($edit, 'Update');
$this->assertSession()->pageTextContains('Generated 5 URL aliases.');
// Prevent existing aliases to be overridden. The bulk generate page should
// only offer to create an alias for paths which have none.
$this->drupalGet('admin/config/search/path/settings');
$this->submitForm(['update_action' => (string) PathautoGeneratorInterface::UPDATE_ACTION_NO_NEW], 'Save configuration');
$this->drupalGet('admin/config/search/path/update_bulk');
$this->assertSession()->fieldValueEquals('action', 'create');
$this->assertSession()->pageTextContains('Pathauto settings are set to ignore paths which already have a URL alias.');
$this->assertSession()->fieldValueNotEquals('action', 'update');
$this->assertSession()->fieldValueNotEquals('action', 'all');
}
/**
* Tests alias generation for nodes that existed before installing Pathauto.
*/
public 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([$node->id()]);
// Execute bulk generation.
// Bulk create aliases.
$edit = [
'update[canonical_entities:node]' => TRUE,
];
$this->drupalGet('admin/config/search/path/update_bulk');
$this->submitForm($edit, 'Update');
// Verify that the alias was created for the node.
$this->assertSession()->pageTextContains('Generated 1 URL alias.');
}
}

View File

@@ -0,0 +1,93 @@
<?php
namespace Drupal\Tests\pathauto\Functional;
use Drupal\comment\Tests\CommentTestTrait;
use Drupal\Tests\BrowserTestBase;
/**
* Tests pathauto settings form.
*
* @group pathauto
*/
class PathautoEnablingEntityTypesTest extends BrowserTestBase {
use PathautoTestHelperTrait;
use CommentTestTrait;
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['node', 'pathauto', 'comment'];
/**
* Admin user.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->drupalCreateContentType(['type' => 'article']);
$this->addDefaultCommentField('node', 'article');
$permissions = [
'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.
*/
public 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->assertCount(0, $this->cssSelect('option[value = "canonical_entities:comment"]:contains(Comment)'));
$this->assertCount(0, $this->cssSelect('option:contains(Broken)'));
// Enable the entity type and create a pattern for it.
$this->drupalGet('/admin/config/search/path/settings');
$edit = [
'enabled_entity_types[comment]' => TRUE,
];
$this->submitForm($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->submitForm($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->assertCount(1, $this->cssSelect('input[name = "enabled_entity_types[comment]"][disabled = "disabled"]'));
}
}

View File

@@ -0,0 +1,218 @@
<?php
namespace Drupal\Tests\pathauto\Functional;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\pathauto\PathautoState;
use Drupal\Tests\BrowserTestBase;
/**
* Mass delete functionality tests.
*
* @group pathauto
*/
class PathautoMassDeleteTest extends BrowserTestBase {
use PathautoTestHelperTrait;
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['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}
*/
protected function setUp(): void {
parent::setUp();
$permissions = [
'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.
*/
public function testDeleteAll() {
/** @var \Drupal\pathauto\AliasStorageHelperInterface $alias_storage_helper */
$alias_storage_helper = \Drupal::service('pathauto.alias_storage_helper');
// 1. Test that deleting all the aliases, of any type, works.
$this->generateAliases();
$edit = [
'delete[all_aliases]' => TRUE,
'options[keep_custom_aliases]' => FALSE,
];
$this->drupalGet('admin/config/search/path/delete_bulk');
$this->submitForm($edit, 'Delete aliases now!');
$this->assertSession()->pageTextContains('All of your path aliases have been deleted.');
$this->assertSession()->addressEquals('admin/config/search/path/delete_bulk');
// Make sure that all of them are actually deleted.
$this->assertEquals(0, $alias_storage_helper->countAll(), 'All the aliases have been deleted.');
// 2. Test deleting only specific (entity type) aliases.
$manager = $this->container->get('plugin.manager.alias_type');
$pathauto_plugins = [
'canonical_entities:node' => 'nodes',
'canonical_entities:taxonomy_term' => 'terms',
'canonical_entities:user' => 'accounts',
];
foreach ($pathauto_plugins as $pathauto_plugin => $attribute) {
$this->generateAliases();
$edit = [
'delete[plugins][' . $pathauto_plugin . ']' => TRUE,
'options[keep_custom_aliases]' => FALSE,
];
$this->drupalGet('admin/config/search/path/delete_bulk');
$this->submitForm($edit, 'Delete aliases now!');
$alias_type = $manager->createInstance($pathauto_plugin);
$this->assertSession()->responseContains(new FormattableMarkup('All of your %label path aliases have been deleted.', ['%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 = [
'delete[all_aliases]' => TRUE,
'options[keep_custom_aliases]' => TRUE,
];
$this->drupalGet('admin/config/search/path/delete_bulk');
$this->submitForm($edit, 'Delete aliases now!');
$this->assertSession()->pageTextContains('All of your automatically generated path aliases have been deleted.');
$this->assertSession()->addressEquals('admin/config/search/path/delete_bulk');
// Make sure that only custom aliases and aliases with no information about
// their state still exist.
$this->assertEquals(3, $alias_storage_helper->countAll(), 'Custom aliases still exist.');
$this->assertEquals('/node/101', $alias_storage_helper->loadBySource('/node/101', 'en')['source']);
$this->assertEquals('/node/104', $alias_storage_helper->loadBySource('/node/104', 'en')['source']);
$this->assertEquals('/node/105', $alias_storage_helper->loadBySource('/node/105', 'en')['source']);
}
/**
* Helper function to generate aliases.
*/
public 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 $account) {
$account->save();
}
}
if (empty($this->terms)) {
$vocabulary = $this->addVocabulary(['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 (['nodes', 'accounts', 'terms'] as $attribute) {
foreach ($this->{$attribute} as $entity) {
$this->assertEntityAliasExists($entity);
}
}
}
}

View File

@@ -0,0 +1,392 @@
<?php
namespace Drupal\Tests\pathauto\Functional;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\node\Entity\Node;
use Drupal\pathauto\Entity\PathautoPattern;
use Drupal\pathauto\PathautoState;
use Drupal\Tests\BrowserTestBase;
/**
* Tests pathauto node UI integration.
*
* @group pathauto
*/
class PathautoNodeWebTest extends BrowserTestBase {
use PathautoTestHelperTrait;
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['node', 'pathauto', 'views', 'taxonomy', 'pathauto_views_test'];
/**
* Admin user.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']);
$this->drupalCreateContentType(['type' => 'article']);
// Allow other modules to add additional permissions for the admin user.
$permissions = [
'administer pathauto',
'administer url aliases',
'create url aliases',
'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.
*/
public function testNodeEditing() {
// Ensure that the Pathauto checkbox is checked by default on the node add
// form.
$this->drupalGet('node/add/page');
$this->assertSession()->checkboxChecked('edit-path-0-pathauto');
// Create a node by saving the node form.
$title = ' Testing: node title [';
$automatic_alias = '/content/testing-node-title';
$this->submitForm(['title[0][value]' => $title], 'Save');
$node = $this->drupalGetNodeByTitle($title);
// Look for alias generated in the form.
$this->drupalGet("node/{$node->id()}/edit");
$this->assertSession()->checkboxChecked('edit-path-0-pathauto');
$this->assertSession()->fieldValueEquals('path[0][alias]', $automatic_alias);
// Check whether the alias actually works.
$this->assertSession()->responseContains($title);
// Manually set the node's alias.
$manual_alias = '/content/' . $node->id();
$edit = [
'path[0][pathauto]' => FALSE,
'path[0][alias]' => $manual_alias,
];
$this->drupalGet($node->toUrl('edit-form'));
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains(new FormattableMarkup('@title has been updated.', ['@title' => $title]));
// Check that the automatic alias checkbox is now unchecked by default.
$this->drupalGet("node/{$node->id()}/edit");
$this->assertSession()->checkboxNotChecked('edit-path-0-pathauto');
$this->assertSession()->fieldValueEquals('path[0][alias]', $manual_alias);
// Submit the node form with the default values.
$this->submitForm(['path[0][pathauto]' => FALSE], 'Save');
$this->assertSession()->pageTextContains(new FormattableMarkup('@title has been updated.', ['@title' => $title]));
// Test that the old (automatic) alias has been deleted and only accessible
// through the new (manual) alias.
$this->drupalGet($automatic_alias);
$this->assertSession()->statusCodeEquals(404);
$this->drupalGet($manual_alias);
$this->assertSession()->pageTextContains($title);
// Test that the manual alias is not kept for new nodes when the pathauto
// checkbox is ticked.
$title = 'Automatic Title';
$edit = [
'title[0][value]' => $title,
'path[0][pathauto]' => TRUE,
'path[0][alias]' => '/should-not-get-created',
];
$this->drupalGet('node/add/page');
$this->submitForm( $edit, 'Save');
$this->assertNoAliasExists(['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')
->accessCheck(TRUE)
->execute();
foreach (PathautoPattern::loadMultiple($ids) as $pattern) {
$pattern->delete();
}
$this->drupalGet('node/add/article');
$this->assertSession()->fieldNotExists('edit-path-0-pathauto');
$this->assertSession()->fieldValueEquals('path[0][alias]', '');
$edit = [];
$edit['title'] = 'My test article';
$this->drupalCreateNode($edit);
$node = $this->drupalGetNodeByTitle($edit['title']);
// Pathauto checkbox should still not exist.
$this->drupalGet($node->toUrl('edit-form'));
$this->assertSession()->fieldNotExists('edit-path-0-pathauto');
$this->assertSession()->fieldValueEquals('path[0][alias]', '');
$this->assertNoEntityAlias($node);
}
/**
* Test node operations.
*/
public function testNodeOperations() {
$node1 = $this->drupalCreateNode(['title' => 'node1']);
$node2 = $this->drupalCreateNode(['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 = [
'action' => 'pathauto_update_alias_node',
'node_bulk_form[' . $index . ']' => TRUE,
];
$this->submitForm($edit, 'Apply to selected items');
$this->assertSession()->pageTextContains('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(['bypass node access']);
$nodeAliasUser = $this->drupalCreateUser(['bypass node access', 'create url aliases']);
$node = $this->drupalCreateNode([
'title' => 'Node version one',
'type' => 'page',
'path' => [
'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->assertSame(PathautoState::SKIP, $node->path->pathauto);
// 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->assertSession()->fieldNotExists('path[0][pathauto]');
$edit = ['title[0][value]' => 'Node version two'];
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains('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->assertSession()->checkboxNotChecked('edit-path-0-pathauto');
// Edit the manual alias and save the node.
$edit = [
'title[0][value]' => 'Node version three',
'path[0][alias]' => '/manually-edited-alias',
];
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains('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->assertSame(PathautoState::CREATE, $node->path->pathauto);
$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.
$entities = PathautoPattern::loadMultiple(NULL);
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->drupalGet('node/add/article');
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains('article Sample article has been created.');
// Test the alias.
$this->assertAliasExists(['alias' => '/sample-article']);
$this->drupalGet('sample-article');
$this->assertSession()->statusCodeEquals(200);
// 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->assertSession()->statusCodeEquals(200);
}
/**
* Tests that nodes with an automatic alias can get a custom alias.
*/
public function testCustomAliasAfterAutomaticAlias() {
// Create a pattern.
$this->createPattern('node', '/content/[node:title]');
// Create a node with an automatic alias.
$edit = [
'title[0][value]' => 'Sample article',
];
$this->drupalGet('node/add/article');
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains('article Sample article has been created.');
// Ensure that the automatic alias got created.
$this->assertAliasExists(['alias' => '/content/sample-article']);
$this->drupalGet('/content/sample-article');
$this->assertSession()->statusCodeEquals(200);
// Now edit the node, set a custom alias.
$edit = [
'path[0][pathauto]' => 0,
'path[0][alias]' => '/sample-pattern-for-article',
];
$this->drupalGet('node/1/edit');
$this->submitForm($edit, 'Save');
// Assert that the new alias exists and the old one does not.
$this->assertAliasExists(['alias' => '/sample-pattern-for-article']);
$this->assertNoAliasExists(['alias' => '/content/sample-article']);
$this->drupalGet('sample-pattern-for-article');
$this->assertSession()->statusCodeEquals(200);
}
/**
* Tests setting custom alias for nodes after removing pattern.
*
* Tests that nodes that had an automatic alias can get a custom alias after
* the pathauto pattern on which the automatic alias was based, is removed.
*/
public function testCustomAliasAfterRemovingPattern() {
// Create a pattern.
$this->createPattern('node', '/content/[node:title]');
// Create a node with an automatic alias.
$edit = [
'title[0][value]' => 'Sample article',
];
$this->drupalGet('node/add/article');
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains('article Sample article has been created.');
// Ensure that the automatic alias got created.
$this->assertAliasExists(['alias' => '/content/sample-article']);
$this->drupalGet('/content/sample-article');
$this->assertSession()->statusCodeEquals(200);
// Go to the edit the node form and confirm that the pathauto checkbox
// exists.
$this->drupalGet('node/1/edit');
$this->assertSession()->elementExists('css', '#edit-path-0-pathauto');
// Delete all patterns to be sure that there will be no match.
$entities = PathautoPattern::loadMultiple(NULL);
foreach ($entities as $entity) {
$entity->delete();
}
// Reload the node edit form and confirm that the pathauto checkbox no
// longer exists.
$this->drupalGet('node/1/edit');
$this->assertSession()->elementNotExists('css', '#edit-path-0-pathauto');
// Set a custom alias. We cannot disable the pathauto checkbox, because
// there is none.
$edit = [
'path[0][alias]' => '/sample-alias-for-article',
];
$this->submitForm($edit, 'Save');
// Check that the new alias exists and the old one does not.
$this->assertAliasExists(['alias' => '/sample-alias-for-article']);
$this->assertNoAliasExists(['alias' => '/content/sample-article']);
$this->drupalGet('sample-alias-for-article');
$this->assertSession()->statusCodeEquals(200);
}
}

View File

@@ -0,0 +1,247 @@
<?php
namespace Drupal\Tests\pathauto\Functional;
use Drupal\pathauto\PathautoGeneratorInterface;
use Drupal\Tests\BrowserTestBase;
/**
* Tests pathauto settings form.
*
* @group pathauto
*/
class PathautoSettingsFormWebTest extends BrowserTestBase {
use PathautoTestHelperTrait;
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['node', 'pathauto'];
/**
* Admin user.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* Form values that are set by default.
*
* @var array
*/
protected $defaultFormValues = [
'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 = [
'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}
*/
protected function setUp(): void {
parent::setUp();
$this->drupalCreateContentType(['type' => 'article']);
$permissions = [
'administer pathauto',
'notify of path changes',
'administer url aliases',
'create url aliases',
'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.
*/
public function testDefaultFormValues() {
$this->drupalGet('/admin/config/search/path/settings');
$this->assertSession()->checkboxNotChecked('edit-verbose');
$this->assertSession()->fieldExists('edit-separator');
$this->assertSession()->checkboxChecked('edit-case');
$this->assertSession()->fieldExists('edit-max-length');
$this->assertSession()->fieldExists('edit-max-component-length');
$this->assertSession()->checkboxChecked('edit-update-action-2');
$this->assertSession()->checkboxChecked('edit-transliterate');
$this->assertSession()->checkboxNotChecked('edit-reduce-ascii');
$this->assertSession()->fieldExists('edit-ignore-words');
}
/**
* Test the verbose option.
*/
public function testVerboseOption() {
$edit = ['verbose' => '1'];
$this->drupalGet('/admin/config/search/path/settings');
$this->submitForm($edit, 'Save configuration');
$this->assertSession()->pageTextContains('The configuration options have been saved.');
$this->assertSession()->checkboxChecked('edit-verbose');
$title = 'Verbose settings test';
$this->drupalGet('/node/add/article');
$this->assertSession()->checkboxChecked('edit-path-0-pathauto');
$this->submitForm(['title[0][value]' => $title], 'Save');
$this->assertSession()->pageTextContains('Created new alias /content/verbose-settings-test for');
$node = $this->drupalGetNodeByTitle($title);
$this->drupalGet('/node/' . $node->id() . '/edit');
$this->submitForm(['title[0][value]' => 'Updated title'], 'Save');
$this->assertSession()->pageTextContains('Created new alias /content/updated-title for');
$this->assertSession()->pageTextContains('replacing /content/verbose-settings-test.');
}
/**
* Tests generating aliases with different settings.
*/
public function testSettingsForm() {
// Ensure the separator settings apply correctly.
$this->checkAlias('My awesome content', '/content/my.awesome.content', ['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', ['case' => FALSE]);
$this->checkAlias('Change Lower', '/content/change-lower', ['case' => '1']);
// Ensure the maximum alias length is working.
$this->checkAlias('My awesome Content', '/content/my-awesome', ['max_length' => '23']);
// Ensure the maximum component length is working.
$this->checkAlias('My awesome Content', '/content/my', ['max_component_length' => '2']);
// Ensure transliteration option is working.
$this->checkAlias('è é àl ö äl ü', '/content/e-e-al-o-al-u', ['transliterate' => '1']);
$this->checkAlias('è é àl äl ö ü', '/content/è-é-àl-äl-ö-ü', ['transliterate' => FALSE]);
$ignore_words = 'a, new, very, should';
$this->checkAlias('a very new alias to test', '/content/alias-to-test', ['ignore_words' => $ignore_words]);
}
/**
* Test the punctuation setting form items.
*/
public 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 = []) {
// Submit the settings form.
$edit = array_merge($this->defaultFormValues + $this->defaultPunctuations, $settings);
$this->drupalGet('/admin/config/search/path/settings');
$this->submitForm($edit, 'Save configuration');
$this->assertSession()->pageTextContains('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(
[
'title' => $title,
'type' => 'article',
]
);
$this->drupalGet($alias);
$this->assertSession()->statusCodeEquals(200);
$this->assertEntityAlias($node, $alias);
}
}

View File

@@ -0,0 +1,109 @@
<?php
namespace Drupal\Tests\pathauto\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Tests pathauto taxonomy UI integration.
*
* @group pathauto
*/
class PathautoTaxonomyWebTest extends BrowserTestBase {
use PathautoTestHelperTrait;
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['taxonomy', 'pathauto', 'views'];
/**
* Admin user.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Allow other modules to add additional permissions for the admin user.
$permissions = [
'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.
*/
public function testTermEditing() {
$this->drupalGet('admin/structure');
$this->drupalGet('admin/structure/taxonomy');
// Add vocabulary "tags".
$this->addVocabulary(['name' => 'tags', 'vid' => 'tags']);
// Create term for testing.
$name = 'Testing: term name [';
$automatic_alias = '/tags/testing-term-name';
$this->drupalGet('admin/structure/taxonomy/manage/tags/add');
$this->submitForm(['name[0][value]' => $name], 'Save');
$name = trim($name);
$this->assertSession()->pageTextContains("Created new term $name.");
$term = $this->drupalGetTermByName($name);
// Look for alias generated in the form.
$this->drupalGet("taxonomy/term/{$term->id()}/edit");
$this->assertSession()->checkboxChecked('edit-path-0-pathauto');
$this->assertSession()->fieldValueEquals('path[0][alias]', $automatic_alias);
// Check whether the alias actually works.
$this->drupalGet($automatic_alias);
$this->assertSession()->pageTextContains($name);
// Manually set the term's alias.
$manual_alias = '/tags/' . $term->id();
$edit = [
'path[0][pathauto]' => FALSE,
'path[0][alias]' => $manual_alias,
];
$this->drupalGet("taxonomy/term/{$term->id()}/edit");
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains("Updated term $name.");
// Check that the automatic alias checkbox is now unchecked by default.
$this->drupalGet("taxonomy/term/{$term->id()}/edit");
$this->assertSession()->checkboxNotChecked('edit-path-0-pathauto');
$this->assertSession()->fieldValueEquals('path[0][alias]', $manual_alias);
// Submit the term form with the default values.
$this->submitForm(['path[0][pathauto]' => FALSE], 'Save');
$this->assertSession()->pageTextContains("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->assertSession()->statusCodeEquals(404);
$this->drupalGet($manual_alias);
$this->assertSession()->pageTextContains($name);
}
}

View File

@@ -0,0 +1,252 @@
<?php
namespace Drupal\Tests\pathauto\Functional;
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\Entity\Term;
use Drupal\taxonomy\Entity\Vocabulary;
use Drupal\taxonomy\VocabularyInterface;
use Drupal\Tests\Traits\Core\PathAliasTestTrait;
/**
* Helper test class with some added functions for testing.
*/
trait PathautoTestHelperTrait {
use PathAliasTestTrait;
/**
* 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' => mb_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) {
$pattern->addSelectionCondition(
[
'id' => 'entity_bundle:' . $entity_type,
'bundles' => [
$bundle => $bundle,
],
'negate' => FALSE,
'context_mapping' => [
$entity_type => $entity_type,
],
]
);
}
/**
* Assert the expected value for a token.
*/
public function assertToken($type, $object, $token, $expected) {
$bubbleable_metadata = new BubbleableMetadata();
$tokens = \Drupal::token()->generate($type, [$token => $token], [$type => $object], [], $bubbleable_metadata);
$tokens += [$token => ''];
$this->assertSame($tokens[$token], $expected, t("Token value for [@type:@token] was '@actual', expected value '@expected'.", [
'@type' => $type,
'@token' => $token,
'@actual' => $tokens[$token],
'@expected' => $expected,
]));
}
/**
* Create a path alias for an entity.
*/
public function saveEntityAlias(EntityInterface $entity, $alias, $langcode = NULL) {
// By default, use the entity language.
if (!$langcode) {
$langcode = $entity->language()->getId();
}
return $this->createPathAlias('/' . $entity->toUrl()->getInternalPath(), $alias, $langcode);
}
/**
* Assert the expected value for an entity path alias.
*/
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);
}
/**
* Assert that an alias exists for the given entity's internal path.
*/
public function assertEntityAliasExists(EntityInterface $entity) {
return $this->assertAliasExists(['path' => '/' . $entity->toUrl()->getInternalPath()]);
}
/**
* Assert that the given entity does not have a path alias.
*/
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);
}
/**
* Assert that no alias exists matching the given entity path/alias.
*/
public function assertNoEntityAliasExists(EntityInterface $entity, $alias = NULL) {
$path = ['path' => '/' . $entity->toUrl()->getInternalPath()];
if (!empty($alias)) {
$path['alias'] = $alias;
}
$this->assertNoAliasExists($path);
}
/**
* Assert the expected alias for the given source/language.
*/
public function assertAlias($source, $expected_alias, $langcode = Language::LANGCODE_NOT_SPECIFIED) {
\Drupal::service('path_alias.manager')->cacheClear($source);
$entity_type_manager = \Drupal::entityTypeManager();
if ($entity_type_manager->hasDefinition('path_alias')) {
$entity_type_manager->getStorage('path_alias')->resetCache();
}
$this->assertEquals($expected_alias, \Drupal::service('path_alias.manager')->getAliasByPath($source, $langcode), t("Alias for %source with language '@language' is correct.",
['%source' => $source, '@language' => $langcode]));
}
/**
* Assert that an alias exists for the given conditions.
*/
public function assertAliasExists($conditions) {
$path = $this->loadPathAliasByConditions($conditions);
$this->assertNotEmpty($path, t('Alias with conditions @conditions found.', ['@conditions' => var_export($conditions, TRUE)]));
return $path;
}
/**
* Assert that no alias exists for the given conditions.
*/
public function assertNoAliasExists($conditions) {
$alias = $this->loadPathAliasByConditions($conditions);
$this->assertEmpty($alias, t('Alias with conditions @conditions not found.', ['@conditions' => var_export($conditions, TRUE)]));
}
/**
* Assert that exactly one alias matches the given conditions.
*/
public function assertAliasIsUnique($conditions) {
$storage = \Drupal::entityTypeManager()->getStorage('path_alias');
$query = $storage->getQuery()->accessCheck(FALSE);
foreach ($conditions as $field => $value) {
$query->condition($field, $value);
}
$entities = $storage->loadMultiple($query->execute());
return $this->assertCount(1, $entities);
}
/**
* Delete all path aliases.
*/
public function deleteAllAliases() {
\Drupal::service('pathauto.alias_storage_helper')->deleteAll();
\Drupal::service('path_alias.manager')->cacheClear();
}
/**
* Create a new vocabulary.
*
* @param array $values
* Vocabulary properties.
*
* @return \Drupal\taxonomy\VocabularyInterface
* The Vocabulary object.
*/
public function addVocabulary(array $values = []) {
$name = mb_strtolower($this->randomMachineName(5));
$values += [
'name' => $name,
'vid' => $name,
];
$vocabulary = Vocabulary::create($values);
$vocabulary->save();
return $vocabulary;
}
/**
* Add a new taxonomy term to the given vocabulary.
*/
public function addTerm(VocabularyInterface $vocabulary, array $values = []) {
$values += [
'name' => mb_strtolower($this->randomMachineName(5)),
'vid' => $vocabulary->id(),
];
$term = Term::create($values);
$term->save();
return $term;
}
/**
* Helper for testTaxonomyPattern().
*/
public function assertEntityPattern($entity_type, $bundle, $langcode, $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->assertSame($expected, $pattern->getPattern());
}
/**
* Load a taxonomy term by name.
*/
public function drupalGetTermByName($name, $reset = FALSE) {
if ($reset) {
// @todo implement cache reset.
}
$terms = \Drupal::entityTypeManager()->getStorage('taxonomy_term')->loadByProperties(['name' => $name]);
return !empty($terms) ? reset($terms) : FALSE;
}
}

View File

@@ -0,0 +1,96 @@
<?php
namespace Drupal\Tests\pathauto\Functional;
use Drupal\Tests\BrowserTestBase;
use Drupal\views\Views;
/**
* Tests pathauto user UI integration.
*
* @group pathauto
*/
class PathautoUserWebTest extends BrowserTestBase {
use PathautoTestHelperTrait;
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['pathauto', 'views'];
/**
* Admin user.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Allow other modules to add additional permissions for the admin user.
$permissions = [
'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.
*/
public function testUserEditing() {
// There should be no Pathauto checkbox on user forms.
$this->drupalGet('user/' . $this->adminUser->id() . '/edit');
$this->assertSession()->fieldValueNotEquals('path[0][pathauto]', '');
}
/**
* Test user operations.
*/
public 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->getDisplayName()) {
break;
}
}
$edit = [
'action' => 'pathauto_update_alias_user',
"user_bulk_form[$key]" => TRUE,
];
$this->drupalGet('admin/people');
$this->submitForm($edit, 'Apply to selected items');
$this->assertSession()->pageTextContains('Update URL alias was applied to 1 item.');
$this->assertEntityAlias($account, '/users/' . mb_strtolower($account->getDisplayName()));
$this->assertEntityAlias($this->adminUser, '/user/' . $this->adminUser->id());
}
}

View File

@@ -0,0 +1,236 @@
<?php
namespace Drupal\Tests\pathauto\FunctionalJavascript;
use Drupal\Core\Language\Language;
use Drupal\Core\Language\LanguageInterface;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\pathauto\PathautoState;
use Drupal\Tests\pathauto\Functional\PathautoTestHelperTrait;
/**
* Test pathauto functionality with localization and translation.
*
* @group pathauto
*/
class PathautoLocaleTest extends WebDriverTestBase {
use PathautoTestHelperTrait;
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['node', 'pathauto', 'locale', 'content_translation'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create Article node type.
$this->drupalCreateContentType(['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.
*/
public function testLanguageAliases() {
$this->createPattern('node', '/content/[node:title]');
// Add predefined French language.
ConfigurableLanguage::createFromLangcode('fr')->save();
$node = [
'title' => 'English node',
'langcode' => 'en',
'path' => [[
'alias' => '/english-node',
'pathauto' => FALSE,
]],
];
$node = $this->drupalCreateNode($node);
$english_alias = $this->loadPathAliasByConditions(['alias' => '/english-node', 'langcode' => 'en']);
$this->assertNotEmpty($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->createPathAlias('/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(['id' => $english_alias->id(), 'alias' => '/content/english-node-0']);
// Create a new node with the same title as before but without
// specifying a language.
$node = $this->drupalCreateNode(['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.
*/
public function testLanguagePatterns() {
// Allow other modules to add additional permissions for the admin user.
$permissions = [
'administer pathauto',
'administer url aliases',
'create url aliases',
'bypass node access',
'access content overview',
'administer languages',
'translate any entity',
'administer content translation',
'create content translations',
];
$admin_user = $this->drupalCreateUser($permissions);
$this->drupalLogin($admin_user);
// Add French language.
$edit = [
'predefined_langcode' => 'fr',
];
$this->drupalGet('admin/config/regional/language/add');
$this->submitForm($edit, 'Add language');
$this->enableArticleTranslation();
// Create a pattern for English articles.
$this->drupalGet('admin/config/search/path/patterns/add');
$session = $this->getSession();
$page = $session->getPage();
$page->fillField('type', 'canonical_entities:node');
$this->assertSession()->assertWaitOnAjaxRequest();
sleep(1);
$page->fillField('label', 'English articles');
$this->assertSession()->waitForElementVisible('css', '#edit-label-machine-name-suffix .machine-name-value');
$edit = [
'bundles[article]' => TRUE,
'languages[en]' => TRUE,
'pattern' => '/the-articles/[node:title]',
];
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains('Pattern English articles saved.');
// Create a pattern for French articles.
$this->drupalGet('admin/config/search/path/patterns/add');
$page->fillField('type', 'canonical_entities:node');
$this->assertSession()->assertWaitOnAjaxRequest();
$page->fillField('label', 'French articles');
$this->assertSession()->waitForElementVisible('css', '#edit-label-machine-name-suffix .machine-name-value');
$edit = [
'bundles[article]' => TRUE,
'languages[fr]' => TRUE,
'pattern' => '/les-articles/[node:title]',
];
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains('Pattern French articles saved.');
// Create a node and its translation. Assert aliases.
$edit = [
'title[0][value]' => 'English node',
'langcode[0][value]' => 'en',
];
$this->drupalGet('node/add/article');
$this->submitForm($edit, 'Save');
$node = $this->drupalGetNodeByTitle('English node');
$this->assertAlias('/node/' . $node->id(), '/the-articles/english-node', 'en');
$this->drupalGet('node/' . $node->id() . '/translations');
$this->clickLink('Add');
$edit = [
'title[0][value]' => 'French node',
];
$this->submitForm($edit, 'Save (this translation)');
$this->rebuildContainer();
$this->assertAlias('/node/' . $node->id(), '/les-articles/french-node', 'fr');
// Bulk delete and Bulk generate patterns. Assert aliases.
$this->deleteAllAliases();
// Bulk create aliases.
$edit = [
'update[canonical_entities:node]' => TRUE,
];
$this->drupalGet('admin/config/search/path/update_bulk');
$this->submitForm($edit, 'Update');
$this->assertSession()->waitForText('Generated 2 URL aliases.');
$this->assertAlias('/node/' . $node->id(), '/the-articles/english-node', 'en');
$this->assertAlias('/node/' . $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->assertEquals(LanguageInterface::LANGCODE_NOT_SPECIFIED, $alias['langcode'], 'PathautoGenerator::createEntityAlias() adjusts the alias langcode from Not Applicable to Not Specified.');
// Check that the alias works.
$this->drupalGet('content/test-node');
$this->assertSession()->pageTextContains('Test node');
}
/**
* Enables content translation on articles.
*/
protected function enableArticleTranslation() {
// Enable content translation on articles.
$this->drupalGet('admin/config/regional/content-language');
// Enable translation for node.
$this->assertSession()->fieldExists('entity_types[node]')->check();
// Open details for Content settings in Drupal 10.2.
$nodeSettings = $this->getSession()->getPage()->find('css', '#edit-settings-node summary');
if ($nodeSettings) {
$nodeSettings->click();
}
$this->assertSession()->fieldExists('settings[node][article][translatable]')->check();
$this->assertSession()->fieldExists('settings[node][article][settings][language][language_alterable]')->check();
$this->getSession()->getPage()->pressButton('Save configuration');
}
}

View File

@@ -0,0 +1,218 @@
<?php
namespace Drupal\Tests\pathauto\FunctionalJavascript;
use Drupal\Core\Url;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\pathauto\Entity\PathautoPattern;
use Drupal\Tests\pathauto\Functional\PathautoTestHelperTrait;
/**
* Test basic pathauto functionality.
*
* @group pathauto
*/
class PathautoUiTest extends WebDriverTestBase {
use PathautoTestHelperTrait;
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['pathauto', 'node', 'block'];
/**
* Admin user.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']);
$this->drupalCreateContentType(['type' => 'article']);
// Allow other modules to add additional permissions for the admin user.
$permissions = [
'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);
}
public function testSettingsValidation() {
$this->drupalGet('/admin/config/search/path/settings');
$this->assertSession()->fieldExists('max_length');
$this->assertSession()->elementAttributeContains('css', '#edit-max-length', 'min', '1');
$this->assertSession()->fieldExists('max_component_length');
$this->assertSession()->elementAttributeContains('css', '#edit-max-component-length', 'min', '1');
}
public function testPatternsWorkflow() {
$this->drupalPlaceBlock('local_tasks_block', ['id' => 'local-tasks-block']);
$this->drupalPlaceBlock('local_actions_block');
$this->drupalPlaceBlock('page_title_block');
$this->drupalGet('admin/config/search/path');
$this->assertSession()->elementContains('css', '#block-local-tasks-block', 'Patterns');
$this->assertSession()->elementContains('css', '#block-local-tasks-block', 'Settings');
$this->assertSession()->elementContains('css', '#block-local-tasks-block', 'Bulk generate');
$this->assertSession()->elementContains('css', '#block-local-tasks-block', 'Delete aliases');
$this->drupalGet('admin/config/search/path/patterns');
$this->clickLink('Add Pathauto pattern');
$session = $this->getSession();
$session->getPage()->fillField('type', 'canonical_entities:node');
$this->assertSession()->assertWaitOnAjaxRequest();
$edit = [
'type' => 'canonical_entities:node',
'bundles[page]' => TRUE,
'label' => 'Page pattern',
'pattern' => '[node:title]/[user:name]/[term:name]',
];
$this->submitForm($edit, 'Save');
$this->assertSession()->waitForElementVisible('css', '[name="id"]');
if (version_compare(\Drupal::VERSION, '10.1', '<')) {
$edit += [
'id' => 'page_pattern',
];
$this->submitForm($edit, 'Save');
}
$this->assertSession()->pageTextContains('Path pattern is using the following invalid tokens: [user:name], [term:name].');
$this->assertSession()->pageTextNotContains('The configuration options have been saved.');
// We do not need ID anymore, it is already set in previous step and made a label by browser.
unset($edit['id']);
$edit['pattern'] = '#[node:title]';
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains('The Path pattern is using the following invalid characters: #.');
$this->assertSession()->pageTextNotContains('The configuration options have been saved.');
// Checking whitespace ending of the string.
$edit['pattern'] = '[node:title] ';
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains("The Path pattern doesn't allow the patterns ending with whitespace.");
$this->assertSession()->pageTextNotContains('The configuration options have been saved.');
// Fix the pattern, then check that it gets saved successfully.
$edit['pattern'] = '[node:title]';
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains('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->assertSession()->pageTextContains($title);
$this->assertEntityAlias($node, $alias);
// Edit workflow, set a new label and weight for the pattern.
$this->drupalGet('/admin/config/search/path/patterns');
$session->getPage()->pressButton('Show row weights');
$this->submitForm(['entities[page_pattern][weight]' => '4'], 'Save');
$session->getPage()->find('css', '.dropbutton-toggle > button')->press();
$this->clickLink('Edit');
$destination_query = ['query' => ['destination' => Url::fromRoute('entity.pathauto_pattern.collection')->toString()]];
$address = Url::fromRoute('entity.pathauto_pattern.edit_form', ['pathauto_pattern' => 'page_pattern'], [$destination_query]);
$this->assertSession()->addressEquals($address);
$this->assertSession()->fieldValueEquals('pattern', '[node:title]');
$this->assertSession()->fieldValueEquals('label', 'Page pattern');
$this->assertSession()->checkboxChecked('edit-status');
$this->assertSession()->linkExists('Delete');
$edit = ['label' => 'Test'];
$this->drupalGet('/admin/config/search/path/patterns/page_pattern');
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains('Pattern Test saved.');
// Check that the pattern weight did not change.
$this->assertSession()->optionExists('edit-entities-page-pattern-weight', '4');
$this->drupalGet('/admin/config/search/path/patterns/page_pattern/duplicate');
$session->getPage()->pressButton('Edit');
$edit = ['label' => 'Test Duplicate', 'id' => 'page_pattern_test_duplicate'];
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains('Pattern Test Duplicate saved.');
PathautoPattern::load('page_pattern_test_duplicate')->delete();
// Disable workflow.
$this->drupalGet('/admin/config/search/path/patterns');
$session->getPage()->find('css', '.dropbutton-toggle > button')->press();
$this->assertSession()->linkNotExists('Enable');
$this->clickLink('Disable');
$this->assertSession()->addressEquals('/admin/config/search/path/patterns/page_pattern/disable');
$this->submitForm([], 'Disable');
$this->assertSession()->pageTextContains('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->assertSession()->linkNotExists('Disable');
$this->clickLink('Enable');
$address = Url::fromRoute('entity.pathauto_pattern.enable', ['pathauto_pattern' => 'page_pattern'], [$destination_query]);
$this->assertSession()->addressEquals($address);
$this->submitForm([], 'Enable');
$this->assertSession()->pageTextContains('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');
$session->getPage()->find('css', '.dropbutton-toggle > button')->press();
$this->clickLink('Delete');
$this->assertSession()->assertWaitOnAjaxRequest();
if (version_compare(\Drupal::VERSION, '10.1', '>=')) {
$this->assertNotEmpty($this->assertSession()->waitForElementVisible('css', '#drupal-modal'));
$this->assertSession()->elementContains('css', '#drupal-modal', 'This action cannot be undone.');
$this->assertSession()->elementExists('css', '.ui-dialog-buttonpane')->pressButton('Delete');
}
else {
$address = Url::fromRoute('entity.pathauto_pattern.delete_form', ['pathauto_pattern' => 'page_pattern'], [$destination_query]);
$this->assertSession()->addressEquals($address);
$this->submitForm([], 'Delete');
}
$this->assertSession()->pageTextContains('The pathauto pattern Test has been deleted.');
$this->assertEmpty(PathautoPattern::load('page_pattern'));
}
}

View File

@@ -0,0 +1,131 @@
<?php
namespace Drupal\Tests\pathauto\Kernel;
use Drupal\Component\Serialization\PhpSerialize;
use Drupal\Component\Utility\Crypt;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\KeyValueStore\KeyValueDatabaseFactory;
use Drupal\KernelTests\KernelTestBase;
use Drupal\pathauto\PathautoState;
use Drupal\pathauto_string_id_test\Entity\PathautoStringIdTest;
use Drupal\Tests\pathauto\Functional\PathautoTestHelperTrait;
/**
* Tests auto-aliasing of entities that use string IDs.
*
* @group pathauto
*/
class PathautoEntityWithStringIdTest extends KernelTestBase {
use PathautoTestHelperTrait;
/**
* The alias type plugin instance.
*
* @var \Drupal\pathauto\AliasTypeBatchUpdateInterface
*/
protected $aliasType;
/**
* {@inheritdoc}
*/
protected static $modules = [
'system',
'user',
'field',
'token',
'path',
'path_alias',
'pathauto',
'pathauto_string_id_test',
];
/**
* {@inheritdoc}
*/
public function register(ContainerBuilder $container) {
parent::register($container);
// Kernel tests are using the 'keyvalue.memory' store but we want to test
// against the 'keyvalue.database'.
$container
->register('keyvalue.database', KeyValueDatabaseFactory::class)
->addArgument(new PhpSerialize())
->addArgument($container->get('database'))
->addTag('persist');
$container->setAlias('keyvalue', 'keyvalue.database');
}
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installConfig(['system', 'pathauto']);
$this->installEntitySchema('path_alias');
$this->installEntitySchema('pathauto_string_id_test');
$this->createPattern('pathauto_string_id_test', '/[pathauto_string_id_test:name]');
/** @var \Drupal\pathauto\AliasTypeManager $alias_type_manager */
$alias_type_manager = $this->container->get('plugin.manager.alias_type');
$this->aliasType = $alias_type_manager->createInstance('canonical_entities:pathauto_string_id_test');
}
/**
* Test aliasing entities with long string ID.
*
* @dataProvider entityWithStringIdProvider
*
* @param string|int $id
* The entity ID
* @param string $expected_key
* The expected key for 'pathauto_state.*' collections.
*/
public function testEntityWithStringId($id, $expected_key) {
$entity = PathautoStringIdTest::create([
'id' => $id,
'name' => $name = $this->randomMachineName(),
]);
$entity->save();
// Check that the path was generated.
$this->assertEntityAlias($entity, mb_strtolower("/$name"));
// Check that the path auto state was saved with the expected key.
$value = \Drupal::keyValue('pathauto_state.pathauto_string_id_test')->get($expected_key);
$this->assertEquals(PathautoState::CREATE, $value);
$context = [];
// Batch delete uses the key-value store collection 'pathauto_state.*. We
// test that after a bulk delete all aliases are removed. Running only once
// the batch delete process is enough as the batch size is 100.
$this->aliasType->batchDelete($context);
// Check that the paths were removed on batch delete.
$this->assertNoEntityAliasExists($entity, "/$name");
}
/**
* Provides test cases for ::testEntityWithStringId().
*
* @see \Drupal\Tests\pathauto\Kernel\PathautoEntityWithStringIdTest::testEntityWithStringId()
*/
public function entityWithStringIdProvider() {
return [
'ascii with less or equal 128 chars' => [
str_repeat('a', 128), str_repeat('a', 128),
],
'ascii with over 128 chars' => [
str_repeat('a', 191), Crypt::hashBase64(str_repeat('a', 191)),
],
'non-ascii with less or equal 128 chars' => [
str_repeat('社', 128), Crypt::hashBase64(str_repeat('社', 128)),
],
'non-ascii with over 128 chars' => [
str_repeat('社', 191), Crypt::hashBase64(str_repeat('社', 191)),
],
'simulating an integer id' => [
123, '123',
],
];
}
}

View File

@@ -0,0 +1,699 @@
<?php
namespace Drupal\Tests\pathauto\Kernel;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Component\Utility\Html;
use Drupal\Core\Language\Language;
use Drupal\Core\Language\LanguageInterface;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\KernelTests\KernelTestBase;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
use Drupal\pathauto\PathautoGeneratorInterface;
use Drupal\pathauto\PathautoState;
use Drupal\taxonomy\Entity\Term;
use Drupal\taxonomy\Entity\Vocabulary;
use Drupal\Tests\pathauto\Functional\PathautoTestHelperTrait;
use Drupal\user\Entity\User;
/**
* Unit tests for Pathauto functions.
*
* @group pathauto
*/
class PathautoKernelTest extends KernelTestBase {
use PathautoTestHelperTrait;
/**
* Modules.
*
* @var string[]
*/
protected static $modules = [
'system',
'field',
'text',
'user',
'node',
'path',
'path_alias',
'pathauto',
'pathauto_custom_punctuation_test',
'taxonomy',
'token',
'filter',
'language',
];
/**
* The current user.
*
* @var \Drupal\user\Entity\UserInterface
*/
protected $currentUser;
/**
* Node pattern.
*
* @var \Drupal\pathauto\PathautoPatternInterface
*/
protected $nodePattern;
/**
* User pattern.
*
* @var \Drupal\pathauto\PathautoPatternInterface
*/
protected $userPattern;
/**
* {@inheritdoc}
*/
public function setUp(): void {
parent::setup();
$this->installEntitySchema('user');
$this->installEntitySchema('node');
$this->installEntitySchema('taxonomy_term');
if ($this->container->get('entity_type.manager')->hasDefinition('path_alias')) {
$this->installEntitySchema('path_alias');
}
$this->installConfig([
'pathauto',
'taxonomy',
'system',
'node',
]);
ConfigurableLanguage::createFromLangcode('fr')->save();
$this->installSchema('node', ['node_access']);
$this->installSchema('system', ['sequences']);
$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(['name' => $this->randomMachineName()]);
$this->currentUser->save();
}
/**
* Test _pathauto_get_schema_alias_maxlength().
*/
public function testGetSchemaAliasMaxLength() {
$this->assertSame(\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 = [
[
'entity' => 'node',
'values' => [
'title' => 'Article fr',
'type' => 'article',
'langcode' => 'fr',
],
'expected' => '/article/[node:title]',
],
[
'entity' => 'node',
'values' => [
'title' => 'Article en',
'type' => 'article',
'langcode' => 'en',
],
'expected' => '/article/en/[node:title]',
],
[
'entity' => 'node',
'values' => [
'title' => 'Article und',
'type' => 'article',
'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
],
'expected' => '/article/[node:title]',
],
[
'entity' => 'node',
'values' => [
'title' => 'Page',
'type' => 'page',
],
'expected' => '/[node:title]',
],
[
'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->assertSame($actual->getPattern(), $test['expected'], new FormattableMarkup("Correct pattern returned for @entity_type with @values", [
'@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(['pathauto']);
// Add a custom setting for the copyright symbol defined in
// pathauto_custom_punctuation_test_pathauto_punctuation_chars_alter().
$this->config('pathauto.settings')->set('punctuation.copyright', PathautoGeneratorInterface::PUNCTUATION_REMOVE);
\Drupal::service('pathauto.generator')->resetCaches();
$tests = [];
// 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';
// Transliteration of special chars that are converted to punctuation.
$tests['© “Drupal”'] = 'drupal';
foreach ($tests as $input => $expected) {
$output = \Drupal::service('pathauto.alias_cleaner')->cleanString($input);
$this->assertEquals($expected, $output, new FormattableMarkup("Drupal::service('pathauto.alias_cleaner')->cleanString('@input') expected '@expected', actual '@output'", [
'@input' => $input,
'@expected' => $expected,
'@output' => $output,
]));
}
}
/**
* Test pathauto_clean_alias().
*/
public function testCleanAlias() {
$tests = [];
$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->assertEquals($expected, $output, new FormattableMarkup("Drupal::service('pathauto.generator')->cleanAlias('@input') expected '@expected', actual '@output'", [
'@input' => $input,
'@expected' => $expected,
'@output' => $output,
]));
}
}
/**
* Test pathauto_path_delete_multiple().
*/
public function testPathDeleteMultiple() {
$this->createPathAlias('/node/1', '/node-1-alias');
$this->createPathAlias('/node/1/view', '/node-1-alias/view');
$this->createPathAlias('/node/1', '/node-1-alias-en', 'en');
$this->createPathAlias('/node/1', '/node-1-alias-fr', 'fr');
$this->createPathAlias('/node/2', '/node-2-alias');
$this->createPathAlias('/node/10', '/node-10-alias');
\Drupal::service('pathauto.alias_storage_helper')->deleteBySourcePrefix('/node/1');
$this->assertNoAliasExists(['path' => "/node/1"]);
$this->assertNoAliasExists(['path' => "/node/1/view"]);
$this->assertAliasExists(['path' => "/node/2"]);
$this->assertAliasExists(['path' => "/node/10"]);
}
/**
* Test the different update actions in createEntityAlias().
*
* Tests \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(['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(['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([
'path' => '/' . $node->toUrl()->getInternalPath(),
'alias' => '/content/second-title',
]);
// Confirm that aliases are not duplicated when entities are re-saved.
$node->save();
$this->assertEntityAlias($node, '/content/third-title');
$this->assertAliasIsUnique([
'path' => '/' . $node->toUrl()->getInternalPath(),
'alias' => '/content/third-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(['alias' => '/content/third-title']);
// The older second alias is not deleted yet.
$older_path = $this->assertAliasExists([
'path' => '/' . $node->toUrl()->getInternalPath(),
'alias' => '/content/second-title',
]);
\Drupal::service('entity_type.manager')->getStorage('path_alias')->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(['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 createEntityAlias().
*
* 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 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, ['name' => 'Parent term']);
$this->assertEntityAlias($term1, '/parent-term');
$term2 = $this->addTerm($vocab, [
'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.
*/
public 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' . mb_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 = \Drupal::service('entity_display.repository')->getViewDisplay('taxonomy_term', 'tags');
$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, '/' . mb_strtolower($parent->getName() . '/' . $child->$fieldname->value));
// Re-saving the parent term should not modify the child term's alias.
$parent->save();
$this->assertEntityAlias($child, '/' . mb_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.
$this->addVocabulary(['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');
}
/**
* Test that aliases matching existing paths are not generated.
*/
public 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(['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.
*/
public function testProgrammaticEntityCreation() {
$node = $this->drupalCreateNode([
'title' => 'Test node',
'path' => ['pathauto' => TRUE],
]);
$this->assertEntityAlias($node, '/content/test-node');
// Check the case when the pathauto widget is hidden, so it can not populate
// the 'pathauto' property, and
// \Drupal\path\Plugin\Field\FieldType\PathFieldItemList::computeValue()
// populates the 'path' field with a 'langcode' property, for example during
// an AJAX call on the entity form.
$node = $this->drupalCreateNode([
'title' => 'Test node 2',
'path' => ['langcode' => 'en'],
]);
$this->assertEntityAlias($node, '/content/test-node-2');
$this->createPattern('taxonomy_term', '/[term:vocabulary]/[term:name]');
$vocabulary = $this->addVocabulary(['name' => 'Tags']);
$term = $this->addTerm($vocabulary, [
'name' => 'Test term',
'path' => ['pathauto' => TRUE],
]);
$this->assertEntityAlias($term, '/tags/test-term');
$edit['name'] = 'Test user';
$edit['mail'] = 'test-user@example.com';
$edit['pass'] = \Drupal::service('password_generator')->generate();
$edit['path'] = ['pathauto' => TRUE];
$edit['status'] = 1;
$account = User::create($edit);
$account->save();
$this->assertEntityAlias($account, '/users/test-user');
}
/**
* Tests word safe alias truncating.
*/
public function testPathAliasUniquifyWordsafe() {
$this->config('pathauto.settings')
->set('max_length', 26)
->save();
$node_1 = $this->drupalCreateNode([
'title' => 'thequick brownfox jumpedover thelazydog',
'type' => 'page',
]);
$node_2 = $this->drupalCreateNode([
'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.
*/
public 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 generates 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');
}
/**
* Tests that the pathauto state property gets set to CREATED for new nodes.
*
* In some cases, this can trigger $node->path to be set up with no default
* value for the pathauto property.
*/
public function testCreateNodeWhileAccessingPath() {
$node = Node::create([
'type' => 'article',
'title' => 'TestAlias',
]);
$node->path->langcode;
$node->save();
$this->assertEntityAlias($node, '/content/testalias');
}
/**
* 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 = []) {
// Populate defaults array.
$settings += [
'title' => $this->randomMachineName(8),
'type' => 'page',
];
$node = Node::create($settings);
$node->save();
return $node;
}
}

View File

@@ -0,0 +1,132 @@
<?php
namespace Drupal\Tests\pathauto\Kernel;
use Drupal\Component\Render\FormattableMarkup;
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
*/
protected static $modules = ['system', 'token', 'path_alias', 'pathauto'];
public function testPathautoTokens() {
$this->installConfig(['pathauto']);
$array = [
'test first arg',
'The Array / value',
];
$tokens = [
'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, []);
$this->assertEquals('test-first-arg/array-value', $replacements['[array:join-path]']);
// Test additional token cleaning and its configuration.
$safe_tokens = $this->config('pathauto.settings')->get('safe_tokens');
$safe_tokens[] = 'safe';
$this->config('pathauto.settings')
->set('safe_tokens', $safe_tokens)
->save();
$safe_tokens = [
'[example:path]',
'[example:url]',
'[example:url-brief]',
'[example:login-url]',
'[example:login-url:relative]',
'[example:url:relative]',
'[example:safe]',
'[safe:example]',
];
$unsafe_tokens = [
'[example:path_part]',
'[example:something_url]',
'[example:unsafe]',
];
foreach ($safe_tokens as $token) {
$replacements = [
$token => 'this/is/a/path',
];
$alias_cleaner->cleanTokenValues($replacements);
$this->assertEquals('this/is/a/path', $replacements[$token], "Token $token cleaned.");
}
foreach ($unsafe_tokens as $token) {
$replacements = [
$token => 'This is not a / path',
];
$alias_cleaner->cleanTokenValues($replacements);
$this->assertEquals('not-path', $replacements[$token], "Token $token not cleaned.");
}
}
/**
* Function copied from TokenTestHelper::assertTokens().
*/
public function assertTokens($type, array $data, array $tokens, array $options = []) {
$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]), new FormattableMarkup("Token value for @token was not generated.", [
'@type' => $type,
'@token' => $token,
]));
}
elseif (!isset($replacements[$token])) {
$this->fail(new FormattableMarkup("Token value for @token was not generated.", [
'@type' => $type,
'@token' => $token,
]));
}
elseif (!empty($options['regex'])) {
$this->assertTrue(preg_match('/^' . $expected . '$/', $replacements[$token]), new FormattableMarkup("Token value for @token was '@actual', matching regular expression pattern '@expected'.", [
'@type' => $type,
'@token' => $token,
'@actual' => $replacements[$token],
'@expected' => $expected,
]));
}
else {
$this->assertSame($expected, $replacements[$token], new FormattableMarkup("Token value for @token was '@actual', expected value '@expected'.", [
'@type' => $type,
'@token' => $token,
'@actual' => $replacements[$token],
'@expected' => $expected,
]));
}
}
return $replacements;
}
public function mapTokenNames($type, array $tokens = []) {
$return = [];
foreach ($tokens as $token) {
$return[$token] = "[$type:$token]";
}
return $return;
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace Drupal\Tests\pathauto\Unit;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Session\AccountInterface;
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(): void {
parent::setUp();
$config_factory = $this->getConfigFactoryStub(['pathauto.settings' => ['verbose' => TRUE]]);
$account = $this->createMock(AccountInterface::class);
$account->expects($this->once())
->method('hasPermission')
->withAnyParameters()
->willReturn(TRUE);
$messenger = $this->createMock(MessengerInterface::class);
$this->messenger = new VerboseMessenger($config_factory, $account, $messenger);
}
/**
* 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");
}
}