Compare commits

..
38 Commits
Author SHA1 Message Date
Bachir Soussi Chiadmi 4cd308e721 added strip-debug to gulp to remove any console.log in production minified script 2018-04-04 16:20:07 +02:00
Bachir Soussi Chiadmi cf1ea6aea7 definitly fix the history bug due to aggregation by loading history in head 2018-04-04 16:08:22 +02:00
Bachir Soussi Chiadmi 52f5a24742 trying to fix redirect to home bug : replaced cookie by localStorage, seems to work 2018-04-04 15:16:31 +02:00
Bachir Soussi Chiadmi 4529db50a8 trying to fix redirect to home bug : replaced cookie by localStorage 2018-04-04 15:13:35 +02:00
Bachir Soussi Chiadmi ca50cab7d7 trying to fix redirect to home bug 2018-04-04 14:59:40 +02:00
Bachir Soussi Chiadmi dc315a0e24 trying to fix redirect to home bug 2018-04-04 14:51:47 +02:00
Bachir Soussi Chiadmi 7b11b8b1f0 trying to fix redirect to home bug 2018-04-04 14:43:47 +02:00
Bachir Soussi Chiadmi 639df7c950 removed custom module libraries dependencies in edlptheme library 2018-04-04 14:38:47 +02:00
Bachir Soussi Chiadmi b5812b2198 bug fix 2018-04-04 13:13:44 +02:00
Bachir Soussi Chiadmi 6263b674b8 refactoring (gulp and libraries) + some fixing 2018-04-04 12:58:58 +02:00
Bachir Soussi Chiadmi c8f54cb9f1 gulp is now always minifing js and css 2018-04-03 23:25:03 +02:00
Bachir Soussi Chiadmi 1a6f571e15 added config_ignore contrib module to ignore performance and devel config 2018-04-03 23:06:38 +02:00
Bachir Soussi Chiadmi cf1885a46b readded system.performance as missing config are deleted on import 2018-04-03 22:58:38 +02:00
Bachir Soussi Chiadmi 8865845e10 gitignore system.performance config file 2018-04-03 22:49:42 +02:00
Bachir Soussi Chiadmi 284346c499 made cachable edlp_agenda home 2018-04-03 22:42:56 +02:00
Bachir Soussi Chiadmi 6aa244cf17 made cachable edlp_productions home 2018-04-03 22:32:17 +02:00
Bachir Soussi Chiadmi 71eec82126 production menu tree reference in ajax response 2018-04-03 22:24:31 +02:00
Bachir Soussi Chiadmi 94501a2aad cleaned edlp_theme bower dependency and libraries 2018-04-03 22:22:21 +02:00
Bachir Soussi Chiadmi f741a01bbe improved edlp_corpus cache invalidation 2018-04-03 22:16:36 +02:00
Bachir Soussi Chiadmi c794046eca made edlp_ajax response cachable 2018-04-03 22:14:53 +02:00
Bachir Soussi Chiadmi 3c72614b3f activated cache for corpus map in edlp_corpus module 2018-04-03 18:05:41 +02:00
Bachir Soussi Chiadmi 6ce6ded0f8 fixed ajax redirecting for non-front pages with drupal performances activated 2018-04-03 17:09:09 +02:00
Bachir Soussi Chiadmi 99b14adf07 fixed active trail ajax links 2018-04-03 16:14:59 +02:00
Bachir Soussi Chiadmi ca2c673cce fixed masonry for productions 2018-04-03 16:05:02 +02:00
Bachir Soussi Chiadmi 4e01b68f5b history nav is now ok 2018-04-03 15:00:13 +02:00
Bachir Soussi Chiadmi f3fc776b71 history is bugging, added todos in code 2018-03-30 00:04:42 +02:00
Bachir Soussi Chiadmi 4b93f24f26 Merge branch 'master' into history-nav 2018-03-29 23:59:01 +02:00
Bachir Soussi Chiadmi 51b6f477dd history nav is working, remains flickering problems 2018-03-29 23:58:36 +02:00
Bachir Soussi Chiadmi 4921e8189b activated pathauto for url aliases 2018-03-29 23:57:27 +02:00
Bachir Soussi Chiadmi 33a04c55b1 fix entries stay hightlighted if doc is currently playing 2018-03-28 13:27:38 +02:00
Bachir Soussi Chiadmi 2969c2544d entries stay hightlighted if doc is currently playing 2018-03-28 13:24:09 +02:00
dev 69b64b7881 beta config updated from inline 2018-03-28 13:03:33 +02:00
Bachir Soussi Chiadmi 9b45cbb77c fix masonry on productions and home 2018-03-28 12:54:27 +02:00
Bachir Soussi Chiadmi f4ad71e57a removed body from production home nodes 2018-03-28 12:07:47 +02:00
Bachir Soussi Chiadmi 4b986903cb imporved home display (masonry) and fixed visibility of production footer menu 2018-03-28 12:03:04 +02:00
Bachir Soussi Chiadmi 0bf219cf35 home is working now like the productions home page 2018-03-28 11:44:36 +02:00
Bachir Soussi Chiadmi 0477bee1d5 ecarté article menu 2018-03-28 10:39:49 +02:00
Bachir Soussi Chiadmi 541f14ac6d removed ajax-loading thermometre 2018-03-28 10:31:48 +02:00
397 changed files with 42008 additions and 5253 deletions
+3
View File
@@ -44,3 +44,6 @@ dbbackups
*.mysql
*.tar.gz
*.zip
# this is not working as missing config a simply delete on import
# sites/default/config/sync/system.performance.yml
@@ -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.
@@ -0,0 +1,83 @@
#Pathauto
If you are developing for this module, have a look at pathauto.api.php.
##Description
The Pathauto module provides support functions for other modules to
automatically generate aliases based on appropriate criteria, with a
central settings path for site administrators.
Implementations are provided for core entity types: content, taxonomy terms,
and users (including blogs and forum pages).
Pathauto also provides a way to delete large numbers of aliases. This feature
is available at Administer > Configuration > Search and metadata > URL aliases >
Delete aliases.
##Benefits
Besides making the page address more reflective of its content than
"node/138", it's important to know that modern search engines give
heavy weight to search terms which appear in a page's URL. By
automatically using keywords based directly on the page content in the URL,
relevant search engine hits for your page can be significantly
enhanced.
##Notices
Pathauto just adds URL aliases to content, users, and taxonomy terms.
Because it's an alias, the standard Drupal URL (for example node/123 or
taxonomy/term/1) will still function as normal. If you have external links
to your site pointing to standard Drupal URLs, or hardcoded links in a module,
template, content or menu which point to standard Drupal URLs it will bypass
the alias set by Pathauto.
There are reasons you might not want two URLs for the same content on your
site. If this applies to you, please note that you will need to update any
hard coded links in your content or blocks.
If you use the "system path" (i.e. node/10) for menu items and settings like
that, Drupal will replace it with the url_alias.
For external links, you might want to consider the Path Redirect or
Global Redirect modules, which allow you to set forwarding either per item or
across the site to your aliased URLs.
###URLs (not) Getting Replaced With Aliases:
Please bear in mind that only URLs passed through Drupal's Drupal's URL and
Link APIs will be replaced with their aliases during page output. If
a module or your template contains hardcoded links, such as
'href="node/$node->nid"', those won't get replaced with their corresponding
aliases.
## Disabling Pathauto for a specific content type (or taxonomy)
When the pattern for a content type is left blank, the default pattern will be
used. But if the default pattern is also blank, Pathauto will be disabled
for that content type.
## Installing Pathauto
1. Install the module as normal, note that there are two dependencies.
2. Configure the module at admin/config/search/path/patterns - add a new pattern by creating by clicking "Add Pathauto pattern".
3. Fill out "Path pattern" with fx [node:title], choose which content types this applies to, give it a label (the name) and save it.
4. When you save new content from now on, it should automatically be assigned an alternative URL
##Credits:
The original module combined the functionality of Mike Ryan's autopath with
Tommy Sundstrom's path_automatic.
Significant enhancements were contributed by jdmquin @ www.bcdems.net.
Matt England added the tracker support (tracker support has been removed in
recent changes).
Other suggestions and patches contributed by the Drupal community.
Current maintainers:
- Dave Reid - http://www.davereid.net
- Greg Knaddison - http://www.knaddison.com
- Mike Ryan - http://mikeryan.name
- Frederik 'Freso' S. Olesen - http://freso.dk
@@ -0,0 +1,13 @@
enabled_entity_types:
- user
punctuation:
hyphen: 1
verbose : FALSE
separator : '-'
max_length : 100
max_component_length: 100
transliterate : TRUE
reduce_ascii : FALSE
case : TRUE
ignore_words : 'a, an, as, at, before, but, by, for, from, is, in, into, like, of, off, on, onto, per, since, than, the, this, that, to, up, via, with'
update_action : 2
@@ -0,0 +1,9 @@
id: pathauto_update_alias_node
label: 'Update URL alias'
status: true
langcode: en
type: node
plugin: pathauto_update_alias
dependencies:
module:
- node
@@ -0,0 +1,9 @@
id: pathauto_update_alias_user
label: 'Update URL alias'
status: true
langcode: en
type: user
plugin: pathauto_update_alias
dependencies:
module:
- user
@@ -0,0 +1,36 @@
pathauto.settings:
type: config_object
mapping:
enabled_entity_types:
label: Enabled entity types
type: sequence
sequence:
type: string
punctuation:
type: sequence
sequence:
type: integer
verbose:
type: boolean
separator:
type: string
max_length:
type: integer
max_component_length:
type: integer
transliterate:
type: boolean
reduce_ascii:
type: boolean
ignore_words:
type: string
case:
type: boolean
ignore_words:
type: string
update_action:
type: integer
action.configuration.pathauto_update_alias:
type: action_configuration_default
label: 'Update URL alias'
@@ -0,0 +1,40 @@
pathauto.pattern.*:
type: config_entity
label: 'Pathauto pattern config'
mapping:
id:
type: string
label: 'ID'
label:
type: label
label: 'Label'
uuid:
type: string
type:
type: string
label: 'Pattern type'
pattern:
type: string
label: 'Pattern'
selection_criteria:
type: sequence
label: 'Selection criteria'
sequence:
type: condition.plugin.[id]
label: 'Selection condition'
selection_logic:
type: string
label: 'Selection logic'
weight:
type: integer
label: 'Weight'
relationships:
type: sequence
label: 'Context definitions'
sequence:
- type: mapping
label: 'Relationship'
mapping:
label:
type: label
label: 'Label'
@@ -0,0 +1,160 @@
<?php
/**
* @file
* Documentation for pathauto API.
*/
use Drupal\Core\Language\Language;
/**
* @todo Update for 8.x-1.x
*
* It may be helpful to review some examples of integration from
* pathauto.pathauto.inc.
*
* Pathauto works by using tokens in path patterns. Thus the simplest
* integration is just to provide tokens. Token support is provided by Drupal
* core. To provide additional token from your module, implement the following
* hooks:
*
* hook_tokens() - http://api.drupal.org/api/function/hook_tokens
* hook_token_info() - http://api.drupal.org/api/function/hook_token_info
*
* If you wish to provide pathauto integration for custom paths provided by your
* module, there are a few steps involved.
*
* 1. hook_pathauto()
* Provide information required by pathauto for the settings form as well as
* bulk generation. See the documentation for hook_pathauto() for more
* details.
*
* 2. pathauto_create_alias()
* At the appropriate time (usually when a new item is being created for
* which a generated alias is desired), call pathauto_create_alias() with the
* appropriate parameters to generate and create the alias. See the user,
* taxonomy, and node hook implementations in pathauto.module for examples.
* Also see the documentation for pathauto_create_alias().
*
* 3. pathauto_path_delete_all()
* At the appropriate time (usually when an item is being deleted), call
* pathauto_path_delete_all() to remove any aliases that were created for the
* content being removed. See the documentation for
* pathauto_path_delete_all() for more details.
*
* 4. hook_path_alias_types()
* For modules that create new types of content that can be aliased with
* pathauto, a hook implementation is needed to allow the user to delete them
* all at once. See the documentation for hook_path_alias_types() below for
* more information.
*
* There are other integration points with pathauto, namely alter hooks that
* allow you to change the data used by pathauto at various points in the
* process. See the below hook documentation for details.
*/
/**
* Alter pathauto alias type definitions.
*
* @param array &$definitions
* Alias type definitions.
*/
function hook_path_alias_types_alter(array &$definitions) {
}
/**
* Determine if a possible URL alias would conflict with any existing paths.
*
* Returning TRUE from this function will trigger pathauto_alias_uniquify() to
* generate a similar URL alias with a suffix to avoid conflicts.
*
* @param string $alias
* The potential URL alias.
* @param string $source
* The source path for the alias (e.g. 'node/1').
* @param string $langcode
* The language code for the alias (e.g. 'en').
*
* @return bool
* TRUE if $alias conflicts with an existing, reserved path, or FALSE/NULL if
* it does not match any reserved paths.
*
* @see pathauto_alias_uniquify()
*/
function hook_pathauto_is_alias_reserved($alias, $source, $langcode) {
// Check our module's list of paths and return TRUE if $alias matches any of
// them.
return (bool) \Drupal::database()->query("SELECT 1 FROM {mytable} WHERE path = :path", [':path' => $alias])->fetchField();
}
/**
* Alter the pattern to be used before an alias is generated by Pathauto.
*
* This hook will only be called if a default pattern is configured (on
* admin/config/search/path/patterns).
*
* @param string $pattern
* The alias pattern for Pathauto to pass to token_replace() to generate the
* URL alias.
* @param array $context
* An associative array of additional options, with the following elements:
* - 'module': The module or entity type being aliased.
* - 'op': A string with the operation being performed on the object being
* aliased. Can be either 'insert', 'update', 'return', or 'bulkupdate'.
* - 'source': A string of the source path for the alias (e.g. 'node/1').
* - 'data': An array of keyed objects to pass to token_replace().
* - 'type': The sub-type or bundle of the object being aliased.
* - 'language': A string of the language code for the alias (e.g. 'en').
* This can be altered by reference.
*/
function hook_pathauto_pattern_alter(&$pattern, array $context) {
// Switch out any [node:created:*] tokens with [node:updated:*] on update.
if ($context['module'] == 'node' && ($context['op'] == 'update')) {
$pattern = preg_replace('/\[node:created(\:[^]]*)?\]/', '[node:updated$1]', $pattern);
}
}
/**
* Alter Pathauto-generated aliases before saving.
*
* @param string $alias
* The automatic alias after token replacement and strings cleaned.
* @param array $context
* An associative array of additional options, with the following elements:
* - 'module': The module or entity type being aliased.
* - 'op': A string with the operation being performed on the object being
* aliased. Can be either 'insert', 'update', 'return', or 'bulkupdate'.
* - 'source': A string of the source path for the alias (e.g. 'node/1').
* This can be altered by reference.
* - 'data': An array of keyed objects to pass to token_replace().
* - 'type': The sub-type or bundle of the object being aliased.
* - 'language': A string of the language code for the alias (e.g. 'en').
* This can be altered by reference.
* - 'pattern': A string of the pattern used for aliasing the object.
*/
function hook_pathauto_alias_alter(&$alias, array &$context) {
// Add a suffix so that all aliases get saved as 'content/my-title.html'
$alias .= '.html';
// Force all aliases to be saved as language neutral.
$context['language'] = Language::LANGCODE_NOT_SPECIFIED;
}
/**
* Alter the list of punctuation characters for Pathauto control.
*
* @param $punctuation
* An array of punctuation to be controlled by Pathauto during replacement
* keyed by punctuation name. Each punctuation record should be an array
* with the following key/value pairs:
* - value: The raw value of the punctuation mark.
* - name: The human-readable name of the punctuation mark. This must be
* translated using t() already.
*/
function hook_pathauto_punctuation_chars_alter(array &$punctuation) {
// Add the trademark symbol.
$punctuation['trademark'] = array('value' => '™', 'name' => t('Trademark symbol'));
// Remove the dollar sign.
unset($punctuation['dollar']);
}
@@ -0,0 +1,20 @@
name : 'Pathauto'
description : 'Provides a mechanism for modules to automatically generate aliases for the content they manage.'
# core: 8.x
type: module
dependencies:
- ctools:ctools
- drupal:path
- token:token
configure: entity.pathauto_pattern.collection
recommends:
- redirect:redirect
# Information added by Drupal.org packaging script on 2018-03-03
version: '8.x-1.1'
core: '8.x'
project: 'pathauto'
datestamp: 1520092688
@@ -0,0 +1,301 @@
<?php
/**
* @file
* Install, update, and uninstall functions for Pathauto.
*
* @ingroup pathauto
*/
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Core\Plugin\Context\Context;
use Drupal\Core\Plugin\Context\ContextDefinition;
use Drupal\pathauto\Entity\PathautoPattern;
/**
* Implements hook_install().
*/
function pathauto_install() {
// Set the weight to 1
module_set_weight('pathauto', 1);
// Ensure the url_alias table exists.
_pathauto_ensure_url_alias_table_exists();
}
/**
* Helper function to ensure the url_alias table exists.
*
* Only necessary on Drupal 8.1.x.
*
* @see https://www.drupal.org/node/2704821
*/
function _pathauto_ensure_url_alias_table_exists() {
$alias_storage = \Drupal::service('path.alias_storage');
if (method_exists($alias_storage, 'schemaDefinition')) {
$database_schema = \Drupal::database()->schema();
if (!$database_schema->tableExists($alias_storage::TABLE)) {
$schema_definition = $alias_storage->schemaDefinition();
$database_schema->createTable($alias_storage::TABLE, $schema_definition);
}
}
}
/**
* Updates pathauto widgets to use the path widget ID.
*/
function pathauto_update_8001() {
// Replace values in the 'entity.definitions.installed' keyvalue collection.
$collection = \Drupal::service('keyvalue')->get('entity.definitions.installed');
foreach ($collection->getAll() as $key => $definitions) {
if (!is_array($definitions) || empty($definitions['path'])) {
continue;
}
// Retrieve and change path base field definition.
$path_definition = $definitions['path'];
if (($options = $path_definition->getDisplayOptions('form')) && $options['type'] = 'pathauto') {
$options['type'] = 'path';
$path_definition->setDisplayOptions('form', $options);
// Save the new value.
$collection->set($key, $definitions);
}
}
foreach (EntityFormDisplay::loadMultiple() as $form_display) {
if ($component = $form_display->getComponent('path')) {
if (isset($component['type']) && $component['type'] == 'pathauto') {
$component['type'] = 'path';
$form_display->setComponent('path', $component);
$form_display->save();
}
}
}
}
/**
* Converts patterns from configuration objects to configuration entities.
*/
function pathauto_update_8100() {
\Drupal::service('module_installer')->install(['ctools']);
$messages = array();
/** @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_bundle_info */
$entity_bundle_info = \Drupal::service('entity_type.bundle.info');
$entity_type_manager = \Drupal::entityTypeManager();
$language_manager = \Drupal::languageManager();
$entity_type_manager->clearCachedDefinitions();
\Drupal::service('plugin.manager.alias_type')->clearCachedDefinitions();
$entity_types = $entity_type_manager->getDefinitions();
// 1. Load all patterns.
$config = \Drupal::configFactory()->getEditable('pathauto.pattern');
$patterns = $config->get('patterns');
// 2. Create a configuration entity per pattern.
foreach ($patterns as $entity_type => $entity_patterns) {
if (!array_key_exists($entity_type, $entity_types)) {
// We found an unknown entity type. Report it.
$messages[] = t('Entity of type @type was not processed. It defines the following patterns: @patterns', array(
'@type' => $entity_type,
'@patterns' => print_r($entity_patterns, TRUE),
));
continue;
}
$entity_label = $entity_types[$entity_type]->getLabel();
if (!empty($entity_patterns['default'])) {
// This is a pattern for an entity type, such as "node".
$pattern = PathautoPattern::create([
'id' => $entity_type,
'label' => $entity_label,
'type' => 'canonical_entities:' . $entity_type,
'pattern' => $entity_patterns['default'],
'weight' => 0,
]);
$pattern->save();
}
// Loop over bundles and create patterns if they have a value.
// Bundle keys may have a language suffix for language-dependant patterns.
if (isset($entity_patterns['bundles'])) {
$bundle_info = $entity_bundle_info->getBundleInfo($entity_type);
foreach ($entity_patterns['bundles'] as $bundle => $bundle_patterns) {
if (empty($bundle_patterns['default'])) {
// This bundle does not define a pattern. Move on to the next one.
continue;
}
if (isset($bundle_info[$bundle])) {
// This is a pattern for a bundle, such as "node_article".
$pattern = PathautoPattern::create([
'id' => $entity_type . '_' . $bundle,
'label' => $entity_label . ' ' . $bundle_info[$bundle]['label'],
'type' => 'canonical_entities:' . $entity_type,
'pattern' => $bundle_patterns['default'],
'weight' => -5,
]);
// Add the bundle condition.
$pattern->addSelectionCondition([
'id' => 'entity_bundle:' . $entity_type,
'bundles' => array($bundle => $bundle),
'negate' => FALSE,
'context_mapping' => [ $entity_type => $entity_type ],
]);
$pattern->save();
}
else {
// This is either a language dependent pattern such as "article_es" or
// an unknown bundle or langcode. Let's figure it out.
$matches = NULL;
$langcode = NULL;
$extracted_bundle = NULL;
$language = NULL;
preg_match('/^(.*)_([a-z-]*)$/', $bundle, $matches);
if (count($matches) == 3) {
list(, $extracted_bundle, $langcode) = $matches;
$language = $language_manager->getLanguage($langcode);
}
// Validate bundle, langcode and language.
if (!isset($bundle_info[$extracted_bundle]) || ($langcode == NULL) || ($language == NULL)) {
$messages[] = t('Unrecognized entity bundle @entity:@bundle was not processed. It defines the following patterns: @patterns', array(
'@entity' => $entity_type,
'@bundle' => $bundle,
'@patterns' => print_r($entity_patterns, TRUE),
));
continue;
}
// This is a pattern for a bundle and a language, such as "node_article_es".
$pattern = PathautoPattern::create([
'id' => $entity_type . '_' . $extracted_bundle . '_' . str_replace('-', '_', $langcode),
'label' => $entity_label . ' ' . $bundle_info[$extracted_bundle]['label'] . ' ' . $language->getName(),
'type' => 'canonical_entities:' . $entity_type,
'pattern' => $bundle_patterns['default'],
'weight' => -10,
]);
// Add the bundle condition.
$pattern->addSelectionCondition([
'id' => 'entity_bundle:' . $entity_type,
'bundles' => array($extracted_bundle => $extracted_bundle),
'negate' => FALSE,
'context_mapping' => [ $entity_type => $entity_type ],
]);
// Add the language condition.
$language_mapping = $entity_type . ':' . $entity_type_manager->getDefinition($entity_type)->getKey('langcode') . ':language';
$pattern->addSelectionCondition([
'id' => 'language',
'langcodes' => [ $langcode => $langcode ],
'negate' => FALSE,
'context_mapping' => [
'language' => $language_mapping,
]
]);
// Add the context relationship for this language.
$pattern->addRelationship($language_mapping, 'Language');
$pattern->save();
}
}
}
}
// 3. Delete the old configuration object that stores patterns.
$config->delete();
// 4. Print out messages.
if (!empty($messages)) {
return implode('</br>', $messages);
}
}
/**
* Update relationship storage.
*/
function pathauto_update_8101() {
foreach (\Drupal::configFactory()->listAll('pathauto.pattern.') as $pattern_config_name) {
$pattern_config = \Drupal::configFactory()->getEditable($pattern_config_name);
$relationships = [];
foreach ((array) $pattern_config->get('context_definitions') as $context_definition) {
$relationships[$context_definition['id']] = ['label' => $context_definition['label']];
}
$pattern_config->clear('context_definitions');
$pattern_config->set('relationships', $relationships);
$pattern_config->save();
}
}
/**
* Update node type conditions from entity_bundle to node_type.
*/
function pathauto_update_8102() {
// Load all pattern configuration entities.
foreach (\Drupal::configFactory()->listAll('pathauto.pattern.') as $pattern_config_name) {
$pattern_config = \Drupal::configFactory()->getEditable($pattern_config_name);
// Loop patterns and swap the entity_bundle:node plugin by the node_type
// plugin.
if ($pattern_config->get('type') == 'canonical_entities:node') {
$selection_criteria = $pattern_config->get('selection_criteria');
foreach ($selection_criteria as $uuid => $condition) {
if ($condition['id'] == 'entity_bundle:node') {
$selection_criteria[$uuid]['id'] = 'node_type';
$pattern_config->set('selection_criteria', $selection_criteria);
$pattern_config->save();
break;
}
}
}
}
}
/**
* Fix invalid default value for ignore_words.
*/
function pathauto_update_8103() {
$config_factory = \Drupal::configFactory();
$config = $config_factory->getEditable('pathauto.settings');
$ignore_words = $config->get('ignore_words');
if ($ignore_words === ', in, is,that, the , this, with, ') {
$config->set('ignore_words', 'a, an, as, at, before, but, by, for, from, is, in, into, like, of, off, on, onto, per, since, than, the, this, that, to, up, via, with')->save(TRUE);
}
}
/**
* Resave patterns so that lookup keys are updated.
*/
function pathauto_update_8104() {
\Drupal::entityTypeManager()->clearCachedDefinitions();
// Load all pattern configuration entities and save them, so that the new
// status lookup keys are saved.
foreach (\Drupal::configFactory()->listAll('pathauto.pattern.') as $pattern_config_name) {
$pattern_config = \Drupal::configFactory()->getEditable($pattern_config_name);
$pattern_config->save();
}
}
/**
* Ensure the url_alias table exists.
*/
function pathauto_update_8105() {
_pathauto_ensure_url_alias_table_exists();
}
/**
* Update default configuration for enabled entity types.
*/
function pathauto_update_8106() {
$config_factory = \Drupal::configFactory();
$config = $config_factory->getEditable('pathauto.settings');
$config->set('enabled_entity_types', ['user']);
$config->save();
}
@@ -0,0 +1,21 @@
(function ($) {
'use strict';
Drupal.behaviors.pathFieldsetSummaries = {
attach: function (context) {
$('fieldset.path-form', context).drupalSetSummary(function (context) {
var path = $('.form-item-path-alias input', context).val();
var automatic = $('.form-item-path-pathauto input', context).attr('checked');
if (automatic) {
return Drupal.t('Automatic alias');
}
else if (path) {
return Drupal.t('Alias: @alias', {'@alias': path});
}
else {
return Drupal.t('No alias');
}
});
}
};
})(jQuery);
@@ -0,0 +1,9 @@
widget:
version: 1.0
js:
pathauto.js: {}
dependencies:
- core/jquery
- core/drupal
- core/drupalSettings
- core/drupal.form
@@ -0,0 +1,6 @@
entity.pathauto_pattern.add_form:
route_name: 'entity.pathauto_pattern.add_form'
title: 'Add Pathauto pattern'
appears_on:
- entity.pathauto_pattern.collection
@@ -0,0 +1,23 @@
pathauto.patterns.form:
route_name: entity.pathauto_pattern.collection
base_route: path.admin_overview
title: 'Patterns'
weight: 10
pathauto.settings.form:
route_name: pathauto.settings.form
base_route: path.admin_overview
title: 'Settings'
weight: 20
pathauto.bulk.update.form:
route_name: pathauto.bulk.update.form
base_route: path.admin_overview
title: 'Bulk generate'
weight: 30
pathauto.admin.delete:
route_name: pathauto.admin.delete
base_route: path.admin_overview
title: 'Delete aliases'
weight: 40
@@ -0,0 +1,153 @@
<?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\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;
/**
* The default ignore word list.
*/
define('PATHAUTO_IGNORE_WORDS', 'a, an, as, at, before, but, by, for, from, is, in, into, like, of, off, on, onto, per, since, than, the, this, that, to, up, via, with');
/**
* Implements hook_hook_info().
*/
function pathauto_hook_info() {
$hooks = array(
'pathauto_pattern_alter',
'pathauto_alias_alter',
'pathauto_is_alias_reserved',
);
return array_fill_keys($hooks, array('group' => 'pathauto'));
}
/**
* Implements hook_help().
*/
function pathauto_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
case 'help.page.pathauto':
$output = '<h3>' . t('About') . '</h3>';
$output .= '<p>' . t('The Pathauto module provides a mechanism to automate the creation of <a href="path">path</a> aliases. This makes URLs more readable and helps search engines index content more effectively. For more information, see the <a href=":online">online documentation for Pathauto</a>.', [':online' => 'https://www.drupal.org/documentation/modules/pathauto']) . '</p>';
$output .= '<dl>';
$output .= '<h3>' . t('Uses') . '</h3>';
$output .= '<dd>' . t('Pathauto is accessed from the tabs it adds to the list of <a href=":aliases">URL aliases</a>.', [':aliases' => Url::fromRoute('path.admin_overview')->toString()]) . '</dd>';
$output .= '<dt>' . t('Creating Pathauto Patterns') . '</dt>';
$output .= '<dd>' . t('The <a href=":pathauto_pattern">"Patterns"</a> page is used to configure automatic path aliasing. New patterns are created here using the <a href=":add_form">Add Pathauto pattern</a> button which presents a form to simplify pattern creation thru the use of <a href="token">available tokens</a>. The patterns page provides a list of all patterns on the site and allows you to edit and reorder them. An alias is generated for the first pattern that applies.', [':pathauto_pattern' => Url::fromRoute('entity.pathauto_pattern.collection')->toString(), ':add_form' => Url::fromRoute('entity.pathauto_pattern.add_form')->toString()]) . '</dd>';
$output .= '<dt>' . t('Pathauto Settings') . '</dt>';
$output .= '<dd>' . t('The <a href=":settings">"Settings"</a> page is used to customize global Pathauto settings for automated pattern creation.', [':settings' => Url::fromRoute('pathauto.settings.form')->toString()]) . '</dd>';
$output .= '<dd>' . t('The <strong>maximum alias length</strong> and <strong>maximum component length</strong> values default to 100 and have a limit of @max from Pathauto. You should enter a value that is the length of the "alias" column of the url_alias database table minus the length of any strings that might get added to the end of the URL. The recommended and default value is 100.', array('@max' => \Drupal::service('pathauto.alias_storage_helper')->getAliasSchemaMaxlength())) . '</dd>';
$output .= '<dt>' . t('Bulk Generation') . '</dt>';
$output .= '<dd>' . t('The <a href=":pathauto_bulk">"Bulk Generate"</a> page allows you to create URL aliases for items that currently have no aliases. This is typically used when installing Pathauto on a site that has existing un-aliased content that needs to be aliased in bulk.', [':pathauto_bulk' => Url::fromRoute('pathauto.bulk.update.form')->toString()]) . '</dd>';
$output .= '<dt>' . t('Delete Aliases') . '</dt>';
$output .= '<dd>' . t('The <a href=":pathauto_delete">"Delete Aliases"</a> page allows you to remove URL aliases from items that have previously been assigned aliases using pathauto.', [':pathauto_delete' => Url::fromRoute('pathauto.admin.delete')->toString()]) . '</dd>';
$output .= '</dl>';
return $output;
case 'entity.pathauto_pattern.collection':
$output = '<p>' . t('This page provides a list of all patterns on the site and allows you to edit and reorder them.') . '</p>';
return $output;
case 'entity.pathauto_pattern.add_form':
$output = '<p>' . t('You need to select a pattern type, then a pattern and filter, and a label. Additional types can be enabled on the <a href=":settings">Settings</a> page.', [':settings' => Url::fromRoute('pathauto.settings.form')->toString()]) . '</p>';
return $output;
case 'pathauto.bulk.update.form':
$output = '<p>' . t('Bulk generation can be used to generate URL aliases for items that currently have no aliases. This is typically used when installing Pathauto on a site that has existing un-aliased content that needs to be aliased in bulk.') . '<br>';
$output .= t('It can also be used to regenerate URL aliases for items that have an old alias and for which the Pathauto pattern has been changed.') . '</p>';
$output .= '<p>' . t('Note that this will only affect items which are configured to have their URL alias automatically set. Items whose URL alias is manually set are not affected.') . '</p>';
return $output;
}
}
/**
* Implements hook_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', array(
'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.', array('%element-title' => $title, '@invalid-characters' => implode(', ', $invalid_characters_used))));
}
if (preg_match('/(\s$)+/', $element['#value'])) {
$form_state->setError($element, t('The %element-title doesn\'t allow the patterns ending with whitespace.', array('%element-title' => $title)));
}
}
return $element;
}
@@ -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.'
@@ -0,0 +1,54 @@
entity.pathauto_pattern.collection:
path: '/admin/config/search/path/patterns'
defaults:
_entity_list: 'pathauto_pattern'
_title: 'Patterns'
requirements:
_permission: 'administer pathauto'
entity.pathauto_pattern.add_form:
path: '/admin/config/search/path/patterns/add'
defaults:
_entity_form: 'pathauto_pattern.default'
_title: 'Add Pathauto pattern'
tempstore_id: 'pathauto.pattern'
requirements:
_permission: 'administer pathauto'
pathauto.settings.form:
path: '/admin/config/search/path/settings'
defaults:
_form: '\Drupal\pathauto\Form\PathautoSettingsForm'
_title: 'Settings'
requirements:
_permission: 'administer pathauto'
entity.pathauto_pattern.enable:
path: '/admin/config/search/path/patterns/{pathauto_pattern}/enable'
defaults:
_entity_form: 'pathauto_pattern.enable'
requirements:
_entity_access: 'pathauto_pattern.update'
entity.pathauto_pattern.disable:
path: '/admin/config/search/path/patterns/{pathauto_pattern}/disable'
defaults:
_entity_form: 'pathauto_pattern.disable'
requirements:
_entity_access: 'pathauto_pattern.update'
pathauto.bulk.update.form:
path: '/admin/config/search/path/update_bulk'
defaults:
_form: '\Drupal\pathauto\Form\PathautoBulkUpdateForm'
_title: 'Bulk generate'
requirements:
_permission: 'administer url aliases'
pathauto.admin.delete:
path: '/admin/config/search/path/delete_bulk'
defaults:
_form: '\Drupal\pathauto\Form\PathautoAdminDelete'
_title: 'Delete aliases'
requirements:
_permission: 'administer url aliases'
@@ -0,0 +1,26 @@
services:
pathauto.generator:
class: Drupal\pathauto\PathautoGenerator
arguments: ['@config.factory', '@module_handler', '@token', '@pathauto.alias_cleaner', '@pathauto.alias_storage_helper', '@pathauto.alias_uniquifier', '@pathauto.verbose_messenger', '@string_translation', '@token.entity_mapper', '@entity_type.manager']
pathauto.alias_cleaner:
class: Drupal\pathauto\AliasCleaner
arguments: ['@config.factory', '@pathauto.alias_storage_helper', '@language_manager', '@cache.discovery', '@transliteration', '@module_handler']
pathauto.alias_storage_helper:
class: Drupal\pathauto\AliasStorageHelper
arguments: ['@config.factory', '@path.alias_storage', '@database','@pathauto.verbose_messenger', '@string_translation']
tags:
- { name: backend_overridable }
pathauto.alias_uniquifier:
class: Drupal\pathauto\AliasUniquifier
arguments: ['@config.factory', '@pathauto.alias_storage_helper','@module_handler', '@router.route_provider', '@path.alias_manager']
pathauto.verbose_messenger:
class: Drupal\pathauto\VerboseMessenger
arguments: ['@config.factory', '@current_user']
plugin.manager.alias_type:
class: Drupal\pathauto\AliasTypeManager
parent: default_plugin_manager
pathauto.settings_cache_tag:
class: Drupal\pathauto\EventSubscriber\PathautoSettingsCacheTag
arguments: ['@entity_field.manager', '@plugin.manager.alias_type']
tags:
- { name: event_subscriber }
@@ -0,0 +1,49 @@
<?php
/**
* @file
* Token integration for the Pathauto module.
*/
use Drupal\Core\Render\BubbleableMetadata;
/**
* Implements hook_token_info().
*/
function pathauto_token_info() {
$info = array();
$info['tokens']['array']['join-path'] = array(
'name' => t('Joined path'),
'description' => t('The array values each cleaned by Pathauto and then joined with the slash into a string that resembles an URL.'),
);
return $info;
}
/**
* Implements hook_tokens().
*/
function pathauto_tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) {
$replacements = array();
if ($type == 'array' && !empty($data['array'])) {
$array = $data['array'];
foreach ($tokens as $name => $original) {
switch ($name) {
case 'join-path':
$values = array();
foreach (token_element_children($array) as $key) {
$value = is_array($array[$key]) ? render($array[$key]) : (string) $array[$key];
$value = \Drupal::service('pathauto.alias_cleaner')->cleanString($value, $options);
$values[] = $value;
}
$replacements[$original] = implode('/', $values);
break;
}
}
}
return $replacements;
}
@@ -0,0 +1,351 @@
<?php
namespace Drupal\pathauto;
use Drupal\Component\Render\PlainTextOutput;
use Drupal\Component\Transliteration\TransliterationInterface;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Language\LanguageManagerInterface;
/**
* Provides an alias cleaner.
*/
class AliasCleaner implements AliasCleanerInterface {
/**
* The config factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* The alias storage helper.
*
* @var AliasStorageHelperInterface
*/
protected $aliasStorageHelper;
/**
* Language manager.
*
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected $languageManager;
/**
* Cache backend.
*
* @var \Drupal\Core\Cache\CacheBackendInterface
*/
protected $cacheBackend;
/**
* Calculated settings cache.
*
* @todo Split this up into separate properties.
*
* @var array
*/
protected $cleanStringCache = array();
/**
* Transliteration service.
*
* @var \Drupal\Component\Transliteration\TransliterationInterface
*/
protected $transliteration;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* Creates a new AliasCleaner.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
* @param \Drupal\pathauto\AliasStorageHelperInterface $alias_storage_helper
* The alias storage helper.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
* The cache backend.
* @param \Drupal\Component\Transliteration\TransliterationInterface $transliteration
* The transliteration service.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
*/
public function __construct(ConfigFactoryInterface $config_factory, AliasStorageHelperInterface $alias_storage_helper, LanguageManagerInterface $language_manager, CacheBackendInterface $cache_backend, TransliterationInterface $transliteration, ModuleHandlerInterface $module_handler) {
$this->configFactory = $config_factory;
$this->aliasStorageHelper = $alias_storage_helper;
$this->languageManager = $language_manager;
$this->cacheBackend = $cache_backend;
$this->transliteration = $transliteration;
$this->moduleHandler = $module_handler;
}
/**
* {@inheritdoc}
*/
public function cleanAlias($alias) {
$config = $this->configFactory->get('pathauto.settings');
$alias_max_length = min($config->get('max_length'), $this->aliasStorageHelper->getAliasSchemaMaxLength());
$output = $alias;
// Trim duplicate, leading, and trailing separators. Do this before cleaning
// backslashes since a pattern like "[token1]/[token2]-[token3]/[token4]"
// could end up like "value1/-/value2" and if backslashes were cleaned first
// this would result in a duplicate blackslash.
$output = $this->getCleanSeparators($output);
// Trim duplicate, leading, and trailing backslashes.
$output = $this->getCleanSeparators($output, '/');
// Shorten to a logical place based on word boundaries.
$output = Unicode::truncate($output, $alias_max_length, TRUE);
return $output;
}
/**
* {@inheritdoc}
*/
public function getCleanSeparators($string, $separator = NULL) {
$config = $this->configFactory->get('pathauto.settings');
if (!isset($separator)) {
$separator = $config->get('separator');
}
$output = $string;
if (strlen($separator)) {
// Trim any leading or trailing separators.
$output = trim($output, $separator);
// Escape the separator for use in regular expressions.
$seppattern = preg_quote($separator, '/');
// Replace multiple separators with a single one.
$output = preg_replace("/$seppattern+/", $separator, $output);
// Replace trailing separators around slashes.
if ($separator !== '/') {
$output = preg_replace("/\/+$seppattern\/+|$seppattern\/+|\/+$seppattern/", "/", $output);
}
else {
// If the separator is a slash, we need to re-add the leading slash
// dropped by the trim function.
$output = '/' . $output;
}
}
return $output;
}
/**
* {@inheritdoc}
*/
public function cleanString($string, array $options = array()) {
if (empty($this->cleanStringCache)) {
// Generate and cache variables used in this method.
$config = $this->configFactory->get('pathauto.settings');
$this->cleanStringCache = array(
'separator' => $config->get('separator'),
'strings' => array(),
'transliterate' => $config->get('transliterate'),
'punctuation' => array(),
'reduce_ascii' => (bool) $config->get('reduce_ascii'),
'ignore_words_regex' => FALSE,
'lowercase' => (bool) $config->get('case'),
'maxlength' => min($config->get('max_component_length'), $this->aliasStorageHelper->getAliasSchemaMaxLength()),
);
// Generate and cache the punctuation replacements for strtr().
$punctuation = $this->getPunctuationCharacters();
foreach ($punctuation as $name => $details) {
$action = $config->get('punctuation.' . $name);
switch ($action) {
case PathautoGeneratorInterface::PUNCTUATION_REMOVE:
$this->cleanStringCache['punctuation'][$details['value']] = '';
break;
case PathautoGeneratorInterface::PUNCTUATION_REPLACE:
$this->cleanStringCache['punctuation'][$details['value']] = $this->cleanStringCache['separator'];
break;
case PathautoGeneratorInterface::PUNCTUATION_DO_NOTHING:
// Literally do nothing.
break;
}
}
// Generate and cache the ignored words regular expression.
$ignore_words = $config->get('ignore_words');
$ignore_words_regex = preg_replace(array('/^[,\s]+|[,\s]+$/', '/[,\s]+/'), array('', '\b|\b'), $ignore_words);
if ($ignore_words_regex) {
$this->cleanStringCache['ignore_words_regex'] = '\b' . $ignore_words_regex . '\b';
if (function_exists('mb_eregi_replace')) {
mb_regex_encoding('UTF-8');
$this->cleanStringCache['ignore_words_callback'] = 'mb_eregi_replace';
}
else {
$this->cleanStringCache['ignore_words_callback'] = 'preg_replace';
$this->cleanStringCache['ignore_words_regex'] = '/' . $this->cleanStringCache['ignore_words_regex'] . '/i';
}
}
}
// Empty strings do not need any processing.
if ($string === '' || $string === NULL) {
return '';
}
$langcode = NULL;
if (!empty($options['language'])) {
$langcode = $options['language']->getId();
}
elseif (!empty($options['langcode'])) {
$langcode = $options['langcode'];
}
// Check if the string has already been processed, and if so return the
// cached result.
if (isset($this->cleanStringCache['strings'][$langcode][(string) $string])) {
return $this->cleanStringCache['strings'][$langcode][(string) $string];
}
// Remove all HTML tags from the string.
$output = Html::decodeEntities($string);
$output = PlainTextOutput::renderFromHtml($output);
// Optionally transliterate.
if ($this->cleanStringCache['transliterate']) {
// If the reduce strings to letters and numbers is enabled, don't bother
// replacing unknown characters with a question mark. Use an empty string
// instead.
$output = $this->transliteration->transliterate($output, $langcode, $this->cleanStringCache['reduce_ascii'] ? '' : '?');
}
// Replace or drop punctuation based on user settings.
$output = strtr($output, $this->cleanStringCache['punctuation']);
// Reduce strings to letters and numbers.
if ($this->cleanStringCache['reduce_ascii']) {
$output = preg_replace('/[^a-zA-Z0-9\/]+/', $this->cleanStringCache['separator'], $output);
}
// Get rid of words that are on the ignore list.
if ($this->cleanStringCache['ignore_words_regex']) {
$words_removed = $this->cleanStringCache['ignore_words_callback']($this->cleanStringCache['ignore_words_regex'], '', $output);
if (Unicode::strlen(trim($words_removed)) > 0) {
$output = $words_removed;
}
}
// Always replace whitespace with the separator.
$output = preg_replace('/\s+/', $this->cleanStringCache['separator'], $output);
// Trim duplicates and remove trailing and leading separators.
$output = $this->getCleanSeparators($this->getCleanSeparators($output, $this->cleanStringCache['separator']));
// Optionally convert to lower case.
if ($this->cleanStringCache['lowercase']) {
$output = Unicode::strtolower($output);
}
// Shorten to a logical place based on word boundaries.
$output = Unicode::truncate($output, $this->cleanStringCache['maxlength'], TRUE);
// Cache this result in the static array.
$this->cleanStringCache['strings'][$langcode][(string) $string] = $output;
return $output;
}
/**
* {@inheritdoc}
*/
public function getPunctuationCharacters() {
if (empty($this->punctuationCharacters)) {
$langcode = $this->languageManager->getCurrentLanguage()->getId();
$cid = 'pathauto:punctuation:' . $langcode;
if ($cache = $this->cacheBackend->get($cid)) {
$this->punctuationCharacters = $cache->data;
}
else {
$punctuation = array();
$punctuation['double_quotes'] = array('value' => '"', 'name' => t('Double quotation marks'));
$punctuation['quotes'] = array('value' => '\'', 'name' => t("Single quotation marks (apostrophe)"));
$punctuation['backtick'] = array('value' => '`', 'name' => t('Back tick'));
$punctuation['comma'] = array('value' => ',', 'name' => t('Comma'));
$punctuation['period'] = array('value' => '.', 'name' => t('Period'));
$punctuation['hyphen'] = array('value' => '-', 'name' => t('Hyphen'));
$punctuation['underscore'] = array('value' => '_', 'name' => t('Underscore'));
$punctuation['colon'] = array('value' => ':', 'name' => t('Colon'));
$punctuation['semicolon'] = array('value' => ';', 'name' => t('Semicolon'));
$punctuation['pipe'] = array('value' => '|', 'name' => t('Vertical bar (pipe)'));
$punctuation['left_curly'] = array('value' => '{', 'name' => t('Left curly bracket'));
$punctuation['left_square'] = array('value' => '[', 'name' => t('Left square bracket'));
$punctuation['right_curly'] = array('value' => '}', 'name' => t('Right curly bracket'));
$punctuation['right_square'] = array('value' => ']', 'name' => t('Right square bracket'));
$punctuation['plus'] = array('value' => '+', 'name' => t('Plus sign'));
$punctuation['equal'] = array('value' => '=', 'name' => t('Equal sign'));
$punctuation['asterisk'] = array('value' => '*', 'name' => t('Asterisk'));
$punctuation['ampersand'] = array('value' => '&', 'name' => t('Ampersand'));
$punctuation['percent'] = array('value' => '%', 'name' => t('Percent sign'));
$punctuation['caret'] = array('value' => '^', 'name' => t('Caret'));
$punctuation['dollar'] = array('value' => '$', 'name' => t('Dollar sign'));
$punctuation['hash'] = array('value' => '#', 'name' => t('Number sign (pound sign, hash)'));
$punctuation['at'] = array('value' => '@', 'name' => t('At sign'));
$punctuation['exclamation'] = array('value' => '!', 'name' => t('Exclamation mark'));
$punctuation['tilde'] = array('value' => '~', 'name' => t('Tilde'));
$punctuation['left_parenthesis'] = array('value' => '(', 'name' => t('Left parenthesis'));
$punctuation['right_parenthesis'] = array('value' => ')', 'name' => t('Right parenthesis'));
$punctuation['question_mark'] = array('value' => '?', 'name' => t('Question mark'));
$punctuation['less_than'] = array('value' => '<', 'name' => t('Less-than sign'));
$punctuation['greater_than'] = array('value' => '>', 'name' => t('Greater-than sign'));
$punctuation['slash'] = array('value' => '/', 'name' => t('Slash'));
$punctuation['back_slash'] = array('value' => '\\', 'name' => t('Backslash'));
// Allow modules to alter the punctuation list and cache the result.
$this->moduleHandler->alter('pathauto_punctuation_chars', $punctuation);
$this->cacheBackend->set($cid, $punctuation);
$this->punctuationCharacters = $punctuation;
}
}
return $this->punctuationCharacters;
}
/**
* {@inheritdoc}
*/
public function cleanTokenValues(&$replacements, $data = array(), $options = array()) {
foreach ($replacements as $token => $value) {
// Only clean non-path tokens.
if (!preg_match('/(path|alias|url|url-brief)\]$/', $token)) {
$replacements[$token] = $this->cleanString($value, $options);
}
}
}
/**
* {@inheritdoc}
*/
public function resetCaches() {
$this->cleanStringCache = array();
}
}
@@ -0,0 +1,103 @@
<?php
namespace Drupal\pathauto;
/**
* @todo add class comment.
*/
interface AliasCleanerInterface {
/**
* Clean up an URL alias.
*
* Performs the following alterations:
* - Trim duplicate, leading, and trailing back-slashes.
* - Trim duplicate, leading, and trailing separators.
* - Shorten to a desired length and logical position based on word boundaries.
*
* @param string $alias
* A string with the URL alias to clean up.
*
* @return string
* The cleaned URL alias.
*/
public function cleanAlias($alias);
/**
* Trims duplicate, leading, and trailing separators from a string.
*
* @param string $string
* The string to clean path separators from.
* @param string $separator
* The path separator to use when cleaning.
*
* @return string
* The cleaned version of the string.
*
* @see pathauto_cleanstring()
* @see pathauto_clean_alias()
*/
public function getCleanSeparators($string, $separator = NULL);
/**
* Clean up a string segment to be used in an URL alias.
*
* Performs the following possible alterations:
* - Remove all HTML tags.
* - Process the string through the transliteration module.
* - Replace or remove punctuation with the separator character.
* - Remove back-slashes.
* - Replace non-ascii and non-numeric characters with the separator.
* - Remove common words.
* - Replace whitespace with the separator character.
* - Trim duplicate, leading, and trailing separators.
* - Convert to lower-case.
* - Shorten to a desired length and logical position based on word boundaries.
*
* This function should *not* be called on URL alias or path strings
* because it is assumed that they are already clean.
*
* @param string $string
* A string to clean.
* @param array $options
* (optional) A keyed array of settings and flags to control the Pathauto
* clean string replacement process. Supported options are:
* - langcode: A language code to be used when translating strings.
*
* @return string
* The cleaned string.
*/
public function cleanString($string, array $options = array());
/**
* Return an array of arrays for punctuation values.
*
* Returns an array of arrays for punctuation values keyed by a name, including
* the value and a textual description.
* Can and should be expanded to include "all" non text punctuation values.
*
* @return array
* An array of arrays for punctuation values keyed by a name, including the
* value and a textual description.
*/
public function getPunctuationCharacters();
/**
* Clean tokens so they are URL friendly.
*
* @param array $replacements
* An array of token replacements
* that need to be "cleaned" for use in the URL.
* @param array $data
* An array of objects used to generate the replacements.
* @param array $options
* An array of options used to generate the replacements.
*/
public function cleanTokenValues(&$replacements, $data = array(), $options = array());
/**
* Resets internal caches.
*/
public function resetCaches();
}
@@ -0,0 +1,255 @@
<?php
namespace Drupal\pathauto;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Database\Connection;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Path\AliasStorageInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
/**
* Provides helper methods for accessing alias storage.
*/
class AliasStorageHelper implements AliasStorageHelperInterface {
use StringTranslationTrait;
/**
* Alias schema max length.
*
* @var int
*/
protected $aliasSchemaMaxLength = 255;
/**
* Config factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* The alias storage.
*
* @var \Drupal\Core\Path\AliasStorageInterface
*/
protected $aliasStorage;
/**
* The database connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected $database;
/**
* The messenger.
*
* @var \Drupal\pathauto\MessengerInterface
*/
protected $messenger;
/**
* The config factory.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
* @param \Drupal\Core\Path\AliasStorageInterface $alias_storage
* The alias storage.
* @param \Drupal\Core\Database\Connection $database
* The database connection.
* @param MessengerInterface $messenger
* The messenger.
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* The string translation service.
*/
public function __construct(ConfigFactoryInterface $config_factory, AliasStorageInterface $alias_storage, Connection $database, MessengerInterface $messenger, TranslationInterface $string_translation) {
$this->configFactory = $config_factory;
$this->aliasStorage = $alias_storage;
$this->database = $database;
$this->messenger = $messenger;
$this->stringTranslation = $string_translation;
}
/**
* {@inheritdoc}
*/
public function getAliasSchemaMaxLength() {
return $this->aliasSchemaMaxLength;
}
/**
* {@inheritdoc}
*/
public function save(array $path, $existing_alias = NULL, $op = NULL) {
$config = $this->configFactory->get('pathauto.settings');
// Alert users if they are trying to create an alias that is the same as the
// internal path.
if ($path['source'] == $path['alias']) {
$this->messenger->addMessage($this->t('Ignoring alias %alias because it is the same as the internal path.', array('%alias' => $path['alias'])));
return NULL;
}
// Skip replacing the current alias with an identical alias.
if (empty($existing_alias) || $existing_alias['alias'] != $path['alias']) {
$path += array(
'pathauto' => TRUE,
'original' => $existing_alias,
'pid' => NULL,
);
// If there is already an alias, respect some update actions.
if (!empty($existing_alias)) {
switch ($config->get('update_action')) {
case PathautoGeneratorInterface::UPDATE_ACTION_NO_NEW:
// Do not create the alias.
return NULL;
case PathautoGeneratorInterface::UPDATE_ACTION_LEAVE:
// Create a new alias instead of overwriting the existing by leaving
// $path['pid'] empty.
break;
case PathautoGeneratorInterface::UPDATE_ACTION_DELETE:
// The delete actions should overwrite the existing alias.
$path['pid'] = $existing_alias['pid'];
break;
}
}
// Save the path array.
$this->aliasStorage->save($path['source'], $path['alias'], $path['language'], $path['pid']);
if (!empty($existing_alias['pid'])) {
$this->messenger->addMessage($this->t(
'Created new alias %alias for %source, replacing %old_alias.',
array(
'%alias' => $path['alias'],
'%source' => $path['source'],
'%old_alias' => $existing_alias['alias'],
)
)
);
}
else {
$this->messenger->addMessage($this->t('Created new alias %alias for %source.', array(
'%alias' => $path['alias'],
'%source' => $path['source'],
)));
}
return $path;
}
}
/**
* {@inheritdoc}
*/
public function loadBySource($source, $language = LanguageInterface::LANGCODE_NOT_SPECIFIED) {
$alias = $this->aliasStorage->load([
'source' => $source,
'langcode' => $language,
]);
// If no alias was fetched and if a language was specified, fallbacks to
// undefined language.
if (!$alias && ($language !== LanguageInterface::LANGCODE_NOT_SPECIFIED)) {
$alias = $this->aliasStorage->load([
'source' => $source,
'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
]);
}
return $alias;
}
/**
* {@inheritdoc}
*/
public function deleteBySourcePrefix($source) {
$pids = $this->loadBySourcePrefix($source);
if ($pids) {
$this->deleteMultiple($pids);
}
}
/**
* {@inheritdoc}
*/
public function deleteAll() {
$this->database->truncate('url_alias')->execute();
}
/**
* {@inheritdoc}
*/
public function deleteEntityPathAll(EntityInterface $entity, $default_uri = NULL) {
$this->deleteBySourcePrefix('/' . $entity->toUrl('canonical')->getInternalPath());
if (isset($default_uri) && $entity->toUrl('canonical')->toString() != $default_uri) {
$this->deleteBySourcePrefix($default_uri);
}
}
/**
* {@inheritdoc}
*/
public function loadBySourcePrefix($source) {
$select = $this->database->select('url_alias', 'u')
->fields('u', array('pid'));
$or_group = $select->orConditionGroup()
->condition('source', $source)
->condition('source', rtrim($source, '/') . '/%', 'LIKE');
return $select
->condition($or_group)
->execute()
->fetchCol();
}
/**
* {@inheritdoc}
*/
public function countBySourcePrefix($source) {
$select = $this->database->select('url_alias', 'u')
->fields('u', array('pid'));
$or_group = $select->orConditionGroup()
->condition('source', $source)
->condition('source', rtrim($source, '/') . '/%', 'LIKE');
return $select
->condition($or_group)
->countQuery()
->execute()
->fetchField();
}
/**
* {@inheritdoc}
*/
public function countAll() {
return $this->database->select('url_alias')
->countQuery()
->execute()
->fetchField();
}
/**
* Delete multiple URL aliases.
*
* Intent of this is to abstract a potential path_delete_multiple() function
* for Drupal 7 or 8.
*
* @param int[] $pids
* An array of path IDs to delete.
*/
public function deleteMultiple($pids) {
foreach ($pids as $pid) {
$this->aliasStorage->delete(array('pid' => $pid));
}
}
}
@@ -0,0 +1,117 @@
<?php
namespace Drupal\pathauto;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Language\LanguageInterface;
/**
* Provides helper methods for accessing alias storage.
*/
interface AliasStorageHelperInterface {
/**
* Fetch the maximum length of the {url_alias}.alias field from the schema.
*
* @return int
* An integer of the maximum URL alias length allowed by the database.
*/
public function getAliasSchemaMaxLength();
/**
* Private function for Pathauto to create an alias.
*
* @param array $path
* An associative array containing the following keys:
* - source: The internal system path.
* - alias: The URL alias.
* - pid: (optional) Unique path alias identifier.
* - language: (optional) The language of the alias.
* @param array|bool|null $existing_alias
* (optional) An associative array of the existing path alias.
* @param string $op
* An optional string with the operation being performed.
*
* @return array|bool
* The saved path or NULL if the path was not saved.
*/
public function save(array $path, $existing_alias = NULL, $op = NULL);
/**
* Fetches an existing URL alias given a path and optional language.
*
* @param string $source
* An internal Drupal path.
* @param string $language
* An optional language code to look up the path in.
*
* @return bool|array
* FALSE if no alias was found or an associative array containing the
* following keys:
* - source (string): The internal system path with a starting slash.
* - alias (string): The URL alias with a starting slash.
* - pid (int): Unique path alias identifier.
* - langcode (string): The language code of the alias.
*/
public function loadBySource($source, $language = LanguageInterface::LANGCODE_NOT_SPECIFIED);
/**
* Delete all aliases by source url.
*
* @param string $source
* An internal Drupal path.
*
* @return bool
* The URL alias source.
*/
public function deleteBySourcePrefix($source);
/**
* Delete all aliases (truncate the url_alias table).
*/
public function deleteAll();
/**
* Delete an entity URL alias and any of its sub-paths.
*
* This function also checks to see if the default entity URI is different
* from the current entity URI and will delete any of the default aliases.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* An entity object.
* @param string $default_uri
* The optional default uri path for the entity.
*/
public function deleteEntityPathAll(EntityInterface $entity, $default_uri = NULL);
/**
* Fetches an existing URL alias given a path prefix.
*
* @param string $source
* An internal Drupal path prefix.
*
* @return integer[]
* An array of PIDs.
*/
public function loadBySourcePrefix($source);
/**
* Returns the count of url aliases for the source.
*
* @param $source
* An internal Drupal path prefix.
*
* @return int
* Number of url aliases for the source.
*/
public function countBySourcePrefix($source);
/**
* Returns the total count of the url aliases.
*
* @return int
* Total number of aliases.
*/
public function countAll();
}
@@ -0,0 +1,31 @@
<?php
namespace Drupal\pathauto;
/**
* Alias types that support batch updates and deletions.
*/
interface AliasTypeBatchUpdateInterface extends AliasTypeInterface {
/**
* Gets called to batch update all entries.
*
* @param string $action
* One of:
* - 'create' to generate a URL alias for paths having none.
* - 'update' to recreate the URL alias for paths already having one, useful if the pattern changed.
* - 'all' to do both actions above at the same time.
* @param array $context
* Batch context.
*/
public function batchUpdate($action, &$context);
/**
* Gets called to batch delete all aliases created by pathauto.
*
* @param array $context
* Batch context.
*/
public function batchDelete(&$context);
}
@@ -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);
}
@@ -0,0 +1,71 @@
<?php
namespace Drupal\pathauto;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
use Drupal\Component\Plugin\FallbackPluginManagerInterface;
/**
* Manages pathauto alias type plugins.
*/
class AliasTypeManager extends DefaultPluginManager implements FallbackPluginManagerInterface {
/**
* Constructs a new AliasType manager instance.
*
* @param \Traversable $namespaces
* An object that implements \Traversable which contains the root paths
* keyed by the corresponding namespace to look for plugin implementations.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
* Cache backend instance to use.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler to invoke the alter hook with.
*/
public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
parent::__construct('Plugin/pathauto/AliasType', $namespaces, $module_handler, 'Drupal\pathauto\AliasTypeInterface', 'Drupal\pathauto\Annotation\AliasType');
$this->alterInfo('pathauto_alias_types');
$this->setCacheBackend($cache_backend, 'pathauto_alias_types');
}
/**
* Returns plugin definitions that support a given token type.
*
* @param string $type
* The type of token plugin must support to be useful.
*
* @return array
* Plugin definitions.
*/
public function getPluginDefinitionByType($type) {
$definitions = array_filter($this->getDefinitions(), function ($definition) use ($type) {
if (!empty($definition['types']) && in_array($type, $definition['types'])) {
return TRUE;
}
return FALSE;
});
return $definitions;
}
/**
* {@inheritdoc}
*/
public function getFallbackPluginId($plugin_id, array $configuration = array()) {
return 'broken';
}
/**
* Gets the definition of all visible plugins for this type.
*
* @return array
* An array of plugin definitions (empty array if no definitions were
* found). Keys are plugin IDs.
*/
public function getVisibleDefinitions() {
$definitions = $this->getDefinitions();
unset($definitions['broken']);
return $definitions;
}
}
@@ -0,0 +1,167 @@
<?php
namespace Drupal\pathauto;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Path\AliasManagerInterface;
use Drupal\Core\Routing\RouteProviderInterface;
/**
* Provides a utility for creating a unique path alias.
*/
class AliasUniquifier implements AliasUniquifierInterface {
/**
* Config factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* The alias storage helper.
*
* @var \Drupal\pathauto\AliasStorageHelperInterface
*/
protected $aliasStorageHelper;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The route provider service.
*
* @var \Drupal\Core\Routing\RouteProviderInterface.
*/
protected $routeProvider;
/**
* The alias manager.
*
* @var \Drupal\Core\Path\AliasManagerInterface
*/
protected $aliasManager;
/**
* Creates a new AliasUniquifier.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
* @param \Drupal\pathauto\AliasStorageHelperInterface $alias_storage_helper
* The alias storage helper.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\Routing\RouteProviderInterface $route_provider
* The route provider service.
*/
public function __construct(ConfigFactoryInterface $config_factory, AliasStorageHelperInterface $alias_storage_helper, ModuleHandlerInterface $module_handler, RouteProviderInterface $route_provider, AliasManagerInterface $alias_manager) {
$this->configFactory = $config_factory;
$this->aliasStorageHelper = $alias_storage_helper;
$this->moduleHandler = $module_handler;
$this->routeProvider = $route_provider;
$this->aliasManager = $alias_manager;
}
/**
* {@inheritdoc}
*/
public function uniquify(&$alias, $source, $langcode) {
$config = $this->configFactory->get('pathauto.settings');
if (!$this->isReserved($alias, $source, $langcode)) {
return;
}
// If the alias already exists, generate a new, hopefully unique, variant.
$maxlength = min($config->get('max_length'), $this->aliasStorageHelper->getAliasSchemaMaxlength());
$separator = $config->get('separator');
$original_alias = $alias;
$i = 0;
do {
// Append an incrementing numeric suffix until we find a unique alias.
$unique_suffix = $separator . $i;
$alias = Unicode::truncate($original_alias, $maxlength - Unicode::strlen($unique_suffix), TRUE) . $unique_suffix;
$i++;
} while ($this->isReserved($alias, $source, $langcode));
}
/**
* {@inheritdoc}
*/
public function isReserved($alias, $source, $langcode = LanguageInterface::LANGCODE_NOT_SPECIFIED) {
// Check if this alias already exists.
if ($existing_source = $this->aliasManager->getPathByAlias($alias, $langcode)) {
if ($existing_source != $alias) {
// If it is an alias for the provided source, it is allowed to keep using
// it. If not, then it is reserved.
return $existing_source != $source;
}
}
// Then check if there is a route with the same path.
if ($this->isRoute($alias)) {
return TRUE;
}
// Finally check if any other modules have reserved the alias.
$args = array(
$alias,
$source,
$langcode,
);
$implementations = $this->moduleHandler->getImplementations('pathauto_is_alias_reserved');
foreach ($implementations as $module) {
$result = $this->moduleHandler->invoke($module, 'pathauto_is_alias_reserved', $args);
if (!empty($result)) {
// As soon as the first module says that an alias is in fact reserved,
// then there is no point in checking the rest of the modules.
return TRUE;
}
}
return FALSE;
}
/**
* Verify if the given path is a valid route.
*
* @param string $path
* A string containing a relative path.
*
* @return bool
* TRUE if the path already exists.
*
* @throws \InvalidArgumentException
*/
public function isRoute($path) {
if (is_file(DRUPAL_ROOT . '/' . $path) || is_dir(DRUPAL_ROOT . '/' . $path)) {
// Do not allow existing files or directories to get assigned an automatic
// alias. Note that we do not need to use is_link() to check for symbolic
// links since this returns TRUE for either is_file() or is_dir() already.
return TRUE;
}
$routes = $this->routeProvider->getRoutesByPattern($path);
// Only return true for an exact match, ignore placeholders.
foreach ($routes as $route) {
if ($route->getPath() == $path) {
return TRUE;
}
}
return FALSE;
}
}
@@ -0,0 +1,42 @@
<?php
namespace Drupal\pathauto;
use Drupal\Core\Language\LanguageInterface;
/**
* Provides an interface for alias uniquifiers.
*/
interface AliasUniquifierInterface {
/**
* Check to ensure a path alias is unique and add suffix variants if necessary.
*
* Given an alias 'content/test' if a path alias with the exact alias already
* exists, the function will change the alias to 'content/test-0' and will
* increase the number suffix until it finds a unique alias.
*
* @param string $alias
* A string with the alias. Can be altered by reference.
* @param string $source
* A string with the path source.
* @param string $langcode
* A string with a language code.
*/
public function uniquify(&$alias, $source, $langcode);
/**
* Checks if an alias is reserved.
*
* @param string $alias
* The alias.
* @param string $source
* The source.
* @param string $langcode
* (optional) The language code.
*
* @return bool
* Returns TRUE if the alias is reserved.
*/
public function isReserved($alias, $source, $langcode = LanguageInterface::LANGCODE_NOT_SPECIFIED);
}
@@ -0,0 +1,37 @@
<?php
namespace Drupal\pathauto\Annotation;
use Drupal\Component\Annotation\Plugin;
/**
* Defines an AliasType annotation.
*
* @Annotation
*/
class AliasType extends Plugin {
/**
* The plugin ID.
*
* @var string
*/
public $id;
/**
* The human-readable name of the action plugin.
*
* @ingroup plugin_translatable
*
* @var \Drupal\Core\Annotation\Translation
*/
public $label;
/**
* The token types.
*
* @var string[]
*/
public $types = array();
}
@@ -0,0 +1,382 @@
<?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\ContextAwarePluginInterface;
use Drupal\Core\Plugin\DefaultSingleLazyPluginCollection;
use Drupal\pathauto\PathautoPatternInterface;
/**
* Defines the Pathauto pattern entity.
*
* @ConfigEntityType(
* id = "pathauto_pattern",
* label = @Translation("Pathauto pattern"),
* handlers = {
* "list_builder" = "Drupal\pathauto\PathautoPatternListBuilder",
* "form" = {
* "default" = "Drupal\pathauto\Form\PatternEditForm",
* "delete" = "Drupal\Core\Entity\EntityDeleteForm",
* "enable" = "Drupal\pathauto\Form\PatternEnableForm",
* "disable" = "Drupal\pathauto\Form\PatternDisableForm"
* },
* "route_provider" = {
* "html" = "Drupal\Core\Entity\Routing\DefaultHtmlRouteProvider",
* },
* },
* config_prefix = "pattern",
* admin_permission = "administer pathauto",
* entity_keys = {
* "id" = "id",
* "label" = "label",
* "uuid" = "uuid",
* "weight" = "weight",
* "status" = "status"
* },
* config_export = {
* "id",
* "label",
* "type",
* "pattern",
* "selection_criteria",
* "selection_logic",
* "weight",
* "relationships"
* },
* lookup_keys = {
* "type",
* "status",
* },
* links = {
* "collection" = "/admin/config/search/path/patterns",
* "edit-form" = "/admin/config/search/path/patterns/{pathauto_pattern}",
* "delete-form" = "/admin/config/search/path/patterns/{pathauto_pattern}/delete",
* "enable" = "/admin/config/search/path/patterns/{pathauto_pattern}/enable",
* "disable" = "/admin/config/search/path/patterns/{pathauto_pattern}/disable"
* }
* )
*/
class PathautoPattern extends ConfigEntityBase implements PathautoPatternInterface {
/**
* The Pathauto pattern ID.
*
* @var string
*/
protected $id;
/**
* The Pathauto pattern label.
*
* @var string
*/
protected $label;
/**
* The pattern type.
*
* A string denoting the type of pathauto pattern this is. For a node path
* this would be 'node', for users it would be 'user', and so on. This allows
* for arbitrary non-entity patterns to be possible if applicable.
*
* @var string
*/
protected $type;
/**
* @var \Drupal\Core\Plugin\DefaultSingleLazyPluginCollection
*/
protected $aliasTypeCollection;
/**
* A tokenized string for alias generation.
*
* @var string
*/
protected $pattern;
/**
* The plugin configuration for the selection criteria condition plugins.
*
* @var array
*/
protected $selection_criteria = [];
/**
* The selection logic for this pattern entity (either 'and' or 'or').
*
* @var string
*/
protected $selection_logic = 'and';
/**
* @var int
*/
protected $weight = 0;
/**
* @var array[]
* Keys are context tokens, and values are arrays with the following keys:
* - label (string|null, optional): The human-readable label of this
* relationship.
*/
protected $relationships = [];
/**
* The plugin collection that holds the selection criteria condition plugins.
*
* @var \Drupal\Component\Plugin\LazyPluginCollection
*/
protected $selectionConditionCollection;
/**
* {@inheritdoc}
*
* Not using core's default logic around ConditionPluginCollection since it
* incorrectly assumes no condition will ever be applied twice.
*/
public function preSave(EntityStorageInterface $storage) {
parent::preSave($storage);
$criteria = [];
foreach ($this->getSelectionConditions() as $id => $condition) {
$criteria[$id] = $condition->getConfiguration();
}
$this->selection_criteria = $criteria;
// Invalidate the static caches.
\Drupal::service('pathauto.generator')->resetCaches();
}
/**
* {@inheritdoc}
*/
public static function postDelete(EntityStorageInterface $storage, array $entities) {
parent::postDelete($storage, $entities);
// Invalidate the static caches.
\Drupal::service('pathauto.generator')->resetCaches();
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
parent::calculateDependencies();
$this->calculatePluginDependencies($this->getAliasType());
foreach ($this->getSelectionConditions() as $instance) {
$this->calculatePluginDependencies($instance);
}
return $this->getDependencies();
}
/**
* {@inheritdoc}
*/
public function getPattern() {
return $this->pattern;
}
/**
* {@inheritdoc}
*/
public function setPattern($pattern) {
$this->pattern = $pattern;
return $this;
}
/**
* {@inheritdoc}
*/
public function getType() {
return $this->type;
}
/**
* {@inheritdoc}
*/
public function getAliasType() {
if (!$this->aliasTypeCollection) {
$this->aliasTypeCollection = new DefaultSingleLazyPluginCollection(\Drupal::service('plugin.manager.alias_type'), $this->getType(), ['default' => $this->getPattern()]);
}
return $this->aliasTypeCollection->get($this->getType());
}
/**
* {@inheritdoc}
*/
public function getWeight() {
return $this->weight;
}
/**
* {@inheritdoc}
*/
public function setWeight($weight) {
$this->weight = $weight;
return $this;
}
/**
* {@inheritdoc}
*/
public function getContexts() {
$contexts = $this->getAliasType()->getContexts();
foreach ($this->getRelationships() as $token => $definition) {
/** @var \Drupal\ctools\TypedDataResolver $resolver */
$resolver = \Drupal::service('ctools.typed_data.resolver');
$context = $resolver->convertTokenToContext($token, $contexts);
$context_definition = $context->getContextDefinition();
if (!empty($definition['label'])) {
$context_definition->setLabel($definition['label']);
}
$contexts[$token] = $context;
}
return $contexts;
}
/**
* {@inheritdoc}
*/
public function hasRelationship($token) {
return isset($this->relationships[$token]);
}
/**
* {@inheritdoc}
*/
public function addRelationship($token, $label = NULL) {
if (!$this->hasRelationship($token)) {
$this->relationships[$token] = [
'label' => $label,
];
}
return $this;
}
/**
* {@inheritdoc}
*/
public function replaceRelationship($token, $label) {
if ($this->hasRelationship($token)) {
$this->relationships[$token] = [
'label' => $label,
];
}
return $this;
}
/**
* {@inheritdoc}
*/
public function removeRelationship($token) {
unset($this->relationships[$token]);
return $this;
}
/**
* {@inheritdoc}
*/
public function getRelationships() {
return $this->relationships;
}
/**
* {@inheritdoc}
*/
public function getSelectionConditions() {
if (!$this->selectionConditionCollection) {
$this->selectionConditionCollection = new ConditionPluginCollection(\Drupal::service('plugin.manager.condition'), $this->get('selection_criteria'));
}
return $this->selectionConditionCollection;
}
/**
* {@inheritdoc}
*/
public function addSelectionCondition(array $configuration) {
$configuration['uuid'] = $this->uuidGenerator()->generate();
$this->getSelectionConditions()->addInstanceId($configuration['uuid'], $configuration);
return $configuration['uuid'];
}
/**
* {@inheritdoc}
*/
public function getSelectionCondition($condition_id) {
return $this->getSelectionConditions()->get($condition_id);
}
/**
* {@inheritdoc}
*/
public function removeSelectionCondition($condition_id) {
$this->getSelectionConditions()->removeInstanceId($condition_id);
return $this;
}
/**
* {@inheritdoc}
*/
public function getSelectionLogic() {
return $this->selection_logic;
}
/**
* {@inheritdoc}
*/
public function applies($object) {
if ($this->getAliasType()->applies($object)) {
$definitions = $this->getAliasType()->getContextDefinitions();
if (count($definitions) > 1) {
throw new \Exception("Alias types do not support more than one context.");
}
$keys = array_keys($definitions);
// Set the context object on our Alias plugin before retrieving contexts.
$this->getAliasType()->setContextValue($keys[0], $object);
/** @var \Drupal\Core\Plugin\Context\ContextInterface[] $base_contexts */
$contexts = $this->getContexts();
/** @var \Drupal\Core\Plugin\Context\ContextHandler $context_handler */
$context_handler = \Drupal::service('context.handler');
$conditions = $this->getSelectionConditions();
foreach ($conditions as $condition) {
// 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) {
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;
}
}
@@ -0,0 +1,54 @@
<?php
namespace Drupal\pathauto\EventSubscriber;
use Drupal\Core\Config\ConfigCrudEvent;
use Drupal\Core\Config\ConfigEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\pathauto\AliasTypeManager;
/**
* A subscriber to clear fielddefinition cache when saving pathauto settings.
*/
class PathautoSettingsCacheTag implements EventSubscriberInterface {
protected $entityFieldManager;
protected $aliasTypeManager;
/**
* Constructs a PathautoSettingsCacheTag object.
*/
public function __construct(EntityFieldManagerInterface $entity_field_manager, AliasTypeManager $alias_type_manager) {
$this->entityFieldManager = $entity_field_manager;
$this->aliasTypeManager = $alias_type_manager;
}
/**
* Invalidate the 'rendered' cache tag whenever the settings are modified.
*
* @param \Drupal\Core\Config\ConfigCrudEvent $event
* The Event to process.
*/
public function onSave(ConfigCrudEvent $event) {
if ($event->getConfig()->getName() === 'pathauto.settings') {
$config = $event->getConfig();
$original_entity_types = $config->getOriginal('enabled_entity_types');
// Clear cached field definitions if the values are changed.
if ($original_entity_types != $config->get('enabled_entity_types')) {
$this->entityFieldManager->clearCachedFieldDefinitions();
$this->aliasTypeManager->clearCachedDefinitions();
}
}
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
$events[ConfigEvents::SAVE][] = ['onSave'];
return $events;
}
}
@@ -0,0 +1,185 @@
<?php
namespace Drupal\pathauto\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\pathauto\AliasTypeManager;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Alias mass delete form.
*/
class PathautoAdminDelete extends FormBase {
/**
* The alias type manager.
*
* @var \Drupal\pathauto\AliasTypeManager
*/
protected $aliasTypeManager;
/**
* Constructs a PathautoAdminDelete object.
*
* @param \Drupal\pathauto\AliasTypeManager $alias_type_manager
* The alias type manager.
*/
public function __construct(AliasTypeManager $alias_type_manager) {
$this->aliasTypeManager = $alias_type_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('plugin.manager.alias_type')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'pathauto_admin_delete';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form['delete'] = [
'#type' => 'fieldset',
'#title' => $this->t('Choose aliases to delete'),
'#tree' => TRUE,
];
// First we do the "all" case.
$storage_helper = \Drupal::service('pathauto.alias_storage_helper');
$total_count = $storage_helper->countAll();
$form['delete']['all_aliases'] = [
'#type' => 'checkbox',
'#title' => $this->t('All aliases'),
'#default_value' => FALSE,
'#description' => $this->t('Delete all aliases. Number of aliases which will be deleted: %count.', ['%count' => $total_count]),
];
// Next, iterate over all visible alias types.
$definitions = $this->aliasTypeManager->getVisibleDefinitions();
foreach ($definitions as $id => $definition) {
/** @var \Drupal\pathauto\AliasTypeInterface $alias_type */
$alias_type = $this->aliasTypeManager->createInstance($id);
$count = $storage_helper->countBySourcePrefix($alias_type->getSourcePrefix());
$form['delete']['plugins'][$id] = [
'#type' => 'checkbox',
'#title' => (string) $definition['label'],
'#default_value' => FALSE,
'#description' => $this->t('Delete aliases for all @label. Number of aliases which will be deleted: %count.', ['@label' => (string) $definition['label'], '%count' => $count]),
];
}
$form['options'] = [
'#type' => 'fieldset',
'#title' => $this->t('Delete options'),
'#tree' => TRUE,
];
// Provide checkbox for not deleting custom aliases.
$form['options']['keep_custom_aliases'] = [
'#type' => 'checkbox',
'#title' => $this->t('Only delete automatically generated aliases'),
'#default_value' => TRUE,
'#description' => $this->t('When checked, aliases which have been manually set are not affected by this mass-deletion.'),
];
// Warn them and give a button that shows we mean business.
$form['warning'] = ['#value' => '<p>' . $this->t('<strong>Note:</strong> there is no confirmation. Be sure of your action before clicking the "Delete aliases now!" button.<br />You may want to make a backup of the database and/or the url_alias table prior to using this feature.') . '</p>'];
$form['buttons']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Delete aliases now!'),
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$delete_all = $form_state->getValue(['delete', 'all_aliases']);
// Keeping custom aliases forces us to go the slow way to correctly check
// the automatic/manual flag.
if ($form_state->getValue(['options', 'keep_custom_aliases'])) {
$batch = [
'title' => $this->t('Bulk deleting URL aliases'),
'operations' => [['Drupal\pathauto\Form\PathautoAdminDelete::batchStart', [$delete_all]]],
'finished' => 'Drupal\pathauto\Form\PathautoAdminDelete::batchFinished',
];
if ($delete_all) {
foreach (array_keys($form_state->getValue(['delete', 'plugins'])) as $id) {
$batch['operations'][] = ['Drupal\pathauto\Form\PathautoAdminDelete::batchProcess', [$id]];
}
}
else {
foreach (array_keys(array_filter($form_state->getValue(['delete', 'plugins']))) as $id) {
$batch['operations'][] = ['Drupal\pathauto\Form\PathautoAdminDelete::batchProcess', [$id]];
}
}
batch_set($batch);
}
else if ($delete_all) {
\Drupal::service('pathauto.alias_storage_helper')->deleteAll();
drupal_set_message($this->t('All of your path aliases have been deleted.'));
}
else {
$storage_helper = \Drupal::service('pathauto.alias_storage_helper');
foreach (array_keys(array_filter($form_state->getValue(['delete', 'plugins']))) as $id) {
$alias_type = $this->aliasTypeManager->createInstance($id);
$storage_helper->deleteBySourcePrefix((string) $alias_type->getSourcePrefix());
drupal_set_message($this->t('All of your %label path aliases have been deleted.', ['%label' => $alias_type->getLabel()]));
}
}
}
/**
* Batch callback; record if aliases of all types must be deleted.
*/
public static function batchStart($delete_all, &$context) {
$context['results']['delete_all'] = $delete_all;
$context['results']['deletions'] = [];
}
/**
* Common batch processing callback for all operations.
*/
public static function batchProcess($id, &$context) {
/** @var \Drupal\pathauto\AliasTypeBatchUpdateInterface $alias_type */
$alias_type = \Drupal::service('plugin.manager.alias_type')->createInstance($id);
$alias_type->batchDelete($context);
}
/**
* Batch finished callback.
*/
public static function batchFinished($success, $results, $operations) {
if ($success) {
if ($results['delete_all']) {
drupal_set_message(t('All of your automatically generated path aliases have been deleted.'));
}
else if (isset($results['deletions'])) {
foreach (array_values($results['deletions']) as $label) {
drupal_set_message(t('All of your automatically generated %label path aliases have been deleted.', ['%label' => $label]));
}
}
}
else {
$error_operation = reset($operations);
drupal_set_message(t('An error occurred while processing @operation with arguments : @args', array('@operation' => $error_operation[0], '@args' => print_r($error_operation[0], TRUE))));
}
}
}
@@ -0,0 +1,164 @@
<?php
namespace Drupal\pathauto\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Drupal\pathauto\AliasTypeBatchUpdateInterface;
use Drupal\pathauto\AliasTypeManager;
use Drupal\pathauto\PathautoGeneratorInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Configure file system settings for this site.
*/
class PathautoBulkUpdateForm extends FormBase {
/**
* The alias type manager.
*
* @var \Drupal\pathauto\AliasTypeManager
*/
protected $aliasTypeManager;
/**
* Constructs a PathautoBulkUpdateForm object.
*
* @param \Drupal\pathauto\AliasTypeManager $alias_type_manager
* The alias type manager.
*/
public function __construct(AliasTypeManager $alias_type_manager) {
$this->aliasTypeManager = $alias_type_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('plugin.manager.alias_type')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'pathauto_bulk_update_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form = array();
$form['#update_callbacks'] = array();
$form['update'] = array(
'#type' => 'checkboxes',
'#title' => $this->t('Select the types of paths for which to generate URL aliases'),
'#options' => array(),
'#default_value' => array(),
);
$definitions = $this->aliasTypeManager->getVisibleDefinitions();
foreach ($definitions as $id => $definition) {
$alias_type = $this->aliasTypeManager->createInstance($id);
if ($alias_type instanceof AliasTypeBatchUpdateInterface) {
$form['update']['#options'][$id] = $alias_type->getLabel();
}
}
$form['action'] = array(
'#type' => 'radios',
'#title' => $this->t('Select which URL aliases to generate'),
'#options' => ['create' => $this->t('Generate a URL alias for un-aliased paths only')],
'#default_value' => 'create',
);
$config = $this->config('pathauto.settings');
if ($config->get('update_action') == PathautoGeneratorInterface::UPDATE_ACTION_NO_NEW) {
// Existing aliases should not be updated.
$form['warning'] = array(
'#markup' => $this->t('<a href=":url">Pathauto settings</a> are set to ignore paths which already have a URL alias. You can only create URL aliases for paths having none.', [':url' => Url::fromRoute('pathauto.settings.form')->toString()]),
);
}
else {
$form['action']['#options']['update'] = $this->t('Update the URL alias for paths having an old URL alias');
$form['action']['#options']['all'] = $this->t('Regenerate URL aliases for all paths');
}
$form['actions']['#type'] = 'actions';
$form['actions']['submit'] = array(
'#type' => 'submit',
'#value' => $this->t('Update'),
);
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$batch = array(
'title' => $this->t('Bulk updating URL aliases'),
'operations' => array(
array('Drupal\pathauto\Form\PathautoBulkUpdateForm::batchStart', array()),
),
'finished' => 'Drupal\pathauto\Form\PathautoBulkUpdateForm::batchFinished',
);
$action = $form_state->getValue('action');
foreach ($form_state->getValue('update') as $id) {
if (!empty($id)) {
$batch['operations'][] = array('Drupal\pathauto\Form\PathautoBulkUpdateForm::batchProcess', [$id, $action]);
}
}
batch_set($batch);
}
/**
* Batch callback; initialize the number of updated aliases.
*/
public static function batchStart(&$context) {
$context['results']['updates'] = 0;
}
/**
* Common batch processing callback for all operations.
*
* Required to load our include the proper batch file.
*/
public static function batchProcess($id, $action, &$context) {
/** @var \Drupal\pathauto\AliasTypeBatchUpdateInterface $alias_type */
$alias_type = \Drupal::service('plugin.manager.alias_type')->createInstance($id);
$alias_type->batchUpdate($action, $context);
}
/**
* Batch finished callback.
*/
public static function batchFinished($success, $results, $operations) {
if ($success) {
if ($results['updates']) {
drupal_set_message(\Drupal::translation()->formatPlural($results['updates'], 'Generated 1 URL alias.', 'Generated @count URL aliases.'));
}
else {
drupal_set_message(t('No new URL aliases to generate.'));
}
}
else {
$error_operation = reset($operations);
drupal_set_message(t('An error occurred while processing @operation with arguments : @args', array('@operation' => $error_operation[0], '@args' => print_r($error_operation[0], TRUE))));
}
}
}
@@ -0,0 +1,264 @@
<?php
namespace Drupal\pathauto\Form;
use Drupal\Component\Utility\Html;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Url;
use Drupal\pathauto\AliasTypeManager;
use Drupal\pathauto\PathautoGeneratorInterface;
use Drupal\Core\Form\FormStateInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Configure file system settings for this site.
*/
class PathautoSettingsForm extends ConfigFormBase {
/**
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
*/
protected $entityFieldManager;
/**
* @var \Drupal\pathauto\AliasTypeManager
*/
protected $aliasTypeManager;
/**
* {@inheritDoc}
*/
public function __construct(ConfigFactoryInterface $config_factory, EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager, AliasTypeManager $alias_type_manager) {
parent::__construct($config_factory);
$this->entityTypeManager = $entity_type_manager;
$this->entityFieldManager = $entity_field_manager;
$this->aliasTypeManager = $alias_type_manager;
}
/**
* {@inheritDoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('config.factory'),
$container->get('entity_type.manager'),
$container->get('entity_field.manager'),
$container->get('plugin.manager.alias_type')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'pathauto_settings_form';
}
/**
* {@inheritdoc}
*/
protected function getEditableConfigNames() {
return ['pathauto.settings'];
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$config = $this->config('pathauto.settings');
$form['enabled_entity_types'] = [
'#type' => 'details',
'#open' => TRUE,
'#title' => $this->t('Enabled entity types'),
'#description' => $this->t('Enable to add a path field and allow to define alias patterns for the given type. Disabled types already define a path field themselves or currently have a pattern.'),
'#tree' => TRUE,
];
// Get all applicable entity types.
foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) {
// Disable a checkbox if it already exists and if the entity type has
// patterns currently defined or if it isn't defined by us.
$patterns_count = \Drupal::entityQuery('pathauto_pattern')
->condition('type', 'canonical_entities:' . $entity_type_id)
->count()
->execute();
if (is_subclass_of($entity_type->getClass(), FieldableEntityInterface::class) && $entity_type->hasLinkTemplate('canonical')) {
$field_definitions = $this->entityFieldManager->getBaseFieldDefinitions($entity_type_id);
$form['enabled_entity_types'][$entity_type_id] = [
'#type' => 'checkbox',
'#title' => $entity_type->getLabel(),
'#default_value' => isset($field_definitions['path']) || in_array($entity_type_id, $config->get('enabled_entity_types')),
'#disabled' => isset($field_definitions['path']) && ($field_definitions['path']->getProvider() != 'pathauto' || $patterns_count),
];
}
}
$form['verbose'] = array(
'#type' => 'checkbox',
'#title' => $this->t('Verbose'),
'#default_value' => $config->get('verbose'),
'#description' => $this->t('Display alias changes (except during bulk updates).'),
);
$form['separator'] = array(
'#type' => 'textfield',
'#title' => $this->t('Separator'),
'#size' => 1,
'#maxlength' => 1,
'#default_value' => $config->get('separator'),
'#description' => $this->t('Character used to separate words in titles. This will replace any spaces and punctuation characters. Using a space or + character can cause unexpected results.'),
);
$form['case'] = array(
'#type' => 'checkbox',
'#title' => $this->t('Character case'),
'#default_value' => $config->get('case'),
'#description' => $this->t('Convert token values to lowercase.'),
);
$max_length = \Drupal::service('pathauto.alias_storage_helper')->getAliasSchemaMaxlength();
$help_link = '';
if (\Drupal::moduleHandler()->moduleExists('help')) {
$help_link = ' ' . $this->t('See <a href=":pathauto-help">Pathauto help</a> for details.', [':pathauto-help' => Url::fromRoute('help.page', ['name' => 'pathauto'])->toString()]);
}
$form['max_length'] = array(
'#type' => 'number',
'#title' => $this->t('Maximum alias length'),
'#size' => 3,
'#maxlength' => 3,
'#default_value' => $config->get('max_length'),
'#min' => 1,
'#max' => $max_length,
'#description' => $this->t('Maximum length of aliases to generate. 100 is the recommended length. @max is the maximum possible length.', array('@max' => $max_length)) . $help_link,
);
$form['max_component_length'] = array(
'#type' => 'number',
'#title' => $this->t('Maximum component length'),
'#size' => 3,
'#maxlength' => 3,
'#default_value' => $config->get('max_component_length'),
'#min' => 1,
'#max' => $max_length,
'#description' => $this->t('Maximum text length of any component in the alias (e.g., [title]). 100 is the recommended length. @max is the maximum possible length.', ['@max' => $max_length]) . $help_link,
);
$description = $this->t('What should Pathauto do when updating an existing content item which already has an alias?');
if (\Drupal::moduleHandler()->moduleExists('redirect')) {
$description .= ' ' . $this->t('The <a href=":url">Redirect module settings</a> affect whether a redirect is created when an alias is deleted.', array(':url' => Url::fromRoute('redirect.settings')->toString()));
}
else {
$description .= ' ' . $this->t('Considering installing the <a href=":url">Redirect module</a> to get redirects when your aliases change.', array(':url' => 'http://drupal.org/project/redirect'));
}
$form['update_action'] = array(
'#type' => 'radios',
'#title' => $this->t('Update action'),
'#default_value' => $config->get('update_action'),
'#options' => array(
PathautoGeneratorInterface::UPDATE_ACTION_NO_NEW => $this->t('Do nothing. Leave the old alias intact.'),
PathautoGeneratorInterface::UPDATE_ACTION_LEAVE => $this->t('Create a new alias. Leave the existing alias functioning.'),
PathautoGeneratorInterface::UPDATE_ACTION_DELETE => $this->t('Create a new alias. Delete the old alias.'),
),
'#description' => $description,
);
$form['transliterate'] = array(
'#type' => 'checkbox',
'#title' => $this->t('Transliterate prior to creating alias'),
'#default_value' => $config->get('transliterate'),
'#description' => $this->t('When a pattern includes certain characters (such as those with accents) should Pathauto attempt to transliterate them into the US-ASCII alphabet?'),
);
$form['reduce_ascii'] = array(
'#type' => 'checkbox',
'#title' => $this->t('Reduce strings to letters and numbers'),
'#default_value' => $config->get('reduce_ascii'),
'#description' => $this->t('Filters the new alias to only letters and numbers found in the ASCII-96 set.'),
);
$form['ignore_words'] = array(
'#type' => 'textarea',
'#title' => $this->t('Strings to Remove'),
'#default_value' => $config->get('ignore_words'),
'#description' => $this->t('Words to strip out of the URL alias, separated by commas. Do not use this to remove punctuation.'),
'#wysiwyg' => FALSE,
);
$form['punctuation'] = array(
'#type' => 'details',
'#title' => $this->t('Punctuation'),
'#open' => FALSE,
'#tree' => TRUE,
);
$punctuation = \Drupal::service('pathauto.alias_cleaner')->getPunctuationCharacters();
foreach ($punctuation as $name => $details) {
// Use the value from config if it exists.
if ($config->get('punctuation.' . $name) !== NULL) {
$details['default'] = $config->get('punctuation.' . $name);
}
else {
// Otherwise use the correct default.
$details['default'] = $details['value'] == $config->get('separator') ? PathautoGeneratorInterface::PUNCTUATION_REPLACE : PathautoGeneratorInterface::PUNCTUATION_REMOVE;
}
$form['punctuation'][$name] = array(
'#type' => 'select',
'#title' => $details['name'] . ' (<code>' . Html::escape($details['value']) . '</code>)',
'#default_value' => $details['default'],
'#options' => array(
PathautoGeneratorInterface::PUNCTUATION_REMOVE => $this->t('Remove'),
PathautoGeneratorInterface::PUNCTUATION_REPLACE => $this->t('Replace by separator'),
PathautoGeneratorInterface::PUNCTUATION_DO_NOTHING => $this->t('No action (do not replace)'),
),
);
}
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$config = $this->config('pathauto.settings');
$form_state->cleanValues();
foreach ($form_state->getValues() as $key => $value) {
if ($key == 'enabled_entity_types') {
$enabled_entity_types = [];
foreach ($value as $entity_type_id => $enabled) {
$field_definitions = $this->entityFieldManager->getBaseFieldDefinitions($entity_type_id);
// Verify that the entity type is enabled and that it is not defined
// or defined by us before adding it to the configuration, so that
// we do not store an entity type that cannot be enabled or disabled.
if ($enabled && (!isset($field_definitions['path']) || ($field_definitions['path']->getProvider() === 'pathauto'))) {
$enabled_entity_types[] = $entity_type_id;
}
}
$value = $enabled_entity_types;
}
$config->set($key, $value);
}
$config->save();
parent::submitForm($form, $form_state);
}
}
@@ -0,0 +1,52 @@
<?php
namespace Drupal\pathauto\Form;
use Drupal\Core\Entity\EntityConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
/**
* Provides the pathauto pattern disable disable form.
*/
class PatternDisableForm extends EntityConfirmFormBase {
/**
* {@inheritdoc}
*/
public function getQuestion() {
return $this->t('Are you sure you want to disable the pattern %label?', array('%label' => $this->entity->label()));
}
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
return new Url('entity.pathauto_pattern.collection');
}
/**
* {@inheritdoc}
*/
public function getConfirmText() {
return $this->t('Disable');
}
/**
* {@inheritdoc}
*/
public function getDescription() {
return $this->t('Disabled patterns are ignored when generating aliases.');
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->entity->disable()->save();
drupal_set_message($this->t('Disabled pattern %label.', array('%label' => $this->entity->label())));
$form_state->setRedirectUrl($this->getCancelUrl());
}
}
@@ -0,0 +1,285 @@
<?php
namespace Drupal\pathauto\Form;
use Drupal\Core\Entity\EntityForm;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\pathauto\AliasTypeManager;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Edit form for pathauto patterns.
*/
class PatternEditForm extends EntityForm {
/**
* @var \Drupal\pathauto\AliasTypeManager
*/
protected $manager;
/**
* @var \Drupal\pathauto\PathautoPatternInterface
*/
protected $entity;
/**
* The entity type bundle info service.
*
* @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
*/
protected $entityTypeBundleInfo;
/**
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected $languageManager;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('plugin.manager.alias_type'),
$container->get('entity_type.bundle.info'),
$container->get('entity_type.manager'),
$container->get('language_manager')
);
}
/**
* PatternEditForm constructor.
*
* @param \Drupal\pathauto\AliasTypeManager $manager
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
*/
function __construct(AliasTypeManager $manager, EntityTypeBundleInfoInterface $entity_type_bundle_info, EntityTypeManagerInterface $entity_type_manager, LanguageManagerInterface $language_manager) {
$this->manager = $manager;
$this->entityTypeBundleInfo = $entity_type_bundle_info;
$this->entityTypeManager = $entity_type_manager;
$this->languageManager = $language_manager;
}
/**
* {@inheritDoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$options = [];
foreach ($this->manager->getVisibleDefinitions() as $plugin_id => $plugin_definition) {
$options[$plugin_id] = $plugin_definition['label'];
}
$form['type'] = [
'#type' => 'select',
'#title' => $this->t('Pattern type'),
'#default_value' => $this->entity->getType(),
'#options' => $options,
'#required' => TRUE,
'#limit_validation_errors' => array(array('type')),
'#submit' => array('::submitSelectType'),
'#executes_submit_callback' => TRUE,
'#ajax' => array(
'callback' => '::ajaxReplacePatternForm',
'wrapper' => 'pathauto-pattern',
'method' => 'replace',
),
];
$form['pattern_container'] = [
'#type' => 'container',
'#prefix' => '<div id="pathauto-pattern">',
'#suffix' => '</div>',
];
// if there is no type yet, stop here.
if ($this->entity->getType()) {
$alias_type = $this->entity->getAliasType();
$form['pattern_container']['pattern'] = array(
'#type' => 'textfield',
'#title' => 'Path pattern',
'#default_value' => $this->entity->getPattern(),
'#size' => 65,
'#maxlength' => 1280,
'#element_validate' => array('token_element_validate', 'pathauto_pattern_validate'),
'#after_build' => array('token_element_validate'),
'#token_types' => $alias_type->getTokenTypes(),
'#min_tokens' => 1,
'#required' => TRUE,
);
// Show the token help relevant to this pattern type.
$form['pattern_container']['token_help'] = array(
'#theme' => 'token_tree_link',
'#token_types' => $alias_type->getTokenTypes(),
);
// Expose bundle and language conditions.
if ($alias_type->getDerivativeId() && $entity_type = $this->entityTypeManager->getDefinition($alias_type->getDerivativeId())) {
$default_bundles = [];
$default_languages = [];
foreach ($this->entity->getSelectionConditions() as $condition_id => $condition) {
if (in_array($condition->getPluginId(), ['entity_bundle:' . $entity_type->id(), 'node_type'])) {
$default_bundles = $condition->getConfiguration()['bundles'];
}
elseif ($condition->getPluginId() == 'language') {
$default_languages = $condition->getConfiguration()['langcodes'];
}
}
if ($entity_type->hasKey('bundle') && $bundles = $this->entityTypeBundleInfo->getBundleInfo($entity_type->id())) {
$bundle_options = [];
foreach ($bundles as $id => $info) {
$bundle_options[$id] = $info['label'];
}
$form['pattern_container']['bundles'] = array(
'#title' => $entity_type->getBundleLabel(),
'#type' => 'checkboxes',
'#options' => $bundle_options,
'#default_value' => $default_bundles,
'#description' => $this->t('Check to which types this pattern should be applied. Leave empty to allow any.'),
);
}
if ($this->languageManager->isMultilingual() && $entity_type->isTranslatable()) {
$language_options = [];
foreach ($this->languageManager->getLanguages() as $id => $language) {
$language_options[$id] = $language->getName();
}
$form['pattern_container']['languages'] = array(
'#title' => $this->t('Languages'),
'#type' => 'checkboxes',
'#options' => $language_options,
'#default_value' => $default_languages,
'#description' => $this->t('Check to which languages this pattern should be applied. Leave empty to allow any.'),
);
}
}
}
$form['label'] = array(
'#type' => 'textfield',
'#title' => $this->t('Label'),
'#maxlength' => 255,
'#default_value' => $this->entity->label(),
'#required' => TRUE,
'#description' => $this->t('A short name to help you identify this pattern in the patterns list.'),
);
$form['id'] = array(
'#type' => 'machine_name',
'#title' => $this->t('ID'),
'#maxlength' => 255,
'#default_value' => $this->entity->id(),
'#required' => TRUE,
'#disabled' => !$this->entity->isNew(),
'#machine_name' => array(
'exists' => 'Drupal\pathauto\Entity\PathautoPattern::load',
),
);
$form['status'] = [
'#title' => $this->t('Enabled'),
'#type' => 'checkbox',
'#default_value' => $this->entity->status(),
];
return parent::buildForm($form, $form_state);
}
/**
* {@inheritDoc}
*/
public function buildEntity(array $form, FormStateInterface $form_state) {
/** @var \Drupal\pathauto\PathautoPatternInterface $entity */
$entity = parent::buildEntity($form, $form_state);
// Will only be used for new patterns.
$default_weight = 0;
$alias_type = $entity->getAliasType();
if ($alias_type->getDerivativeId() && $this->entityTypeManager->hasDefinition($alias_type->getDerivativeId())) {
$entity_type = $alias_type->getDerivativeId();
// First, remove bundle and language conditions.
foreach ($entity->getSelectionConditions() as $condition_id => $condition) {
if (in_array($condition->getPluginId(), ['entity_bundle:' . $entity_type, 'node_type', 'language'])) {
$entity->removeSelectionCondition($condition_id);
}
}
if ($bundles = array_filter((array) $form_state->getValue('bundles'))) {
$default_weight -= 5;
$plugin_id = $entity_type == 'node' ? 'node_type' : 'entity_bundle:' . $entity_type;
$entity->addSelectionCondition(
[
'id' => $plugin_id,
'bundles' => $bundles,
'negate' => FALSE,
'context_mapping' => [
$entity_type => $entity_type,
]
]
);
}
if ($languages = array_filter((array) $form_state->getValue('languages'))) {
$default_weight -= 5;
$language_mapping = $entity_type . ':' . $this->entityTypeManager->getDefinition($entity_type)->getKey('langcode') . ':language';
$entity->addSelectionCondition(
[
'id' => 'language',
'langcodes' => array_combine($languages, $languages),
'negate' => FALSE,
'context_mapping' => [
'language' => $language_mapping,
]
]
);
$entity->addRelationship($language_mapping, t('Language'));
}
}
if ($entity->isNew()) {
$entity->setWeight($default_weight);
}
return $entity;
}
/**
* {@inheritDoc}
*/
public function save(array $form, FormStateInterface $form_state) {
parent::save($form, $form_state);
drupal_set_message($this->t('Pattern @label saved.', ['@label' => $this->entity->label()]));
$form_state->setRedirectUrl($this->entity->toUrl('collection'));
}
/**
* Handles switching the type selector.
*/
public function ajaxReplacePatternForm($form, FormStateInterface $form_state) {
return $form['pattern_container'];
}
/**
* Handles submit call when alias type is selected.
*/
public function submitSelectType(array $form, FormStateInterface $form_state) {
$this->entity = $this->buildEntity($form, $form_state);
$form_state->setRebuild();
}
}
@@ -0,0 +1,52 @@
<?php
namespace Drupal\pathauto\Form;
use Drupal\Core\Entity\EntityConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
/**
* Provides the pathauto pattern disable disable form.
*/
class PatternEnableForm extends EntityConfirmFormBase {
/**
* {@inheritdoc}
*/
public function getQuestion() {
return $this->t('Are you sure you want to enable the pattern %label?', array('%label' => $this->entity->label()));
}
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
return new Url('entity.pathauto_pattern.collection');
}
/**
* {@inheritdoc}
*/
public function getConfirmText() {
return $this->t('Enable');
}
/**
* {@inheritdoc}
*/
public function getDescription() {
return '';
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->entity->enable()->save();
drupal_set_message($this->t('Enabled pattern %label.', array('%label' => $this->entity->label())));
$form_state->setRedirectUrl($this->getCancelUrl());
}
}
@@ -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);
}
@@ -0,0 +1,49 @@
<?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);
}
}
/**
* @inheritDoc
*/
public function delete() {
\Drupal::service('pathauto.alias_storage_helper')->deleteEntityPathAll($this->getEntity());
$this->first()->get('pathauto')->purge();
}
}
@@ -0,0 +1,365 @@
<?php
namespace Drupal\pathauto;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\RevisionableInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\Core\Utility\Token;
use Drupal\token\TokenEntityMapperInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
/**
* Provides methods for generating path aliases.
*/
class PathautoGenerator implements PathautoGeneratorInterface {
use StringTranslationTrait;
/**
* Config factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* Module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* Token service.
*
* @var \Drupal\Core\Utility\Token
*/
protected $token;
/**
* Calculated pattern for a specific entity.
*
* @var array
*/
protected $patterns = array();
/**
* Available patterns per entity type ID.
*
* @var array
*/
protected $patternsByEntityType = array();
/**
* The alias cleaner.
*
* @var \Drupal\pathauto\AliasCleanerInterface
*/
protected $aliasCleaner;
/**
* The alias storage helper.
*
* @var \Drupal\pathauto\AliasStorageHelperInterface
*/
protected $aliasStorageHelper;
/**
* The alias uniquifier.
*
* @var \Drupal\pathauto\AliasUniquifierInterface
*/
protected $aliasUniquifier;
/**
* The messenger service.
*
* @var \Drupal\pathauto\MessengerInterface
*/
protected $messenger;
/**
* @var \Drupal\token\TokenEntityMapperInterface
*/
protected $tokenEntityMapper;
/**
* @var Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Creates a new Pathauto manager.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\Utility\Token $token
* The token utility.
* @param \Drupal\pathauto\AliasCleanerInterface $alias_cleaner
* The alias cleaner.
* @param \Drupal\pathauto\AliasStorageHelperInterface $alias_storage_helper
* The alias storage helper.
* @param AliasUniquifierInterface $alias_uniquifier
* The alias uniquifier.
* @param MessengerInterface $messenger
* The messenger service.
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* The string translation service.
* @param Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager
*/
public function __construct(ConfigFactoryInterface $config_factory, ModuleHandlerInterface $module_handler, Token $token, AliasCleanerInterface $alias_cleaner, AliasStorageHelperInterface $alias_storage_helper, AliasUniquifierInterface $alias_uniquifier, MessengerInterface $messenger, TranslationInterface $string_translation, TokenEntityMapperInterface $token_entity_mappper, EntityTypeManagerInterface $entity_type_manager) {
$this->configFactory = $config_factory;
$this->moduleHandler = $module_handler;
$this->token = $token;
$this->aliasCleaner = $alias_cleaner;
$this->aliasStorageHelper = $alias_storage_helper;
$this->aliasUniquifier = $alias_uniquifier;
$this->messenger = $messenger;
$this->stringTranslation = $string_translation;
$this->tokenEntityMapper = $token_entity_mappper;
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@inheritdoc}
*/
public function createEntityAlias(EntityInterface $entity, $op) {
// Retrieve and apply the pattern for this content type.
$pattern = $this->getPatternByEntity($entity);
if (empty($pattern)) {
// No pattern? Do nothing (otherwise we may blow away existing aliases...)
return NULL;
}
$source = '/' . $entity->toUrl()->getInternalPath();
$config = $this->configFactory->get('pathauto.settings');
$langcode = $entity->language()->getId();
// Core does not handle aliases with language Not Applicable.
if ($langcode == LanguageInterface::LANGCODE_NOT_APPLICABLE) {
$langcode = LanguageInterface::LANGCODE_NOT_SPECIFIED;
}
// Build token data.
$data = [
$this->tokenEntityMapper->getTokenTypeForEntityType($entity->getEntityTypeId()) => $entity,
];
// Allow other modules to alter the pattern.
$context = array(
'module' => $entity->getEntityType()->getProvider(),
'op' => $op,
'source' => $source,
'data' => $data,
'bundle' => $entity->bundle(),
'language' => &$langcode,
);
// @todo Is still hook still useful?
$this->moduleHandler->alter('pathauto_pattern', $pattern, $context);
// Special handling when updating an item which is already aliased.
$existing_alias = NULL;
if ($op == 'update' || $op == 'bulkupdate') {
if ($existing_alias = $this->aliasStorageHelper->loadBySource($source, $langcode)) {
switch ($config->get('update_action')) {
case PathautoGeneratorInterface::UPDATE_ACTION_NO_NEW:
// If an alias already exists,
// and the update action is set to do nothing,
// then gosh-darn it, do nothing.
return NULL;
}
}
}
// Replace any tokens in the pattern.
// Uses callback option to clean replacements. No sanitization.
// Pass empty BubbleableMetadata object to explicitly ignore cacheablity,
// as the result is never rendered.
$alias = $this->token->replace($pattern->getPattern(), $data, array(
'clear' => TRUE,
'callback' => array($this->aliasCleaner, 'cleanTokenValues'),
'langcode' => $langcode,
'pathauto' => TRUE,
), new BubbleableMetadata());
// Check if the token replacement has not actually replaced any values. If
// that is the case, then stop because we should not generate an alias.
// @see token_scan()
$pattern_tokens_removed = preg_replace('/\[[^\s\]:]*:[^\s\]]*\]/', '', $pattern->getPattern());
if ($alias === $pattern_tokens_removed) {
return NULL;
}
$alias = $this->aliasCleaner->cleanAlias($alias);
// Allow other modules to alter the alias.
$context['source'] = &$source;
$context['pattern'] = $pattern;
$this->moduleHandler->alter('pathauto_alias', $alias, $context);
// If we have arrived at an empty string, discontinue.
if (!Unicode::strlen($alias)) {
return NULL;
}
// If the alias already exists, generate a new, hopefully unique, variant.
$original_alias = $alias;
$this->aliasUniquifier->uniquify($alias, $source, $langcode);
if ($original_alias != $alias) {
// Alert the user why this happened.
$this->messenger->addMessage($this->t('The automatically generated alias %original_alias conflicted with an existing alias. Alias changed to %alias.', array(
'%original_alias' => $original_alias,
'%alias' => $alias,
)), $op);
}
// Return the generated alias if requested.
if ($op == 'return') {
return $alias;
}
// Build the new path alias array and send it off to be created.
$path = array(
'source' => $source,
'alias' => $alias,
'language' => $langcode,
);
return $this->aliasStorageHelper->save($path, $existing_alias, $op);
}
/**
* Loads pathauto patterns for a given entity type ID
*
* @param string $entity_type_id
* An entity type ID.
*
* @return \Drupal\pathauto\PathautoPatternInterface[]
* A list of patterns, sorted by weight.
*/
protected function getPatternByEntityType($entity_type_id) {
if (!isset($this->patternsByEntityType[$entity_type_id])) {
$ids = \Drupal::entityQuery('pathauto_pattern')
->condition('type', array_keys(\Drupal::service('plugin.manager.alias_type')
->getPluginDefinitionByType($this->tokenEntityMapper->getTokenTypeForEntityType($entity_type_id))))
->condition('status', 1)
->sort('weight')
->execute();
$this->patternsByEntityType[$entity_type_id] = \Drupal::entityTypeManager()
->getStorage('pathauto_pattern')
->loadMultiple($ids);
}
return $this->patternsByEntityType[$entity_type_id];
}
/**
* {@inheritdoc}
*/
public function getPatternByEntity(EntityInterface $entity) {
$langcode = $entity->language()->getId();
if (!isset($this->patterns[$entity->getEntityTypeId()][$entity->id()][$langcode])) {
foreach ($this->getPatternByEntityType($entity->getEntityTypeId()) as $pattern) {
if ($pattern->applies($entity)) {
$this->patterns[$entity->getEntityTypeId()][$entity->id()][$langcode] = $pattern;
break;
}
}
// If still not set.
if (!isset($this->patterns[$entity->getEntityTypeId()][$entity->id()][$langcode])) {
$this->patterns[$entity->getEntityTypeId()][$entity->id()][$langcode] = NULL;
}
}
return $this->patterns[$entity->getEntityTypeId()][$entity->id()][$langcode];
}
/**
* {@inheritdoc}
*/
public function resetCaches() {
$this->patterns = [];
$this->patternsByEntityType = [];
$this->aliasCleaner->resetCaches();
}
/**
* {@inheritdoc}
*/
public function updateEntityAlias(EntityInterface $entity, $op, array $options = array()) {
// Skip if the entity does not have the path field.
if (!($entity instanceof ContentEntityInterface) || !$entity->hasField('path')) {
return NULL;
}
// Skip if pathauto processing is disabled.
if ($entity->path->pathauto != PathautoState::CREATE && empty($options['force'])) {
return NULL;
}
// Only act if this is the default revision.
if ($entity instanceof RevisionableInterface && !$entity->isDefaultRevision()) {
return NULL;
}
$options += array('language' => $entity->language()->getId());
$type = $entity->getEntityTypeId();
// Skip processing if the entity has no pattern.
if (!$this->getPatternByEntity($entity)) {
return NULL;
}
// Deal with taxonomy specific logic.
// @todo Update and test forum related code.
if ($type == 'taxonomy_term') {
$config_forum = $this->configFactory->get('forum.settings');
if ($entity->getVocabularyId() == $config_forum->get('vocabulary')) {
$type = 'forum';
}
}
try {
$result = $this->createEntityAlias($entity, $op);
}
catch (\InvalidArgumentException $e) {
drupal_set_message($e->getMessage(), 'error');
return NULL;
}
// @todo Move this to a method on the pattern plugin.
if ($type == 'taxonomy_term') {
foreach ($this->loadTermChildren($entity->id()) as $subterm) {
$this->updateEntityAlias($subterm, $op, $options);
}
}
return $result;
}
/**
* Finds all children of a term ID.
*
* @param int $tid
* Term ID to retrieve parents for.
*
* @return \Drupal\taxonomy\TermInterface[]
* An array of term objects that are the children of the term $tid.
*/
protected function loadTermChildren($tid) {
return $this->entityTypeManager->getStorage('taxonomy_term')->loadChildren($tid);
}
}
@@ -0,0 +1,89 @@
<?php
namespace Drupal\pathauto;
use Drupal\Core\Entity\EntityInterface;
/**
* Provides and interface for PathautoGenerator.
*/
interface PathautoGeneratorInterface {
/**
* "Do nothing. Leave the old alias intact."
*/
const UPDATE_ACTION_NO_NEW = 0;
/**
* "Create a new alias. Leave the existing alias functioning."
*/
const UPDATE_ACTION_LEAVE = 1;
/**
* "Create a new alias. Delete the old alias."
*/
const UPDATE_ACTION_DELETE = 2;
/**
* Remove the punctuation from the alias.
*/
const PUNCTUATION_REMOVE = 0;
/**
* Replace the punctuation with the separator in the alias.
*/
const PUNCTUATION_REPLACE = 1;
/**
* Leave the punctuation as it is in the alias.
*/
const PUNCTUATION_DO_NOTHING = 2;
/**
* Resets internal caches.
*/
public function resetCaches();
/**
* Load an alias pattern entity by entity, bundle, and language.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* An entity.
* @return \Drupal\pathauto\PathautoPatternInterface|null
*/
public function getPatternByEntity(EntityInterface $entity);
/**
* Apply patterns to create an alias.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity.
* @param string $op
* Operation being performed on the content being aliased
* ('insert', 'update', 'return', or 'bulkupdate').
*
* @return array|string
* The alias that was created.
*
* @see _pathauto_set_alias()
*/
public function createEntityAlias(EntityInterface $entity, $op);
/**
* Creates or updates an alias for the given entity.
*
* @param EntityInterface $entity
* Entity for which to update the alias.
* @param string $op
* The operation performed (insert, update)
* @param array $options
* - force: will force updating the path
* - language: the language for which to create the alias
*
* @return array|null
* - An array with alias data in case the alias has been created or updated.
* - NULL if no operation performed.
*/
public function updateEntityAlias(EntityInterface $entity, $op, array $options = array());
}
@@ -0,0 +1,51 @@
<?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);
}
else {
\Drupal::service('pathauto.generator')->updateEntityAlias($this->getEntity(), $update ? 'update' : 'insert');
}
$this->get('pathauto')->persist();
}
/**
* {@inheritdoc}
*/
public function isEmpty() {
// Make sure that the pathauto state flag does not get lost if just that is
// changed.
return !$this->alias && !$this->get('pathauto')->hasValue();
}
}
@@ -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);
}
@@ -0,0 +1,51 @@
<?php
namespace Drupal\pathauto;
use Drupal\Core\Config\Entity\DraggableListBuilder;
use Drupal\Core\Entity\EntityInterface;
/**
* Provides a listing of Pathauto pattern entities.
*/
class PathautoPatternListBuilder extends DraggableListBuilder {
/**
* {@inheritdoc}
*/
protected $limit = FALSE;
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'pathauto_pattern_list';
}
/**
* {@inheritdoc}
*/
public function buildHeader() {
$header['label'] = $this->t('Label');
$header['pattern'] = $this->t('Pattern');
$header['type'] = $this->t('Pattern type');
$header['conditions'] = $this->t('Conditions');
return $header + parent::buildHeader();
}
/**
* {@inheritdoc}
*/
public function buildRow(EntityInterface $entity) {
/* @var \Drupal\pathauto\PathautoPatternInterface $entity */
$row['label'] = $entity->label();
$row['patern']['#markup'] = $entity->getPattern();
$row['type']['#markup'] = $entity->getAliasType()->getLabel();
$row['conditions']['#theme'] = 'item_list';
foreach ($entity->getSelectionConditions() as $condition) {
$row['conditions']['#items'][] = $condition->summary();
}
return $row + parent::buildRow($entity);
}
}
@@ -0,0 +1,158 @@
<?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;
/**
* {@inheritdoc}
*/
public function getValue() {
if ($this->value === NULL) {
// If no value has been set or loaded yet, try to load a value if this
// entity has already been saved.
$this->value = \Drupal::keyValue($this->getCollection())
->get(static::getPathautoStateKey($this->parent->getEntity()->id()));
// If it was not yet saved or no value was found, then set the flag to
// create the alias if there is a matching pattern.
if ($this->value === NULL) {
$entity = $this->parent->getEntity();
$pattern = \Drupal::service('pathauto.generator')->getPatternByEntity($entity);
$this->value = !empty($pattern) ? static::CREATE : static::SKIP;
}
}
return $this->value;
}
/**
* {@inheritdoc}
*/
public function setValue($value, $notify = TRUE) {
$this->value = $value;
// Notify the parent of any changes.
if ($notify && isset($this->parent)) {
$this->parent->onChange($this->name);
}
}
/**
* Returns TRUE if a value was set.
*/
public function hasValue() {
return $this->value !== NULL;
}
/**
* Persists the state.
*/
public function persist() {
\Drupal::keyValue($this->getCollection())
->set(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);
}
}
@@ -0,0 +1,71 @@
<?php
namespace Drupal\pathauto;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\path\Plugin\Field\FieldWidget\PathWidget;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
/**
* Extends the core path widget.
*/
class PathautoWidget extends PathWidget {
/**
* {@inheritdoc}
*/
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
$element = parent::formElement($items, $delta, $element, $form, $form_state);
$entity = $items->getEntity();
// Taxonomy terms do not have an actual fieldset for path settings.
// Merge in the defaults.
// @todo Impossible to do this in widget, use another solution
/*
$form['path'] += array(
'#type' => 'fieldset',
'#title' => $this->t('URL path settings'),
'#collapsible' => TRUE,
'#collapsed' => empty($form['path']['alias']),
'#group' => 'additional_settings',
'#attributes' => array(
'class' => array('path-form'),
),
'#access' => \Drupal::currentUser()->hasPermission('create url aliases') || \Drupal::currentUser()->hasPermission('administer url aliases'),
'#weight' => 30,
'#tree' => TRUE,
'#element_validate' => array('path_form_element_validate'),
);*/
$pattern = \Drupal::service('pathauto.generator')->getPatternByEntity($entity);
if (empty($pattern)) {
return $element;
}
if (\Drupal::currentUser()->hasPermission('administer pathauto')) {
$description = $this->t('Uncheck this to create a custom alias below. <a href="@admin_link">Configure URL alias patterns.</a>', ['@admin_link' => Url::fromRoute('entity.pathauto_pattern.collection')->toString()]);
}
else {
$description = $this->t('Uncheck this to create a custom alias below.');
}
$element['pathauto'] = array(
'#type' => 'checkbox',
'#title' => $this->t('Generate automatic URL alias'),
'#default_value' => $entity->path->pathauto,
'#description' => $description,
'#weight' => -1,
);
// Add JavaScript that will disable the path textfield when the automatic
// alias checkbox is checked.
$element['alias']['#states']['disabled']['input[name="path[' . $delta . '][pathauto]"]'] = array('checked' => TRUE);
// Override path.module's vertical tabs summary.
$element['alias']['#attached']['library'] = ['pathauto/widget'];
return $element;
}
}
@@ -0,0 +1,36 @@
<?php
namespace Drupal\pathauto\Plugin\Action;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Action\ActionBase;
use Drupal\Core\Session\AccountInterface;
use Drupal\pathauto\PathautoState;
/**
* Pathauto entity update action.
*
* @Action(
* id = "pathauto_update_alias",
* label = @Translation("Update URL alias of an entity"),
* )
*/
class UpdateAction extends ActionBase {
/**
* {@inheritdoc}
*/
public function execute($entity = NULL) {
$entity->path->pathauto = PathautoState::CREATE;
\Drupal::service('pathauto.generator')->updateEntityAlias($entity, 'bulkupdate', array('message' => TRUE));
}
/**
* {@inheritdoc}
*/
public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
$result = AccessResult::allowedIfHasPermission($account, 'create url aliases');
return $return_as_object ? $result : $result->isAllowed();
}
}
@@ -0,0 +1,94 @@
<?php
namespace Drupal\pathauto\Plugin\Deriver;
use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Plugin\Context\ContextDefinition;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\token\TokenEntityMapperInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Deriver that exposes content entities as alias type plugins.
*/
class EntityAliasTypeDeriver extends DeriverBase implements ContainerDeriverInterface {
use StringTranslationTrait;
/**
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
*/
protected $entityFieldManager;
/**
* @var \Drupal\token\TokenEntityMapperInterface
*/
protected $tokenEntityMapper;
/**
* Constructs new EntityAliasTypeDeriver.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
* The entity field manager.
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* The string translation service.
* @apram \Drupal\Token\TokenEntityMapperInterface $token_entity_mapper
* The token entity mapper.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager, TranslationInterface $string_translation, TokenEntityMapperInterface $token_entity_mapper) {
$this->entityTypeManager = $entity_type_manager;
$this->entityFieldManager = $entity_field_manager;
$this->stringTranslation = $string_translation;
$this->tokenEntityMapper = $token_entity_mapper;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, $base_plugin_id) {
return new static(
$container->get('entity_type.manager'),
$container->get('entity_field.manager'),
$container->get('string_translation'),
$container->get('token.entity_mapper')
);
}
/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) {
// An entity type must have a canonical link template and support fields.
if ($entity_type->hasLinkTemplate('canonical') && is_subclass_of($entity_type->getClass(), FieldableEntityInterface::class)) {
$base_fields = $this->entityFieldManager->getBaseFieldDefinitions($entity_type_id);
if (!isset($base_fields['path'])) {
// The entity type does not have a path field and is therefore not
// supported.
continue;
}
$this->derivatives[$entity_type_id] = $base_plugin_definition;
$this->derivatives[$entity_type_id]['label'] = $entity_type->getLabel();
$this->derivatives[$entity_type_id]['types'] = [$this->tokenEntityMapper->getTokenTypeForEntityType($entity_type_id)];
$this->derivatives[$entity_type_id]['provider'] = $entity_type->getProvider();
$this->derivatives[$entity_type_id]['context'] = [
$entity_type_id => new ContextDefinition("entity:$entity_type_id", $this->t('@label being aliased', ['@label' => $entity_type->getLabel()]))
];
}
}
return $this->derivatives;
}
}
@@ -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');
}
}
@@ -0,0 +1,334 @@
<?php
namespace Drupal\pathauto\Plugin\pathauto\AliasType;
use Drupal\Core\Database\Connection;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\KeyValueStore\KeyValueFactoryInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Plugin\Context\Context;
use Drupal\Core\Plugin\ContextAwarePluginBase;
use Drupal\pathauto\AliasTypeBatchUpdateInterface;
use Drupal\pathauto\AliasTypeInterface;
use Drupal\pathauto\PathautoState;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* A pathauto alias type plugin for entities with canonical links.
*
* @AliasType(
* id = "canonical_entities",
* deriver = "\Drupal\pathauto\Plugin\Deriver\EntityAliasTypeDeriver"
* )
*/
class EntityAliasTypeBase extends ContextAwarePluginBase implements AliasTypeInterface, AliasTypeBatchUpdateInterface, ContainerFactoryPluginInterface {
/**
* The module handler service.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The language manager service.
*
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected $languageManager;
/**
* The entity manager service.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The key/value manager service.
*
* @var \Drupal\Core\KeyValueStore\KeyValueFactoryInterface
*/
protected $keyValue;
/**
* The database connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected $database;
/**
* The path prefix for this entity type.
*
* @var string
*/
protected $prefix;
/**
* Constructs a EntityAliasTypeBase instance.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler service.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager service.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity manager service.
* @param \Drupal\Core\KeyValueStore\KeyValueFactoryInterface $key_value
* The key/value manager service.
* @param \Drupal\Core\Database\Connection $database
* The database connection.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, ModuleHandlerInterface $module_handler, LanguageManagerInterface $language_manager, EntityTypeManagerInterface $entity_type_manager, KeyValueFactoryInterface $key_value, Connection $database) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->moduleHandler = $module_handler;
$this->languageManager = $language_manager;
$this->entityTypeManager = $entity_type_manager;
$this->keyValue = $key_value;
$this->database = $database;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('module_handler'),
$container->get('language_manager'),
$container->get('entity_type.manager'),
$container->get('keyvalue'),
$container->get('database')
);
}
/**
* {@inheritdoc}
*/
public function getLabel() {
$definition = $this->getPluginDefinition();
// Cast the admin label to a string since it is an object.
// @see \Drupal\Core\StringTranslation\TranslationWrapper
return (string) $definition['label'];
}
/**
* {@inheritdoc}
*/
public function getTokenTypes() {
$definition = $this->getPluginDefinition();
return $definition['types'];
}
/**
* {@inheritdoc}
*/
public function batchUpdate($action, &$context) {
if (!isset($context['sandbox']['current'])) {
$context['sandbox']['count'] = 0;
$context['sandbox']['current'] = 0;
}
$entity_type = $this->entityTypeManager->getDefinition($this->getEntityTypeId());
$id_key = $entity_type->getKey('id');
$query = $this->database->select($entity_type->get('base_table'), 'base_table');
$query->leftJoin('url_alias', 'ua', "CONCAT('" . $this->getSourcePrefix() . "' , base_table.$id_key) = ua.source");
$query->addField('base_table', $id_key, 'id');
switch ($action) {
case 'create':
$query->isNull('ua.source');
break;
case 'update':
$query->isNotNull('ua.source');
break;
case 'all':
// Nothing to do. We want all paths.
break;
default:
// Unknown action. Abort!
return;
}
$query->condition('base_table.' . $id_key, $context['sandbox']['current'], '>');
$query->orderBy('base_table.' . $id_key);
$query->addTag('pathauto_bulk_update');
$query->addMetaData('entity', $this->getEntityTypeId());
// Get the total amount of items to process.
if (!isset($context['sandbox']['total'])) {
$context['sandbox']['total'] = $query->countQuery()->execute()->fetchField();
// If there are no entities to update, then stop immediately.
if (!$context['sandbox']['total']) {
$context['finished'] = 1;
return;
}
}
$query->range(0, 25);
$ids = $query->execute()->fetchCol();
$updates = $this->bulkUpdate($ids);
$context['sandbox']['count'] += count($ids);
$context['sandbox']['current'] = max($ids);
$context['results']['updates'] += $updates;
$context['message'] = $this->t('Updated alias for %label @id.', array('%label' => $entity_type->getLabel(), '@id' => end($ids)));
if ($context['sandbox']['count'] != $context['sandbox']['total']) {
$context['finished'] = $context['sandbox']['count'] / $context['sandbox']['total'];
}
}
/**
* {@inheritdoc}
*/
public function batchDelete(&$context) {
if (!isset($context['sandbox']['current'])) {
$context['sandbox']['count'] = 0;
$context['sandbox']['current'] = 0;
}
$entity_type = $this->entityTypeManager->getDefinition($this->getEntityTypeId());
$id_key = $entity_type->getKey('id');
$query = $this->database->select($entity_type->get('base_table'), 'base_table');
$query->innerJoin('url_alias', 'ua', "CONCAT('" . $this->getSourcePrefix() . "' , base_table.$id_key) = ua.source");
$query->addField('base_table', $id_key, 'id');
$query->addField('ua', 'pid');
$query->condition('ua.pid', $context['sandbox']['current'], '>');
$query->orderBy('ua.pid');
$query->addTag('pathauto_bulk_delete');
$query->addMetaData('entity', $this->getEntityTypeId());
// Get the total amount of items to process.
if (!isset($context['sandbox']['total'])) {
$context['sandbox']['total'] = $query->countQuery()->execute()->fetchField();
// If there are no entities to delete, then stop immediately.
if (!$context['sandbox']['total']) {
$context['finished'] = 1;
return;
}
}
$query->range(0, 100);
$pids_by_id = $query->execute()->fetchAllKeyed();
PathautoState::bulkDelete($this->getEntityTypeId(), $pids_by_id);
$context['sandbox']['count'] += count($pids_by_id);
$context['sandbox']['current'] = max($pids_by_id);
$context['results']['deletions'][] = $this->getLabel();
if ($context['sandbox']['count'] != $context['sandbox']['total']) {
$context['finished'] = $context['sandbox']['count'] / $context['sandbox']['total'];
}
}
/**
* Returns the entity type ID.
*
* @return string
* The entity type ID.
*/
protected function getEntityTypeId() {
return $this->getDerivativeId();
}
/**
* Update the URL aliases for multiple entities.
*
* @param array $ids
* An array of entity IDs.
* @param array $options
* An optional array of additional options.
*
* @return int
* The number of updated URL aliases.
*/
protected function bulkUpdate(array $ids, array $options = array()) {
$options += array('message' => FALSE);
$updates = 0;
$entities = $this->entityTypeManager->getStorage($this->getEntityTypeId())->loadMultiple($ids);
foreach ($entities as $entity) {
// Update aliases for the entity's default language and its translations.
foreach ($entity->getTranslationLanguages() as $langcode => $language) {
$translated_entity = $entity->getTranslation($langcode);
$result = \Drupal::service('pathauto.generator')->updateEntityAlias($translated_entity, 'bulkupdate', $options);
if ($result) {
$updates++;
}
}
}
if (!empty($options['message'])) {
drupal_set_message(\Drupal::translation()->formatPlural(count($ids), 'Updated 1 %label URL alias.', 'Updated @count %label URL aliases.'), array('%label' => $this->getLabel()));
}
return $updates;
}
/**
* Deletes the URL aliases for multiple entities.
*
* @param int[] $pids_by_id
* A list of path IDs keyed by entity ID.
*
* @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;
}
}
@@ -0,0 +1,106 @@
<?php
namespace Drupal\pathauto\Plugin\pathauto\AliasType;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Database\Connection;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\KeyValueStore\KeyValueFactoryInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* A pathauto alias type plugin for forum terms.
*
* @AliasType(
* id = "forum",
* label = @Translation("Forum"),
* types = {"term"},
* provider = "forum",
* context = {
* "taxonomy_term" = @ContextDefinition("entity:taxonomy_term")
* }
* )
*/
class ForumAliasType extends EntityAliasTypeBase implements ContainerFactoryPluginInterface {
/**
* The config factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* Constructs a ForumAliasType instance.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler service.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager service.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity manager service.
* @param \Drupal\Core\KeyValueStore\KeyValueFactoryInterface $key_value
* The key/value manager service.
* @param \Drupal\Core\Database\Connection $database
* The database service.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, ModuleHandlerInterface $module_handler, LanguageManagerInterface $language_manager, EntityTypeManagerInterface $entity_type_manager, KeyValueFactoryInterface $key_value, Connection $database, ConfigFactoryInterface $config_factory) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $module_handler, $language_manager, $entity_type_manager, $key_value, $database);
$this->configFactory = $config_factory;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('module_handler'),
$container->get('language_manager'),
$container->get('entity_type.manager'),
$container->get('keyvalue'),
$container->get('database'),
$container->get('config.factory')
);
}
/**
* {@inheritdoc}
*/
protected function getEntityTypeId() {
return 'taxonomy_term';
}
/**
* {@inheritdoc}
*/
public function getSourcePrefix() {
return '/forum/';
}
/**
* {@inheritdoc}
*/
public function applies($object) {
if (parent::applies($object)) {
/** @var \Drupal\taxonomy\TermInterface $object */
$vid = $this->configFactory->get('forum.settings')->get('vocabulary');
return $object->getVocabularyId() == $vid;
}
return FALSE;
}
}
@@ -0,0 +1,156 @@
<?php
namespace Drupal\pathauto\Tests;
use Drupal\pathauto\PathautoGeneratorInterface;
use Drupal\pathauto\PathautoState;
use Drupal\simpletest\WebTestBase;
/**
* Bulk update functionality tests.
*
* @group pathauto
*/
class PathautoBulkUpdateTest extends WebTestBase {
use PathautoTestHelperTrait;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('node', 'pathauto', 'forum');
/**
* Admin user.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* The created nodes.
*
* @var \Drupal\node\NodeInterface
*/
protected $nodes;
/**
* The created patterns.
*
* @var \Drupal\pathauto\PathautoPatternInterface
*/
protected $patterns;
/**
* {inheritdoc}
*/
function setUp() {
parent::setUp();
// Allow other modules to add additional permissions for the admin user.
$permissions = array(
'administer pathauto',
'administer url aliases',
'create url aliases',
'administer forums',
);
$this->adminUser = $this->drupalCreateUser($permissions);
$this->drupalLogin($this->adminUser);
$this->patterns = array();
$this->patterns['node'] = $this->createPattern('node', '/content/[node:title]');
$this->patterns['user'] = $this->createPattern('user', '/users/[user:name]');
$this->patterns['forum'] = $this->createPattern('forum', '/forums/[term:name]');
}
function testBulkUpdate() {
// Create some nodes.
$this->nodes = array();
for ($i = 1; $i <= 5; $i++) {
$node = $this->drupalCreateNode();
$this->nodes[$node->id()] = $node;
}
// Clear out all aliases.
$this->deleteAllAliases();
// Bulk create aliases.
$edit = array(
'update[canonical_entities:node]' => TRUE,
'update[canonical_entities:user]' => TRUE,
'update[forum]' => TRUE,
);
$this->drupalPostForm('admin/config/search/path/update_bulk', $edit, t('Update'));
// This has generated 8 aliases: 5 nodes, 2 users and 1 forum.
$this->assertText('Generated 8 URL aliases.');
// Check that aliases have actually been created.
foreach ($this->nodes as $node) {
$this->assertEntityAliasExists($node);
}
$this->assertEntityAliasExists($this->adminUser);
// This is the default "General discussion" forum.
$this->assertAliasExists(['source' => '/taxonomy/term/1']);
// Add a new node.
$new_node = $this->drupalCreateNode(array('path' => array('alias' => '', 'pathauto' => PathautoState::SKIP)));
// Run the update again which should not run against any nodes.
$this->drupalPostForm('admin/config/search/path/update_bulk', $edit, t('Update'));
$this->assertText('No new URL aliases to generate.');
$this->assertNoEntityAliasExists($new_node);
// Make sure existing aliases can be overriden.
$this->drupalPostForm('admin/config/search/path/settings', ['update_action' => PathautoGeneratorInterface::UPDATE_ACTION_DELETE], t('Save configuration'));
// Patterns did not change, so no aliases should be regenerated.
$edit['action'] = 'all';
$this->drupalPostForm('admin/config/search/path/update_bulk', $edit, t('Update'));
$this->assertText('No new URL aliases to generate.');
// Update the node pattern, and leave other patterns alone. Existing nodes should get a new alias,
// except the node above whose alias is manually set. Other aliases must be left alone.
$this->patterns['node']->delete();
$this->patterns['node'] = $this->createPattern('node', '/archive/node-[node:nid]');
$this->drupalPostForm('admin/config/search/path/update_bulk', $edit, t('Update'));
$this->assertText('Generated 5 URL aliases.');
// Prevent existing aliases to be overriden. The bulk generate page should only offer
// to create an alias for paths which have none.
$this->drupalPostForm('admin/config/search/path/settings', ['update_action' => PathautoGeneratorInterface::UPDATE_ACTION_NO_NEW], t('Save configuration'));
$this->drupalGet('admin/config/search/path/update_bulk');
$this->assertFieldByName('action', 'create');
$this->assertText('Pathauto settings are set to ignore paths which already have a URL alias.');
$this->assertNoFieldByName('action', 'update');
$this->assertNoFieldByName('action', 'all');
}
/**
* Tests alias generation for nodes that existed before installing Pathauto.
*/
function testBulkUpdateExistingContent() {
// Create a node.
$node = $this->drupalCreateNode();
// Delete its alias and Pathauto metadata.
\Drupal::service('pathauto.alias_storage_helper')->deleteEntityPathAll($node);
$node->path->first()->get('pathauto')->purge();
\Drupal::entityTypeManager()->getStorage('node')->resetCache(array($node->id()));
// Execute bulk generation.
// Bulk create aliases.
$edit = array(
'update[canonical_entities:node]' => TRUE,
);
$this->drupalPostForm('admin/config/search/path/update_bulk', $edit, t('Update'));
// Verify that the alias was created for the node.
$this->assertText('Generated 1 URL alias.');
}
}
@@ -0,0 +1,87 @@
<?php
namespace Drupal\pathauto\Tests;
use Drupal\simpletest\WebTestBase;
use Drupal\comment\Tests\CommentTestTrait;
/**
* Tests pathauto settings form.
*
* @group pathauto
*/
class PathautoEnablingEntityTypesTest extends WebTestBase {
use PathautoTestHelperTrait;
use CommentTestTrait;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('node', 'pathauto', 'comment');
/**
* Admin user.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* {inheritdoc}
*/
function setUp() {
parent::setUp();
$this->drupalCreateContentType(array('type' => 'article'));
$this->addDefaultCommentField('node', 'article');
$permissions = array(
'administer pathauto',
'administer url aliases',
'create url aliases',
'administer nodes',
'post comments',
);
$this->adminUser = $this->drupalCreateUser($permissions);
$this->drupalLogin($this->adminUser);
}
/**
* A suite of tests to verify if the feature to enable and disable the
* ability to define alias patterns for a given entity type works. Test with
* the comment module, as it is not enabled by default.
*/
function testEnablingEntityTypes() {
// Verify that the comment entity type is not available when trying to add
// a new pattern, nor "broken".
$this->drupalGet('/admin/config/search/path/patterns/add');
$this->assertEqual(count($this->cssSelect('option[value = "canonical_entities:comment"]:contains(Comment)')), 0);
$this->assertEqual(count($this->cssSelect('option:contains(Broken)')), 0);
// Enable the entity type and create a pattern for it.
$this->drupalGet('/admin/config/search/path/settings');
$edit = [
'enabled_entity_types[comment]' => TRUE,
];
$this->drupalPostForm(NULL, $edit, 'Save configuration');
$this->createPattern('comment', '/comment/[comment:body]');
// Create a node, a comment type and a comment entity.
$node = $this->drupalCreateNode(['type' => 'article']);
$this->drupalGet('/node/' . $node->id());
$edit = [
'comment_body[0][value]' => 'test-body',
];
$this->drupalPostForm(NULL, $edit, 'Save');
// Verify that an alias has been generated and that the type can no longer
// be disabled.
$this->assertAliasExists(['alias' => '/comment/test-body']);
$this->drupalGet('/admin/config/search/path/settings');
$this->assertEqual(count($this->cssSelect('input[name = "enabled_entity_types[comment]"][disabled = "disabled"]')), 1);
}
}
@@ -0,0 +1,216 @@
<?php
namespace Drupal\pathauto\Tests;
use Drupal\Core\Language\Language;
use Drupal\Core\Language\LanguageInterface;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\pathauto\PathautoState;
use Drupal\simpletest\WebTestBase;
/**
* Test pathauto functionality with localization and translation.
*
* @group pathauto
*/
class PathautoLocaleTest extends WebTestBase {
use PathautoTestHelperTrait;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('node', 'pathauto', 'locale', 'content_translation');
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
// Create Article node type.
$this->drupalCreateContentType(array('type' => 'article', 'name' => 'Article'));
}
/**
* Test that when an English node is updated, its old English alias is
* updated and its newer French alias is left intact.
*/
function testLanguageAliases() {
$this->createPattern('node', '/content/[node:title]');
// Add predefined French language.
ConfigurableLanguage::createFromLangcode('fr')->save();
$node = array(
'title' => 'English node',
'langcode' => 'en',
'path' => array(array(
'alias' => '/english-node',
'pathauto' => FALSE,
)),
);
$node = $this->drupalCreateNode($node);
$english_alias = \Drupal::service('path.alias_storage')->load(array('alias' => '/english-node', 'langcode' => 'en'));
$this->assertTrue($english_alias, 'Alias created with proper language.');
// Also save a French alias that should not be left alone, even though
// it is the newer alias.
$this->saveEntityAlias($node, '/french-node', 'fr');
// Add an alias with the soon-to-be generated alias, causing the upcoming
// alias update to generate a unique alias with the '-0' suffix.
$this->saveAlias('/node/invalid', '/content/english-node', Language::LANGCODE_NOT_SPECIFIED);
// Update the node, triggering a change in the English alias.
$node->path->pathauto = PathautoState::CREATE;
$node->save();
// Check that the new English alias replaced the old one.
$this->assertEntityAlias($node, '/content/english-node-0', 'en');
$this->assertEntityAlias($node, '/french-node', 'fr');
$this->assertAliasExists(array('pid' => $english_alias['pid'], 'alias' => '/content/english-node-0'));
// Create a new node with the same title as before but without
// specifying a language.
$node = $this->drupalCreateNode(array('title' => 'English node', 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED));
// Check that the new node had a unique alias generated with the '-0'
// suffix.
$this->assertEntityAlias($node, '/content/english-node-0', LanguageInterface::LANGCODE_NOT_SPECIFIED);
}
/**
* Test that patterns work on multilingual content.
*/
function testLanguagePatterns() {
// Allow other modules to add additional permissions for the admin user.
$permissions = array(
'administer pathauto',
'administer url aliases',
'create url aliases',
'bypass node access',
'access content overview',
'administer languages',
'translate any entity',
'administer content translation'
);
$admin_user = $this->drupalCreateUser($permissions);
$this->drupalLogin($admin_user);
// Add French language.
$edit = array(
'predefined_langcode' => 'fr',
);
$this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add language'));
$this->enableArticleTranslation();
// Create a pattern for English articles.
$this->drupalGet('admin/config/search/path/patterns/add');
$edit = array(
'type' => 'canonical_entities:node',
);
$this->drupalPostAjaxForm(NULL, $edit, 'type');
$edit += array(
'pattern' => '/the-articles/[node:title]',
'label' => 'English articles',
'id' => 'english_articles',
'bundles[article]' => TRUE,
'languages[en]' => TRUE,
);
$this->drupalPostForm(NULL, $edit, 'Save');
$this->assertText('Pattern English articles saved.');
// Create a pattern for French articles.
$this->drupalGet('admin/config/search/path/patterns/add');
$edit = array(
'type' => 'canonical_entities:node',
);
$this->drupalPostAjaxForm(NULL, $edit, 'type');
$edit += array(
'pattern' => '/les-articles/[node:title]',
'label' => 'French articles',
'id' => 'french_articles',
'bundles[article]' => TRUE,
'languages[fr]' => TRUE,
);
$this->drupalPostForm(NULL, $edit, 'Save');
$this->assertText('Pattern French articles saved.');
// Create a node and its translation. Assert aliases.
$edit = array(
'title[0][value]' => 'English node',
'langcode[0][value]' => 'en',
);
$this->drupalPostForm('node/add/article', $edit, t('Save'));
$english_node = $this->drupalGetNodeByTitle('English node');
return;
$this->assertAlias('/node/' . $english_node->id(), '/the-articles/english-node', 'en');
$this->drupalGet('node/' . $english_node->id() . '/translations');
$this->clickLink(t('Add'));
$edit = array(
'title[0][value]' => 'French node',
);
$this->drupalPostForm(NULL, $edit, t('Save (this translation)'));
$this->rebuildContainer();
$english_node = $this->drupalGetNodeByTitle('English node');
$french_node = $english_node->getTranslation('fr');
$this->assertAlias('/node/' . $french_node->id(), '/les-articles/french-node', 'fr');
// Bulk delete and Bulk generate patterns. Assert aliases.
$this->deleteAllAliases();
// Bulk create aliases.
$edit = array(
'update[canonical_entities:node]' => TRUE,
);
$this->drupalPostForm('admin/config/search/path/update_bulk', $edit, t('Update'));
$this->assertText(t('Generated 2 URL aliases.'));
$this->assertAlias('/node/' . $english_node->id(), '/the-articles/english-node', 'en');
$this->assertAlias('/node/' . $french_node->id(), '/les-articles/french-node', 'fr');
}
/**
* Tests the alias created for a node with language Not Applicable.
*/
public function testLanguageNotApplicable() {
$this->drupalLogin($this->rootUser);
$this->enableArticleTranslation();
// Create a pattern for nodes.
$pattern = $this->createPattern('node', '/content/[node:title]', -1);
$pattern->save();
// Create a node with language Not Applicable.
$node = $this->createNode(['type' => 'article', 'title' => 'Test node', 'langcode' => LanguageInterface::LANGCODE_NOT_APPLICABLE]);
// Check that the generated alias has language Not Specified.
$alias = \Drupal::service('pathauto.alias_storage_helper')->loadBySource('/node/' . $node->id());
$this->assertEqual($alias['langcode'], LanguageInterface::LANGCODE_NOT_SPECIFIED, 'PathautoGenerator::createEntityAlias() adjusts the alias langcode from Not Applicable to Not Specified.');
// Check that the alias works.
$this->drupalGet('content/test-node');
$this->assertResponse(200);
}
/**
* Enables content translation on articles.
*/
protected function enableArticleTranslation() {
// Enable content translation on articles.
$this->drupalGet('admin/config/regional/content-language');
$edit = array(
'entity_types[node]' => TRUE,
'settings[node][article][translatable]' => TRUE,
'settings[node][article][settings][language][language_alterable]' => TRUE,
);
$this->drupalPostForm(NULL, $edit, t('Save configuration'));
}
}
@@ -0,0 +1,200 @@
<?php
namespace Drupal\pathauto\Tests;
use Drupal\pathauto\PathautoState;
use Drupal\simpletest\WebTestBase;
/**
* Mass delete functionality tests.
*
* @group pathauto
*/
class PathautoMassDeleteTest extends WebTestBase {
use PathautoTestHelperTrait;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('node', 'taxonomy', 'pathauto');
/**
* Admin user.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* The test nodes.
*
* @var \Drupal\node\NodeInterface
*/
protected $nodes;
/**
* The test accounts.
*
* @var \Drupal\user\UserInterface
*/
protected $accounts;
/**
* The test terms.
*
* @var \Drupal\taxonomy\TermInterface
*/
protected $terms;
/**
* {inheritdoc}
*/
function setUp() {
parent::setUp();
$permissions = array(
'administer pathauto',
'administer url aliases',
'create url aliases',
);
$this->adminUser = $this->drupalCreateUser($permissions);
$this->drupalLogin($this->adminUser);
$this->createPattern('node', '/content/[node:title]');
$this->createPattern('user', '/users/[user:name]');
$this->createPattern('taxonomy_term', '/[term:vocabulary]/[term:name]');
}
/**
* Tests the deletion of all the aliases.
*/
function testDeleteAll() {
// 1. Test that deleting all the aliases, of any type, works.
$this->generateAliases();
$edit = array(
'delete[all_aliases]' => TRUE,
'options[keep_custom_aliases]' => FALSE,
);
$this->drupalPostForm('admin/config/search/path/delete_bulk', $edit, t('Delete aliases now!'));
$this->assertText(t('All of your path aliases have been deleted.'));
$this->assertUrl('admin/config/search/path/delete_bulk');
// Make sure that all of them are actually deleted.
$aliases = \Drupal::database()->select('url_alias', 'ua')->fields('ua', array())->execute()->fetchAll();
$this->assertEqual($aliases, array(), "All the aliases have been deleted.");
// 2. Test deleting only specific (entity type) aliases.
$manager = $this->container->get('plugin.manager.alias_type');
$pathauto_plugins = array('canonical_entities:node' => 'nodes', 'canonical_entities:taxonomy_term' => 'terms', 'canonical_entities:user' => 'accounts');
foreach ($pathauto_plugins as $pathauto_plugin => $attribute) {
$this->generateAliases();
$edit = array(
'delete[plugins][' . $pathauto_plugin . ']' => TRUE,
'options[keep_custom_aliases]' => FALSE,
);
$this->drupalPostForm('admin/config/search/path/delete_bulk', $edit, t('Delete aliases now!'));
$alias_type = $manager->createInstance($pathauto_plugin);
$this->assertRaw(t('All of your %label path aliases have been deleted.', array('%label' => $alias_type->getLabel())));
// Check that the aliases were actually deleted.
foreach ($this->{$attribute} as $entity) {
$this->assertNoEntityAlias($entity);
}
// Check that the other aliases are not deleted.
foreach ($pathauto_plugins as $_pathauto_plugin => $_attribute) {
// Skip the aliases that should be deleted.
if ($_pathauto_plugin == $pathauto_plugin) {
continue;
}
foreach ($this->{$_attribute} as $entity) {
$this->assertEntityAliasExists($entity);
}
}
}
// 3. Test deleting automatically generated aliases only.
$this->generateAliases();
$edit = array(
'delete[all_aliases]' => TRUE,
'options[keep_custom_aliases]' => TRUE,
);
$this->drupalPostForm('admin/config/search/path/delete_bulk', $edit, t('Delete aliases now!'));
$this->assertText(t('All of your automatically generated path aliases have been deleted.'));
$this->assertUrl('admin/config/search/path/delete_bulk');
// Make sure that only custom aliases and aliases with no information about
// their state still exist.
$aliases = \Drupal::database()->select('url_alias', 'ua')->fields('ua', ['source'])->execute()->fetchCol();
$this->assertEqual($aliases, ['/node/101', '/node/104', '/node/105'], 'Custom aliases still exist.');
}
/**
* Helper function to generate aliases.
*/
function generateAliases() {
// Delete all aliases to avoid duplicated aliases. They will be recreated below.
$this->deleteAllAliases();
// We generate a bunch of aliases for nodes, users and taxonomy terms. If
// the entities are already created we just update them, otherwise we create
// them.
if (empty($this->nodes)) {
// Create a large number of nodes (100+) to make sure that the batch code works.
for ($i = 1; $i <= 105; $i++) {
// Set the alias of two nodes manually.
$settings = ($i > 103) ? ['path' => ['alias' => "/custom_alias_$i", 'pathauto' => PathautoState::SKIP]] : [];
$node = $this->drupalCreateNode($settings);
$this->nodes[$node->id()] = $node;
}
}
else {
foreach ($this->nodes as $node) {
if ($node->id() > 103) {
// The alias is set manually.
$node->set('path', ['alias' => '/custom_alias_' . $node->id()]);
}
$node->save();
}
}
// Delete information about the state of an alias to make sure that aliases
// with no such data are left alone by default.
\Drupal::keyValue('pathauto_state.node')->delete(101);
if (empty($this->accounts)) {
for ($i = 1; $i <= 5; $i++) {
$account = $this->drupalCreateUser();
$this->accounts[$account->id()] = $account;
}
}
else {
foreach ($this->accounts as $id => $account) {
$account->save();
}
}
if (empty($this->terms)) {
$vocabulary = $this->addVocabulary(array('name' => 'test vocabulary', 'vid' => 'test_vocabulary'));
for ($i = 1; $i <= 5; $i++) {
$term = $this->addTerm($vocabulary);
$this->terms[$term->id()] = $term;
}
}
else {
foreach ($this->terms as $term) {
$term->save();
}
}
// Check that we have aliases for the entities.
foreach (array('nodes', 'accounts', 'terms') as $attribute) {
foreach ($this->{$attribute} as $entity) {
$this->assertEntityAliasExists($entity);
}
}
}
}
@@ -0,0 +1,291 @@
<?php
namespace Drupal\pathauto\Tests;
use Drupal\pathauto\Entity\PathautoPattern;
use Drupal\node\Entity\Node;
use Drupal\pathauto\PathautoState;
use Drupal\simpletest\WebTestBase;
/**
* Tests pathauto node UI integration.
*
* @group pathauto
*/
class PathautoNodeWebTest extends WebTestBase {
use PathautoTestHelperTrait;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('node', 'pathauto', 'views', 'taxonomy', 'pathauto_views_test');
/**
* Admin user.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* {inheritdoc}
*/
function setUp() {
parent::setUp();
$this->drupalCreateContentType(array('type' => 'page', 'name' => 'Basic page'));
$this->drupalCreateContentType(array('type' => 'article'));
// Allow other modules to add additional permissions for the admin user.
$permissions = array(
'administer pathauto',
'administer url aliases',
'create url aliases',
'bypass node access',
'access content overview',
);
$this->adminUser = $this->drupalCreateUser($permissions);
$this->drupalLogin($this->adminUser);
$this->createPattern('node', '/content/[node:title]');
}
/**
* Tests editing nodes with different settings.
*/
function testNodeEditing() {
// Ensure that the Pathauto checkbox is checked by default on the node add form.
$this->drupalGet('node/add/page');
$this->assertFieldChecked('edit-path-0-pathauto');
// Create a node by saving the node form.
$title = ' Testing: node title [';
$automatic_alias = '/content/testing-node-title';
$this->drupalPostForm(NULL, array('title[0][value]' => $title), t('Save'));
$node = $this->drupalGetNodeByTitle($title);
// Look for alias generated in the form.
$this->drupalGet("node/{$node->id()}/edit");
$this->assertFieldChecked('edit-path-0-pathauto');
$this->assertFieldByName('path[0][alias]', $automatic_alias, 'Generated alias visible in the path alias field.');
// Check whether the alias actually works.
$this->drupalGet($automatic_alias);
$this->assertText($title, 'Node accessible through automatic alias.');
// Manually set the node's alias.
$manual_alias = '/content/' . $node->id();
$edit = array(
'path[0][pathauto]' => FALSE,
'path[0][alias]' => $manual_alias,
);
$this->drupalPostForm($node->toUrl('edit-form'), $edit, t('Save'));
$this->assertText(t('@type @title has been updated.', array('@type' => 'page', '@title' => $title)));
// Check that the automatic alias checkbox is now unchecked by default.
$this->drupalGet("node/{$node->id()}/edit");
$this->assertNoFieldChecked('edit-path-0-pathauto');
$this->assertFieldByName('path[0][alias]', $manual_alias);
// Submit the node form with the default values.
$this->drupalPostForm(NULL, array('path[0][pathauto]' => FALSE), t('Save'));
$this->assertText(t('@type @title has been updated.', array('@type' => 'page', '@title' => $title)));
// Test that the old (automatic) alias has been deleted and only accessible
// through the new (manual) alias.
$this->drupalGet($automatic_alias);
$this->assertResponse(404, 'Node not accessible through automatic alias.');
$this->drupalGet($manual_alias);
$this->assertText($title, 'Node accessible through manual alias.');
// Test that the manual alias is not kept for new nodes when the pathauto
// checkbox is ticked.
$title = 'Automatic Title';
$edit = array(
'title[0][value]' => $title,
'path[0][pathauto]' => TRUE,
'path[0][alias]' => '/should-not-get-created',
);
$this->drupalPostForm('node/add/page', $edit, t('Save'));
$this->assertNoAliasExists(array('alias' => 'should-not-get-created'));
$node = $this->drupalGetNodeByTitle($title);
$this->assertEntityAlias($node, '/content/automatic-title');
// Remove the pattern for nodes, the pathauto checkbox should not be
// displayed.
$ids = \Drupal::entityQuery('pathauto_pattern')
->condition('type', 'canonical_entities:node')
->execute();
foreach (PathautoPattern::loadMultiple($ids) as $pattern) {
$pattern->delete();
}
$this->drupalGet('node/add/article');
$this->assertNoFieldById('edit-path-0-pathauto');
$this->assertFieldByName('path[0][alias]', '');
$edit = array();
$edit['title'] = 'My test article';
$this->drupalCreateNode($edit);
//$this->drupalPostForm(NULL, $edit, t('Save'));
$node = $this->drupalGetNodeByTitle($edit['title']);
// Pathauto checkbox should still not exist.
$this->drupalGet($node->toUrl('edit-form'));
$this->assertNoFieldById('edit-path-0-pathauto');
$this->assertFieldByName('path[0][alias]', '');
$this->assertNoEntityAlias($node);
}
/**
* Test node operations.
*/
function testNodeOperations() {
$node1 = $this->drupalCreateNode(array('title' => 'node1'));
$node2 = $this->drupalCreateNode(array('title' => 'node2'));
// Delete all current URL aliases.
$this->deleteAllAliases();
$this->drupalGet('admin/content');
// Check which of the two nodes is first.
if (strpos($this->getTextContent(), 'node1') < strpos($this->getTextContent(), 'node2')) {
$index = 0;
}
else {
$index = 1;
}
$edit = array(
'action' => 'pathauto_update_alias_node',
'node_bulk_form[' . $index . ']' => TRUE,
);
$this->drupalPostForm(NULL, $edit, t('Apply to selected items'));
$this->assertText('Update URL alias was applied to 1 item.');
$this->assertEntityAlias($node1, '/content/' . $node1->getTitle());
$this->assertEntityAlias($node2, '/node/' . $node2->id());
}
/**
* @todo Merge this with existing node test methods?
*/
public function testNodeState() {
$nodeNoAliasUser = $this->drupalCreateUser(array('bypass node access'));
$nodeAliasUser = $this->drupalCreateUser(array('bypass node access', 'create url aliases'));
$node = $this->drupalCreateNode(array(
'title' => 'Node version one',
'type' => 'page',
'path' => array(
'pathauto' => PathautoState::SKIP,
),
));
$this->assertNoEntityAlias($node);
// Set a manual path alias for the node.
$node->path->alias = '/test-alias';
$node->save();
// Ensure that the pathauto field was saved to the database.
\Drupal::entityTypeManager()->getStorage('node')->resetCache();
$node = Node::load($node->id());
$this->assertIdentical($node->path->pathauto, PathautoState::SKIP);
// Ensure that the manual path alias was saved and an automatic alias was not generated.
$this->assertEntityAlias($node, '/test-alias');
$this->assertNoEntityAliasExists($node, '/content/node-version-one');
// Save the node as a user who does not have access to path fieldset.
$this->drupalLogin($nodeNoAliasUser);
$this->drupalGet('node/' . $node->id() . '/edit');
$this->assertNoFieldByName('path[0][pathauto]');
$edit = array('title[0][value]' => 'Node version two');
$this->drupalPostForm(NULL, $edit, 'Save');
$this->assertText('Basic page Node version two has been updated.');
$this->assertEntityAlias($node, '/test-alias');
$this->assertNoEntityAliasExists($node, '/content/node-version-one');
$this->assertNoEntityAliasExists($node, '/content/node-version-two');
// Load the edit node page and check that the Pathauto checkbox is unchecked.
$this->drupalLogin($nodeAliasUser);
$this->drupalGet('node/' . $node->id() . '/edit');
$this->assertNoFieldChecked('edit-path-0-pathauto');
// Edit the manual alias and save the node.
$edit = array(
'title[0][value]' => 'Node version three',
'path[0][alias]' => '/manually-edited-alias',
);
$this->drupalPostForm(NULL, $edit, 'Save');
$this->assertText('Basic page Node version three has been updated.');
$this->assertEntityAlias($node, '/manually-edited-alias');
$this->assertNoEntityAliasExists($node, '/test-alias');
$this->assertNoEntityAliasExists($node, '/content/node-version-one');
$this->assertNoEntityAliasExists($node, '/content/node-version-two');
$this->assertNoEntityAliasExists($node, '/content/node-version-three');
// Programatically save the node with an automatic alias.
\Drupal::entityTypeManager()->getStorage('node')->resetCache();
$node = Node::load($node->id());
$node->path->pathauto = PathautoState::CREATE;
$node->save();
// Ensure that the pathauto field was saved to the database.
\Drupal::entityTypeManager()->getStorage('node')->resetCache();
$node = Node::load($node->id());
$this->assertIdentical($node->path->pathauto, PathautoState::CREATE);
$this->assertEntityAlias($node, '/content/node-version-three');
$this->assertNoEntityAliasExists($node, '/manually-edited-alias');
$this->assertNoEntityAliasExists($node, '/test-alias');
$this->assertNoEntityAliasExists($node, '/content/node-version-one');
$this->assertNoEntityAliasExists($node, '/content/node-version-two');
$node->delete();
$this->assertNull(\Drupal::keyValue('pathauto_state.node')->get($node->id()), 'Pathauto state was deleted');
}
/**
* Tests that nodes without a Pathauto pattern can set custom aliases.
*/
public function testCustomAliasWithoutPattern() {
// First, delete all patterns to be sure that there will be no match.
$entity_ids = \Drupal::entityQuery('pathauto_pattern')->execute();
$entities = PathautoPattern::loadMultiple($entity_ids);
foreach ($entities as $entity) {
$entity->delete();
}
// Next, create a node with a custom alias.
$edit = [
'title[0][value]' => 'Sample article',
'path[0][alias]' => '/sample-article',
];
$this->drupalPostForm('node/add/article', $edit, t('Save'));
$this->assertText(t('article Sample article has been created.'));
// Test the alias.
$this->assertAliasExists(array('alias' => '/sample-article'));
$this->drupalGet('sample-article');
$this->assertResponse(200, 'A node without a pattern can have a custom alias.');
// Now create a node through the API.
$node = Node::create(['type' => 'article', 'title' => 'Sample article API', 'path' => ['alias' => '/sample-article-api']]);
$node->save();
// Test the alias.
$this->assertAliasExists(['alias' => '/sample-article-api']);
$this->drupalGet('sample-article-api');
$this->assertResponse(200);
}
}
@@ -0,0 +1,239 @@
<?php
namespace Drupal\pathauto\Tests;
use Drupal\pathauto\PathautoGeneratorInterface;
use Drupal\simpletest\WebTestBase;
/**
* Tests pathauto settings form.
*
* @group pathauto
*/
class PathautoSettingsFormWebTest extends WebTestBase {
use PathautoTestHelperTrait;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('node', 'pathauto');
/**
* Admin user.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* Form values that are set by default.
*
* @var array
*/
protected $defaultFormValues = array(
'verbose' => FALSE,
'separator' => '-',
'case' => '1',
'max_length' => '100',
'max_component_length' => '100',
'update_action' => '2',
'transliterate' => '1',
'reduce_ascii' => FALSE,
'ignore_words' => 'a, an, as, at, before, but, by, for, from, is, in, into, like, of, off, on, onto, per, since, than, the, this, that, to, up, via, with',
);
/**
* Punctuation form items with default values.
*
* @var array
*/
protected $defaultPunctuations = array(
'punctuation[double_quotes]' => '0',
'punctuation[quotes]' => '0',
'punctuation[backtick]' => '0',
'punctuation[comma]' => '0',
'punctuation[period]' => '0',
'punctuation[hyphen]' => '1',
'punctuation[underscore]' => '0',
'punctuation[colon]' => '0',
'punctuation[semicolon]' => '0',
'punctuation[pipe]' => '0',
'punctuation[left_curly]' => '0',
'punctuation[left_square]' => '0',
'punctuation[right_curly]' => '0',
'punctuation[right_square]' => '0',
'punctuation[plus]' => '0',
'punctuation[equal]' => '0',
'punctuation[asterisk]' => '0',
'punctuation[ampersand]' => '0',
'punctuation[percent]' => '0',
'punctuation[caret]' => '0',
'punctuation[dollar]' => '0',
'punctuation[hash]' => '0',
'punctuation[exclamation]' => '0',
'punctuation[tilde]' => '0',
'punctuation[left_parenthesis]' => '0',
'punctuation[right_parenthesis]' => '0',
'punctuation[question_mark]' => '0',
'punctuation[less_than]' => '0',
'punctuation[greater_than]' => '0',
'punctuation[slash]' => '0',
'punctuation[back_slash]' => '0',
);
/**
* {inheritdoc}
*/
function setUp() {
parent::setUp();
$this->drupalCreateContentType(array('type' => 'article'));
$permissions = array(
'administer pathauto',
'notify of path changes',
'administer url aliases',
'create url aliases',
'bypass node access',
);
$this->adminUser = $this->drupalCreateUser($permissions);
$this->drupalLogin($this->adminUser);
$this->createPattern('node', '/content/[node:title]');
}
/**
* Test if the default values are shown correctly in the form.
*/
function testDefaultFormValues() {
$this->drupalGet('/admin/config/search/path/settings');
$this->assertNoFieldChecked('edit-verbose');
$this->assertField('edit-separator', $this->defaultFormValues['separator']);
$this->assertFieldChecked('edit-case');
$this->assertField('edit-max-length', $this->defaultFormValues['max_length']);
$this->assertField('edit-max-component-length', $this->defaultFormValues['max_component_length']);
$this->assertFieldChecked('edit-update-action-2');
$this->assertFieldChecked('edit-transliterate');
$this->assertNoFieldChecked('edit-reduce-ascii');
$this->assertField('edit-ignore-words', $this->defaultFormValues['ignore_words']);
}
/**
* Test the verbose option.
*/
function testVerboseOption() {
$edit = array('verbose' => '1');
$this->drupalPostForm('/admin/config/search/path/settings', $edit, t('Save configuration'));
$this->assertText(t('The configuration options have been saved.'));
$this->assertFieldChecked('edit-verbose');
$title = 'Verbose settings test';
$this->drupalGet('/node/add/article');
$this->assertFieldChecked('edit-path-0-pathauto');
$this->drupalPostForm(NULL, array('title[0][value]' => $title), t('Save'));
$this->assertText('Created new alias /content/verbose-settings-test for');
$node = $this->drupalGetNodeByTitle($title);
$this->drupalPostForm('/node/' . $node->id() . '/edit', array('title[0][value]' => 'Updated title'), t('Save'));
$this->assertText('Created new alias /content/updated-title for');
$this->assertText('replacing /content/verbose-settings-test.');
}
/**
* Tests generating aliases with different settings.
*/
function testSettingsForm() {
// Ensure the separator settings apply correctly.
$this->checkAlias('My awesome content', '/content/my.awesome.content', array('separator' => '.'));
// Ensure the character case setting works correctly.
// Leave case the same as source token values.
$this->checkAlias('My awesome Content', '/content/My-awesome-Content', array('case' => FALSE));
$this->checkAlias('Change Lower', '/content/change-lower', array('case' => '1'));
// Ensure the maximum alias length is working.
$this->checkAlias('My awesome Content', '/content/my-awesome', array('max_length' => '23'));
// Ensure the maximum component length is working.
$this->checkAlias('My awesome Content', '/content/my', array('max_component_length' => '2'));
// Ensure transliteration option is working.
$this->checkAlias('è é àl ö äl ü', '/content/e-e-al-o-al-u', array('transliterate' => '1'));
$this->checkAlias('è é àl äl ö ü', '/content/è-é-àl-äl-ö-ü', array('transliterate' => FALSE));
$ignore_words = 'a, new, very, should';
$this->checkAlias('a very new alias to test', '/content/alias-to-test', array('ignore_words' => $ignore_words));
}
/**
* Test the punctuation setting form items.
*/
function testPunctuationSettings() {
// Test the replacement of punctuations.
$settings = [];
foreach ($this->defaultPunctuations as $key => $punctuation) {
$settings[$key] = PathautoGeneratorInterface::PUNCTUATION_REPLACE;
}
$title = 'aa"b`c,d.e-f_g:h;i|j{k[l}m]n+o=p*q%r^s$t#u!v~w(x)y?z>1/2\3';
$alias = '/content/aa-b-c-d-e-f-g-h-i-j-k-l-m-n-o-p-q-r-s-t-u-v-w-x-y-z-1-2-3';
$this->checkAlias($title, $alias, $settings);
// Test the removal of punctuations.
$settings = [];
foreach ($this->defaultPunctuations as $key => $punctuation) {
$settings[$key] = PathautoGeneratorInterface::PUNCTUATION_REMOVE;
}
$title = 'a"b`c,d.e-f_g:h;i|j{k[l}m]n+o=p*q%r^s$t#u!v~w(x)y?z>1/2\3';
$alias = '/content/abcdefghijklmnopqrstuvwxyz123';
$this->checkAlias($title, $alias, $settings);
// Keep all punctuations in alias.
$settings = [];
foreach ($this->defaultPunctuations as $key => $punctuation) {
$settings[$key] = PathautoGeneratorInterface::PUNCTUATION_DO_NOTHING;
}
$title = 'al"b`c,d.e-f_g:h;i|j{k[l}m]n+o=p*q%r^s$t#u!v~w(x)y?z>1/2\3';
$alias = '/content/al"b`c,d.e-f_g:h;i|j{k[l}m]n+o=p*q%r^s$t#u!v~w(x)y?z>1/2\3';
$this->checkAlias($title, $alias, $settings);
}
/**
* Helper method to check the an aliases.
*
* @param string $title
* The node title to build the aliases from.
* @param string $alias
* The expected alias.
* @param array $settings
* The form values the alias should be generated with.
*/
protected function checkAlias($title, $alias, $settings = array()) {
// Submit the settings form.
$edit = array_merge($this->defaultFormValues + $this->defaultPunctuations, $settings);
$this->drupalPostForm('/admin/config/search/path/settings', $edit, t('Save configuration'));
$this->assertText(t('The configuration options have been saved.'));
// If we do not clear the caches here, AliasCleaner will use its
// cleanStringCache instance variable. Due to that the creation of aliases
// with $this->createNode() will only work correctly on the first call.
\Drupal::service('pathauto.generator')->resetCaches();
// Create a node and check if the settings applied.
$node = $this->createNode(
array(
'title' => $title,
'type' => 'article',
)
);
$this->drupalGet($alias);
$this->assertResponse(200);
$this->assertEntityAlias($node, $alias);
}
}
@@ -0,0 +1,102 @@
<?php
namespace Drupal\pathauto\Tests;
use Drupal\simpletest\WebTestBase;
/**
* Tests pathauto taxonomy UI integration.
*
* @group pathauto
*/
class PathautoTaxonomyWebTest extends WebTestBase {
use PathautoTestHelperTrait;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('taxonomy', 'pathauto', 'views');
/**
* Admin user.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* {inheritdoc}
*/
function setUp() {
parent::setUp();
// Allow other modules to add additional permissions for the admin user.
$permissions = array(
'administer pathauto',
'administer url aliases',
'create url aliases',
'administer taxonomy',
);
$this->adminUser = $this->drupalCreateUser($permissions);
$this->drupalLogin($this->adminUser);
$this->createPattern('taxonomy_term', '/[term:vocabulary]/[term:name]');
}
/**
* Basic functional testing of Pathauto with taxonomy terms.
*/
function testTermEditing() {
$this->drupalGet('admin/structure');
$this->drupalGet('admin/structure/taxonomy');
// Add vocabulary "tags".
$vocabulary = $this->addVocabulary(array('name' => 'tags', 'vid' => 'tags'));
// Create term for testing.
$name = 'Testing: term name [';
$automatic_alias = '/tags/testing-term-name';
$this->drupalPostForm('admin/structure/taxonomy/manage/tags/add', array('name[0][value]' => $name), 'Save');
$name = trim($name);
$this->assertText("Created new term $name.");
$term = $this->drupalGetTermByName($name);
// Look for alias generated in the form.
$this->drupalGet("taxonomy/term/{$term->id()}/edit");
$this->assertFieldChecked('edit-path-0-pathauto');
$this->assertFieldByName('path[0][alias]', $automatic_alias, 'Generated alias visible in the path alias field.');
// Check whether the alias actually works.
$this->drupalGet($automatic_alias);
$this->assertText($name, 'Term accessible through automatic alias.');
// Manually set the term's alias.
$manual_alias = '/tags/' . $term->id();
$edit = array(
'path[0][pathauto]' => FALSE,
'path[0][alias]' => $manual_alias,
);
$this->drupalPostForm("taxonomy/term/{$term->id()}/edit", $edit, t('Save'));
$this->assertText("Updated term $name.");
// Check that the automatic alias checkbox is now unchecked by default.
$this->drupalGet("taxonomy/term/{$term->id()}/edit");
$this->assertNoFieldChecked('edit-path-0-pathauto');
$this->assertFieldByName('path[0][alias]', $manual_alias);
// Submit the term form with the default values.
$this->drupalPostForm(NULL, array('path[0][pathauto]' => FALSE), t('Save'));
$this->assertText("Updated term $name.");
// Test that the old (automatic) alias has been deleted and only accessible
// through the new (manual) alias.
$this->drupalGet($automatic_alias);
$this->assertResponse(404, 'Term not accessible through automatic alias.');
$this->drupalGet($manual_alias);
$this->assertText($name, 'Term accessible through manual alias.');
}
}
@@ -0,0 +1,190 @@
<?php
namespace Drupal\pathauto\Tests;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Language\Language;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\pathauto\Entity\PathautoPattern;
use Drupal\pathauto\PathautoPatternInterface;
use Drupal\taxonomy\VocabularyInterface;
use Drupal\taxonomy\Entity\Vocabulary;
use Drupal\taxonomy\Entity\Term;
/**
* Helper test class with some added functions for testing.
*/
trait PathautoTestHelperTrait {
/**
* Creates a pathauto pattern.
*
* @param string $entity_type_id
* The entity type.
* @param string $pattern
* The path pattern.
* @param int $weight
* (optional) The pattern weight.
*
* @return \Drupal\pathauto\PathautoPatternInterface
* The created pattern.
*/
protected function createPattern($entity_type_id, $pattern, $weight = 10) {
$type = ($entity_type_id == 'forum') ? 'forum' : 'canonical_entities:' . $entity_type_id;
$pattern = PathautoPattern::create([
'id' => Unicode::strtolower($this->randomMachineName()),
'type' => $type,
'pattern' => $pattern,
'weight' => $weight,
]);
$pattern->save();
return $pattern;
}
/**
* Add a bundle condition to a pathauto pattern.
*
* @param \Drupal\pathauto\PathautoPatternInterface $pattern
* The pattern.
* @param string $entity_type
* The entity type ID.
* @param string $bundle
* The bundle
*/
protected function addBundleCondition(PathautoPatternInterface $pattern, $entity_type, $bundle) {
$plugin_id = $entity_type == 'node' ? 'node_type' : 'entity_bundle:' . $entity_type;
$pattern->addSelectionCondition(
[
'id' => $plugin_id,
'bundles' => [
$bundle => $bundle,
],
'negate' => FALSE,
'context_mapping' => [
$entity_type => $entity_type,
]
]
);
}
public function assertToken($type, $object, $token, $expected) {
$bubbleable_metadata = new BubbleableMetadata();
$tokens = \Drupal::token()->generate($type, array($token => $token), array($type => $object), [], $bubbleable_metadata);
$tokens += array($token => '');
$this->assertIdentical($tokens[$token], $expected, t("Token value for [@type:@token] was '@actual', expected value '@expected'.", array('@type' => $type, '@token' => $token, '@actual' => $tokens[$token], '@expected' => $expected)));
}
public function saveAlias($source, $alias, $langcode = Language::LANGCODE_NOT_SPECIFIED) {
\Drupal::service('path.alias_storage')->delete(array('source' => $source, 'language', 'langcode' => $langcode));
return \Drupal::service('path.alias_storage')->save($source, $alias, $langcode);
}
public function saveEntityAlias(EntityInterface $entity, $alias, $langcode = NULL) {
// By default, use the entity language.
if (!$langcode) {
$langcode = $entity->language()->getId();
}
return $this->saveAlias('/' . $entity->toUrl()->getInternalPath(), $alias, $langcode);
}
public function assertEntityAlias(EntityInterface $entity, $expected_alias, $langcode = NULL) {
// By default, use the entity language.
if (!$langcode) {
$langcode = $entity->language()->getId();
}
$this->assertAlias('/' . $entity->toUrl()->getInternalPath(), $expected_alias, $langcode);
}
public function assertEntityAliasExists(EntityInterface $entity) {
return $this->assertAliasExists(array('source' => '/' . $entity->toUrl()->getInternalPath()));
}
public function assertNoEntityAlias(EntityInterface $entity, $langcode = NULL) {
// By default, use the entity language.
if (!$langcode) {
$langcode = $entity->language()->getId();
}
$this->assertEntityAlias($entity, '/' . $entity->toUrl()->getInternalPath(), $langcode);
}
public function assertNoEntityAliasExists(EntityInterface $entity, $alias = NULL) {
$path = array('source' => '/' . $entity->toUrl()->getInternalPath());
if (!empty($alias)) {
$path['alias'] = $alias;
}
$this->assertNoAliasExists($path);
}
public function assertAlias($source, $expected_alias, $langcode = Language::LANGCODE_NOT_SPECIFIED) {
\Drupal::service('path.alias_manager')->cacheClear($source);
$this->assertEqual($expected_alias, \Drupal::service('path.alias_manager')->getAliasByPath($source, $langcode), t("Alias for %source with language '@language' is correct.",
array('%source' => $source, '@language' => $langcode)));
}
public function assertAliasExists($conditions) {
$path = \Drupal::service('path.alias_storage')->load($conditions);
$this->assertTrue($path, t('Alias with conditions @conditions found.', array('@conditions' => var_export($conditions, TRUE))));
return $path;
}
public function assertNoAliasExists($conditions) {
$alias = \Drupal::service('path.alias_storage')->load($conditions);
$this->assertFalse($alias, t('Alias with conditions @conditions not found.', array('@conditions' => var_export($conditions, TRUE))));
}
public function deleteAllAliases() {
\Drupal::database()->delete('url_alias')->execute();
\Drupal::service('path.alias_manager')->cacheClear();
}
/**
* @param array $values
* @return \Drupal\taxonomy\VocabularyInterface
*/
public function addVocabulary(array $values = array()) {
$name = Unicode::strtolower($this->randomMachineName(5));
$values += array(
'name' => $name,
'vid' => $name,
);
$vocabulary = Vocabulary::create($values);
$vocabulary->save();
return $vocabulary;
}
public function addTerm(VocabularyInterface $vocabulary, array $values = array()) {
$values += array(
'name' => Unicode::strtolower($this->randomMachineName(5)),
'vid' => $vocabulary->id(),
);
$term = Term::create($values);
$term->save();
return $term;
}
public function assertEntityPattern($entity_type, $bundle, $langcode = Language::LANGCODE_NOT_SPECIFIED, $expected) {
$values = [
'langcode' => $langcode,
\Drupal::entityTypeManager()->getDefinition($entity_type)->getKey('bundle') => $bundle,
];
$entity = \Drupal::entityTypeManager()->getStorage($entity_type)->create($values);
$pattern = \Drupal::service('pathauto.generator')->getPatternByEntity($entity);
$this->assertIdentical($expected, $pattern->getPattern());
}
public function drupalGetTermByName($name, $reset = FALSE) {
if ($reset) {
// @todo - implement cache reset.
}
$terms = \Drupal::entityTypeManager()->getStorage('taxonomy_term')->loadByProperties(array('name' => $name));
return !empty($terms) ? reset($terms) : FALSE;
}
}
@@ -0,0 +1,191 @@
<?php
namespace Drupal\pathauto\Tests;
use Drupal\Core\Url;
use Drupal\simpletest\WebTestBase;
use Drupal\pathauto\Entity\PathautoPattern;
/**
* Test basic pathauto functionality.
*
* @group pathauto
*/
class PathautoUiTest extends WebTestBase {
use PathautoTestHelperTrait;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('pathauto', 'node');
/**
* Admin user.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* {inheritdoc}
*/
function setUp() {
parent::setUp();
$this->drupalCreateContentType(array('type' => 'page', 'name' => 'Basic page'));
$this->drupalCreateContentType(array('type' => 'article'));
// Allow other modules to add additional permissions for the admin user.
$permissions = array(
'administer pathauto',
'administer url aliases',
'create url aliases',
'administer nodes',
'bypass node access',
'access content overview',
);
$this->adminUser = $this->drupalCreateUser($permissions);
$this->drupalLogin($this->adminUser);
}
function testSettingsValidation() {
$edit = array();
$edit['max_length'] = 'abc';
$edit['max_component_length'] = 'abc';
$this->drupalPostForm('admin/config/search/path/settings', $edit, 'Save configuration');
/*$this->assertText('The field Maximum alias length is not a valid number.');
$this->assertText('The field Maximum component length is not a valid number.');*/
$this->assertNoText('The configuration options have been saved.');
$edit['max_length'] = '0';
$edit['max_component_length'] = '0';
$this->drupalPostForm('admin/config/search/path/settings', $edit, 'Save configuration');
/*$this->assertText('The field Maximum alias length cannot be less than 1.');
$this->assertText('The field Maximum component length cannot be less than 1.');*/
$this->assertNoText('The configuration options have been saved.');
$edit['max_length'] = '999';
$edit['max_component_length'] = '999';
$this->drupalPostForm('admin/config/search/path/settings', $edit, 'Save configuration');
/*$this->assertText('The field Maximum alias length cannot be greater than 255.');
$this->assertText('The field Maximum component length cannot be greater than 255.');*/
$this->assertNoText('The configuration options have been saved.');
$edit['max_length'] = '50';
$edit['max_component_length'] = '50';
$this->drupalPostForm('admin/config/search/path/settings', $edit, 'Save configuration');
$this->assertText('The configuration options have been saved.');
}
function testPatternsWorkflow() {
// Try to save an empty pattern, should not be allowed.
$this->drupalGet('admin/config/search/path/patterns/add');
$edit = array(
'type' => 'canonical_entities:node',
);
$this->drupalPostAjaxForm(NULL, $edit, 'type');
$edit += array(
'bundles[page]' => TRUE,
'label' => 'Page pattern',
'id' => 'page_pattern',
);
$this->drupalPostForm(NULL, $edit, 'Save');
$this->assertText('Path pattern field is required.');
$this->assertNoText('The configuration options have been saved.');
// Try to save an invalid pattern.
$edit += array(
'pattern' => '[node:title]/[user:name]/[term:name]',
);
$this->drupalPostForm(NULL, $edit, 'Save');
$this->assertText('Path pattern is using the following invalid tokens: [user:name], [term:name].');
$this->assertNoText('The configuration options have been saved.');
$edit['pattern'] = '#[node:title]';
$this->drupalPostForm(NULL, $edit, 'Save');
$this->assertText('The Path pattern is using the following invalid characters: #.');
$this->assertNoText('The configuration options have been saved.');
// Checking whitespace ending of the string.
$edit['pattern'] = '[node:title] ';
$this->drupalPostForm(NULL, $edit, 'Save');
$this->assertText('The Path pattern doesn\'t allow the patterns ending with whitespace.');
$this->assertNoText('The configuration options have been saved.');
// Fix the pattern, then check that it gets saved successfully.
$edit['pattern'] = '[node:title]';
$this->drupalPostForm(NULL, $edit, 'Save');
$this->assertText('Pattern Page pattern saved.');
\Drupal::service('pathauto.generator')->resetCaches();
// Create a node with pattern enabled and check if the pattern applies.
$title = 'Page Pattern enabled';
$alias = '/page-pattern-enabled';
$node = $this->createNode(['title' => $title, 'type' => 'page']);
$this->drupalGet($alias);
$this->assertResponse(200);
$this->assertEntityAlias($node, $alias);
// Edit workflow, set a new label and weight for the pattern.
$this->drupalPostForm('/admin/config/search/path/patterns', ['entities[page_pattern][weight]' => '4'], t('Save'));
$this->clickLink(t('Edit'));
$destination_query = ['query' => ['destination' => Url::fromRoute('entity.pathauto_pattern.collection')->toString()]];
$this->assertUrl('/admin/config/search/path/patterns/page_pattern', $destination_query);
$this->assertFieldByName('pattern', '[node:title]');
$this->assertFieldByName('label', 'Page pattern');
$this->assertFieldChecked('edit-status');
$this->assertLink(t('Delete'));
$edit = array('label' => 'Test');
$this->drupalPostForm('/admin/config/search/path/patterns/page_pattern', $edit, t('Save'));
$this->assertText('Pattern Test saved.');
// Check that the pattern weight did not change.
$this->assertOptionSelected('edit-entities-page-pattern-weight', '4');
// Disable workflow.
$this->drupalGet('/admin/config/search/path/patterns');
$this->assertNoLink(t('Enable'));
$this->clickLink(t('Disable'));
$this->assertUrl('/admin/config/search/path/patterns/page_pattern/disable', $destination_query);
$this->drupalPostForm(NULL, [], t('Disable'));
$this->assertText('Disabled pattern Test.');
// Load the pattern from storage and check if its disabled.
$pattern = PathautoPattern::load('page_pattern');
$this->assertFalse($pattern->status());
\Drupal::service('pathauto.generator')->resetCaches();
// Create a node with pattern disabled and check that we have no new alias.
$title = 'Page Pattern disabled';
$node = $this->createNode(['title' => $title, 'type' => 'page']);
$this->assertNoEntityAlias($node);
// Enable workflow.
$this->drupalGet('/admin/config/search/path/patterns');
$this->assertNoLink(t('Disable'));
$this->clickLink(t('Enable'));
$this->assertUrl('/admin/config/search/path/patterns/page_pattern/enable', $destination_query);
$this->drupalPostForm(NULL, [], t('Enable'));
$this->assertText('Enabled pattern Test.');
// Reload pattern from storage and check if its enabled.
$pattern = PathautoPattern::load('page_pattern');
$this->assertTrue($pattern->status());
// Delete workflow.
$this->drupalGet('/admin/config/search/path/patterns');
$this->clickLink(t('Delete'));
$this->assertUrl('/admin/config/search/path/patterns/page_pattern/delete', $destination_query);
$this->assertText(t('This action cannot be undone.'));
$this->drupalPostForm(NULL, [], t('Delete'));
$this->assertText('The pathauto pattern Test has been deleted.');
$this->assertFalse(PathautoPattern::load('page_pattern'));
}
}
@@ -0,0 +1,92 @@
<?php
namespace Drupal\pathauto\Tests;
use Drupal\Component\Utility\Unicode;
use Drupal\simpletest\WebTestBase;
use Drupal\views\Views;
/**
* Tests pathauto user UI integration.
*
* @group pathauto
*/
class PathautoUserWebTest extends WebTestBase {
use PathautoTestHelperTrait;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('pathauto', 'views');
/**
* Admin user.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* {inheritdoc}
*/
function setUp() {
parent::setUp();
// Allow other modules to add additional permissions for the admin user.
$permissions = array(
'administer pathauto',
'administer url aliases',
'create url aliases',
'administer users',
);
$this->adminUser = $this->drupalCreateUser($permissions);
$this->drupalLogin($this->adminUser);
$this->createPattern('user', '/users/[user:name]');
}
/**
* Basic functional testing of Pathauto with users.
*/
function testUserEditing() {
// There should be no Pathauto checkbox on user forms.
$this->drupalGet('user/' . $this->adminUser->id() . '/edit');
$this->assertNoFieldById('path[0][pathauto]');
}
/**
* Test user operations.
*/
function testUserOperations() {
$account = $this->drupalCreateUser();
// Delete all current URL aliases.
$this->deleteAllAliases();
// Find the position of just created account in the user_admin_people view.
$view = Views::getView('user_admin_people');
$view->initDisplay();
$view->preview('page_1');
foreach ($view->result as $key => $row) {
if ($view->field['name']->getValue($row) == $account->getUsername()) {
break;
}
}
$edit = array(
'action' => 'pathauto_update_alias_user',
"user_bulk_form[$key]" => TRUE,
);
$this->drupalPostForm('admin/people', $edit, t('Apply to selected items'));
$this->assertText('Update URL alias was applied to 1 item.');
$this->assertEntityAlias($account, '/users/' . Unicode::strtolower($account->getUsername()));
$this->assertEntityAlias($this->adminUser, '/user/' . $this->adminUser->id());
}
}
@@ -0,0 +1,63 @@
<?php
namespace Drupal\pathauto;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Session\AccountInterface;
/**
* Provides a verbose messenger.
*/
class VerboseMessenger implements MessengerInterface {
/**
* The verbose flag.
*
* @var bool
*/
protected $isVerbose;
/**
* The config factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* The current user account.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $account;
/**
* Creates a verbose messenger.
*/
public function __construct(ConfigFactoryInterface $config_factory, AccountInterface $account) {
$this->configFactory = $config_factory;
$this->account = $account;
}
/**
* {@inheritdoc}
*/
public function addMessage($message, $op = NULL) {
if (!isset($this->isVerbose)) {
$config = $this->configFactory->get('pathauto.settings');
$this->isVerbose = $config->get('verbose') && $this->account->hasPermission('notify of path changes');
}
if (!$this->isVerbose || (isset($op) && in_array($op, array('bulkupdate', 'return')))) {
return FALSE;
}
if ($message) {
drupal_set_message($message);
}
return TRUE;
}
}
@@ -0,0 +1,13 @@
name: 'Pathauto testing module'
type: module
# core: '8.x'
description: 'Pathauto for Entity with string ID.'
package: Testing
dependencies:
- token
# Information added by Drupal.org packaging script on 2018-03-03
version: '8.x-1.1'
core: '8.x'
project: 'pathauto'
datestamp: 1520092688
@@ -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;
}
}
@@ -0,0 +1,231 @@
langcode: en
status: true
dependencies:
module:
- node
- taxonomy
- user
id: articles
label: articles
module: pathauto_views_test
description: ''
tag: ''
base_table: node_field_data
base_field: nid
core: 8.x
display:
default:
display_plugin: default
id: default
display_title: Master
position: 0
display_options:
access:
type: perm
options:
perm: 'access content'
cache:
type: tag
options: { }
query:
type: views_query
options:
disable_sql_rewrite: false
distinct: false
replica: false
query_comment: ''
query_tags: { }
exposed_form:
type: basic
options:
submit_button: Apply
reset_button: false
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
pager:
type: full
options:
items_per_page: 10
offset: 0
id: 0
total_pages: null
expose:
items_per_page: false
items_per_page_label: 'Items per page'
items_per_page_options: '5, 10, 25, 50'
items_per_page_options_all: false
items_per_page_options_all_label: '- All -'
offset: false
offset_label: Offset
tags:
previous: ' Previous'
next: 'Next '
first: '« First'
last: 'Last »'
quantity: 9
style:
type: default
row:
type: 'entity:node'
options:
view_mode: teaser
fields:
title:
id: title
table: node_field_data
field: title
entity_type: node
entity_field: title
label: ''
alter:
alter_text: false
make_link: false
absolute: false
trim: false
word_boundary: false
ellipsis: false
strip_tags: false
html: false
hide_empty: false
empty_zero: false
settings:
link_to_entity: true
plugin_id: field
relationship: none
group_type: group
admin_label: ''
exclude: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_alter_empty: true
click_sort_column: value
type: string
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
filters:
status:
value: '1'
table: node_field_data
field: status
plugin_id: boolean
entity_type: node
entity_field: status
id: status
expose:
operator: ''
group: 1
type:
id: type
table: node_field_data
field: type
value:
article: article
entity_type: node
entity_field: type
plugin_id: bundle
sorts:
created:
id: created
table: node_field_data
field: created
order: DESC
entity_type: node
entity_field: created
plugin_id: date
relationship: none
group_type: group
admin_label: ''
exposed: false
expose:
label: ''
granularity: second
title: articles
header: { }
footer: { }
empty: { }
relationships: { }
arguments:
tid:
id: tid
table: taxonomy_index
field: tid
relationship: none
group_type: group
admin_label: ''
default_action: ignore
exception:
value: all
title_enable: false
title: All
title_enable: false
title: ''
default_argument_type: fixed
default_argument_options:
argument: ''
default_argument_skip_url: false
summary_options:
base_path: ''
count: true
items_per_page: 25
override: false
summary:
sort_order: asc
number_of_records: 0
format: default_summary
specify_validation: false
validate:
type: none
fail: 'not found'
validate_options: { }
break_phrase: false
add_table: false
require_value: false
reduce_duplicates: false
plugin_id: taxonomy_index_tid
display_extenders: { }
cache_metadata:
max-age: -1
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url
- url.query_args
- 'user.node_grants:view'
- user.permissions
tags: { }
page_1:
display_plugin: page
id: page_1
display_title: Page
position: 1
display_options:
display_extenders: { }
path: articles
cache_metadata:
max-age: -1
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url
- url.query_args
- 'user.node_grants:view'
- user.permissions
tags: { }
@@ -0,0 +1,14 @@
name: 'Views Test Config'
type: module
description: 'Provides default views for tests.'
package: Testing
# version: VERSION
# core: 8.x
dependencies:
- views
# Information added by Drupal.org packaging script on 2018-03-03
version: '8.x-1.1'
core: '8.x'
project: 'pathauto'
datestamp: 1520092688
@@ -0,0 +1,130 @@
<?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\pathauto\PathautoState;
use Drupal\pathauto\Tests\PathautoTestHelperTrait;
use Drupal\KernelTests\KernelTestBase;
use Drupal\pathauto_string_id_test\Entity\PathautoStringIdTest;
/**
* 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',
'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() {
parent::setUp();
$this->installSchema('system', ['key_value']);
$this->installConfig(['system']);
$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, "/$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'
],
];
}
}
@@ -0,0 +1,588 @@
<?php
namespace Drupal\Tests\pathauto\Kernel;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Language\Language;
use Drupal\Core\Language\LanguageInterface;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\node\Entity\NodeType;
use Drupal\pathauto\PathautoGeneratorInterface;
use Drupal\pathauto\PathautoState;
use Drupal\pathauto\Tests\PathautoTestHelperTrait;
use Drupal\KernelTests\KernelTestBase;
use Drupal\taxonomy\Entity\Term;
use Drupal\taxonomy\Entity\Vocabulary;
use Drupal\node\Entity\Node;
use Drupal\user\Entity\User;
/**
* Unit tests for Pathauto functions.
*
* @group pathauto
*/
class PathautoKernelTest extends KernelTestBase {
use PathautoTestHelperTrait;
public static $modules = array('system', 'field', 'text', 'user', 'node', 'path', 'pathauto', 'taxonomy', 'token', 'filter', 'ctools', 'language');
protected $currentUser;
/**
* @var \Drupal\pathauto\PathautoPatternInterface
*/
protected $nodePattern;
/**
* @var \Drupal\pathauto\PathautoPatternInterface
*/
protected $userPattern;
public function setUp() {
parent::setup();
$this->installConfig(array('pathauto', 'taxonomy', 'system', 'node'));
$this->installEntitySchema('user');
$this->installEntitySchema('node');
$this->installEntitySchema('taxonomy_term');
ConfigurableLanguage::createFromLangcode('fr')->save();
$this->installSchema('node', array('node_access'));
$this->installSchema('system', array('url_alias', 'sequences', 'router'));
$type = NodeType::create(['type' => 'page']);
$type->save();
node_add_body_field($type);
$this->nodePattern = $this->createPattern('node', '/content/[node:title]');
$this->userPattern = $this->createPattern('user', '/users/[user:name]');
\Drupal::service('router.builder')->rebuild();
$this->currentUser = User::create(array('name' => $this->randomMachineName()));
$this->currentUser->save();
}
/**
* Test _pathauto_get_schema_alias_maxlength().
*/
public function testGetSchemaAliasMaxLength() {
$this->assertIdentical(\Drupal::service('pathauto.alias_storage_helper')->getAliasSchemaMaxlength(), 255);
}
/**
* Test pathauto_pattern_load_by_entity().
*/
public function testPatternLoadByEntity() {
$pattern = $this->createPattern('node', '/article/[node:title]', -1);
$this->addBundleCondition($pattern, 'node', 'article');
$pattern->save();
$pattern = $this->createPattern('node', '/article/en/[node:title]', -2);
$this->addBundleCondition($pattern, 'node', 'article');
$pattern->addSelectionCondition(
[
'id' => 'language',
'langcodes' => [
'en' => 'en',
],
'negate' => FALSE,
'context_mapping' => [
'language' => 'node:langcode:language',
]
]
);
$pattern->addRelationship('node:langcode:language');
$pattern->save();
$pattern = $this->createPattern('node', '/[node:title]', -1);
$this->addBundleCondition($pattern, 'node', 'page');
$pattern->save();
$tests = array(
array(
'entity' => 'node',
'values' => [
'title' => 'Article fr',
'type' => 'article',
'langcode' => 'fr',
],
'expected' => '/article/[node:title]',
),
array(
'entity' => 'node',
'values' => [
'title' => 'Article en',
'type' => 'article',
'langcode' => 'en',
],
'expected' => '/article/en/[node:title]',
),
array(
'entity' => 'node',
'values' => [
'title' => 'Article und',
'type' => 'article',
'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
],
'expected' => '/article/[node:title]',
),
array(
'entity' => 'node',
'values' => [
'title' => 'Page',
'type' => 'page',
],
'expected' => '/[node:title]',
),
array(
'entity' => 'user',
'values' => [
'name' => 'User',
],
'expected' => '/users/[user:name]',
),
);
foreach ($tests as $test) {
$entity = \Drupal::entityTypeManager()->getStorage($test['entity'])->create($test['values']);
$entity->save();
$actual = \Drupal::service('pathauto.generator')->getPatternByEntity($entity);
$this->assertIdentical($actual->getPattern(), $test['expected'], t("Correct pattern returned for @entity_type with @values", array(
'@entity' => $test['entity'],
'@values' => print_r($test['values'], TRUE),
)));
}
}
/**
* Test potential conflicts with the same alias in different languages.
*/
public function testSameTitleDifferentLanguages() {
// Create two English articles with the same title.
$edit = [
'title' => 'Sample page',
'type' => 'page',
'langcode' => 'en',
];
$node1 = $this->drupalCreateNode($edit);
$this->assertEntityAlias($node1, '/content/sample-page', 'en');
$node2 = $this->drupalCreateNode($edit);
$this->assertEntityAlias($node2, '/content/sample-page-0', 'en');
// Now, create a French article with the same title, and verify that it gets
// the basic alias with the correct langcode.
$edit['langcode'] = 'fr';
$node3 = $this->drupalCreateNode($edit);
$this->assertEntityAlias($node3, '/content/sample-page', 'fr');
}
/**
* Test pathauto_cleanstring().
*/
public function testCleanString() {
// Test with default settings defined in pathauto.settings.yml.
$this->installConfig(array('pathauto'));
\Drupal::service('pathauto.generator')->resetCaches();
$tests = array();
// Test the 'ignored words' removal.
$tests['this'] = 'this';
$tests['this with that'] = 'this-with-that';
$tests['this thing with that thing'] = 'thing-thing';
// Test 'ignored words' removal and duplicate separator removal.
$tests[' - Pathauto is the greatest - module ever - '] = 'pathauto-greatest-module-ever';
// Test length truncation and lowering of strings.
$long_string = $this->randomMachineName(120);
$tests[$long_string] = strtolower(substr($long_string, 0, 100));
// Test that HTML tags are removed.
$tests['This <span class="text">text</span> has <br /><a href="http://example.com"><strong>HTML tags</strong></a>.'] = 'text-has-html-tags';
$tests[Html::escape('This <span class="text">text</span> has <br /><a href="http://example.com"><strong>HTML tags</strong></a>.')] = 'text-has-html-tags';
// Transliteration.
$tests['ľščťžýáíéňô'] = 'lsctzyaieno';
foreach ($tests as $input => $expected) {
$output = \Drupal::service('pathauto.alias_cleaner')->cleanString($input);
$this->assertEqual($output, $expected, t("Drupal::service('pathauto.alias_cleaner')->cleanString('@input') expected '@expected', actual '@output'", array(
'@input' => $input,
'@expected' => $expected,
'@output' => $output,
)));
}
}
/**
* Test pathauto_clean_alias().
*/
public function testCleanAlias() {
$tests = array();
$tests['one/two/three'] = '/one/two/three';
$tests['/one/two/three/'] = '/one/two/three';
$tests['one//two///three'] = '/one/two/three';
$tests['one/two--three/-/--/-/--/four---five'] = '/one/two-three/four-five';
$tests['one/-//three--/four'] = '/one/three/four';
foreach ($tests as $input => $expected) {
$output = \Drupal::service('pathauto.alias_cleaner')->cleanAlias($input);
$this->assertEqual($output, $expected, t("Drupal::service('pathauto.generator')->cleanAlias('@input') expected '@expected', actual '@output'", array(
'@input' => $input,
'@expected' => $expected,
'@output' => $output,
)));
}
}
/**
* Test pathauto_path_delete_multiple().
*/
public function testPathDeleteMultiple() {
$this->saveAlias('/node/1', '/node-1-alias');
$this->saveAlias('/node/1/view', '/node-1-alias/view');
$this->saveAlias('/node/1', '/node-1-alias-en', 'en');
$this->saveAlias('/node/1', '/node-1-alias-fr', 'fr');
$this->saveAlias('/node/2', '/node-2-alias');
$this->saveAlias('/node/10', '/node-10-alias');
\Drupal::service('pathauto.alias_storage_helper')->deleteBySourcePrefix('/node/1');
$this->assertNoAliasExists(array('source' => "/node/1"));
$this->assertNoAliasExists(array('source' => "/node/1/view"));
$this->assertAliasExists(array('source' => "/node/2"));
$this->assertAliasExists(array('source' => "/node/10"));
}
/**
* Test the different update actions in \Drupal::service('pathauto.generator')->createEntityAlias().
*/
public function testUpdateActions() {
$config = $this->config('pathauto.settings');
// Test PATHAUTO_UPDATE_ACTION_NO_NEW with unaliased node and 'insert'.
$config->set('update_action', PathautoGeneratorInterface::UPDATE_ACTION_NO_NEW);
$config->save();
$node = $this->drupalCreateNode(array('title' => 'First title'));
$this->assertEntityAlias($node, '/content/first-title');
$node->path->pathauto = PathautoState::CREATE;
// Default action is PATHAUTO_UPDATE_ACTION_DELETE.
$config->set('update_action', PathautoGeneratorInterface::UPDATE_ACTION_DELETE);
$config->save();
$node->setTitle('Second title');
$node->save();
$this->assertEntityAlias($node, '/content/second-title');
$this->assertNoAliasExists(array('alias' => '/content/first-title'));
// Test PATHAUTO_UPDATE_ACTION_LEAVE
$config->set('update_action', PathautoGeneratorInterface::UPDATE_ACTION_LEAVE);
$config->save();
$node->setTitle('Third title');
$node->save();
$this->assertEntityAlias($node, '/content/third-title');
$this->assertAliasExists(array('source' => '/' . $node->toUrl()->getInternalPath(), 'alias' => '/content/second-title'));
$config->set('update_action', PathautoGeneratorInterface::UPDATE_ACTION_DELETE);
$config->save();
$node->setTitle('Fourth title');
$node->save();
$this->assertEntityAlias($node, '/content/fourth-title');
$this->assertNoAliasExists(array('alias' => '/content/third-title'));
// The older second alias is not deleted yet.
$older_path = $this->assertAliasExists(array('source' => '/' . $node->toUrl()->getInternalPath(), 'alias' => '/content/second-title'));
\Drupal::service('path.alias_storage')->delete($older_path);
$config->set('update_action', PathautoGeneratorInterface::UPDATE_ACTION_NO_NEW);
$config->save();
$node->setTitle('Fifth title');
$node->save();
$this->assertEntityAlias($node, '/content/fourth-title');
$this->assertNoAliasExists(array('alias' => '/content/fifth-title'));
// Test PATHAUTO_UPDATE_ACTION_NO_NEW with unaliased node and 'update'.
$this->deleteAllAliases();
$node->save();
$this->assertEntityAlias($node, '/content/fifth-title');
// Test PATHAUTO_UPDATE_ACTION_NO_NEW with unaliased node and 'bulkupdate'.
$this->deleteAllAliases();
$node->setTitle('Sixth title');
\Drupal::service('pathauto.generator')->updateEntityAlias($node, 'bulkupdate');
$this->assertEntityAlias($node, '/content/sixth-title');
}
/**
* Test that \Drupal::service('pathauto.generator')->createEntityAlias() will not create an alias for a pattern
* that does not get any tokens replaced.
*/
public function testNoTokensNoAlias() {
$this->installConfig(['filter']);
$this->nodePattern
->setPattern('/content/[node:body]')
->save();
$node = $this->drupalCreateNode();
$this->assertNoEntityAliasExists($node);
$node->body->value = 'hello';
$node->save();
$this->assertEntityAlias($node, '/content/hello');
}
/**
* Test the handling of path vs non-path tokens in pathauto_clean_token_values().
*/
public function testPathTokens() {
$this->createPattern('taxonomy_term', '/[term:parent:url:path]/[term:name]');
$vocab = $this->addVocabulary();
$term1 = $this->addTerm($vocab, array('name' => 'Parent term'));
$this->assertEntityAlias($term1, '/parent-term');
$term2 = $this->addTerm($vocab, array('name' => 'Child term', 'parent' => $term1->id()));
$this->assertEntityAlias($term2, '/parent-term/child-term');
$this->saveEntityAlias($term1, '/My Crazy/Alias/');
$term2->save();
$this->assertEntityAlias($term2, '/My Crazy/Alias/child-term');
}
/**
* Test using fields for path structures.
*/
function testParentChildPathTokens() {
// First create a field which will be used to create the path. It must
// begin with a letter.
$this->installEntitySchema('taxonomy_term');
Vocabulary::create(['vid' => 'tags'])->save();
$fieldname = 'a' . Unicode::strtolower($this->randomMachineName());
$field_storage = FieldStorageConfig::create(['entity_type' => 'taxonomy_term', 'field_name' => $fieldname, 'type' => 'string']);
$field_storage->save();
$field = FieldConfig::create(['field_storage' => $field_storage, 'bundle' => 'tags']);
$field->save();
$display = entity_get_display('taxonomy_term', 'tags', 'default');
$display->setComponent($fieldname, ['type' => 'string']);
$display->save();
// Make the path pattern of a field use the value of this field appended
// to the parent taxonomy term's pattern if there is one.
$this->createPattern('taxonomy_term', '/[term:parents:join-path]/[term:' . $fieldname . ']');
// Start by creating a parent term.
$parent = Term::create(['vid' => 'tags', $fieldname => $this->randomMachineName(), 'name' => $this->randomMachineName()]);
$parent->save();
// Create the child term.
$child = Term::create(['vid' => 'tags', $fieldname => $this->randomMachineName(), 'parent' => $parent, 'name' => $this->randomMachineName()]);
$child->save();
$this->assertEntityAlias($child, '/' . Unicode::strtolower($parent->getName() . '/' . $child->$fieldname->value));
// Re-saving the parent term should not modify the child term's alias.
$parent->save();
$this->assertEntityAlias($child, '/' . Unicode::strtolower($parent->getName() . '/' . $child->$fieldname->value));
}
/**
* Tests aliases on taxonomy terms.
*/
public function testTaxonomyPattern() {
// Create a vocabulary and test that it's pattern variable works.
$vocab = $this->addVocabulary(array('vid' => 'name'));
$this->createPattern('taxonomy_term', 'base');
$pattern = $this->createPattern('taxonomy_term', 'bundle', -1);
$this->addBundleCondition($pattern, 'taxonomy_term', 'name');
$pattern->save();
$this->assertEntityPattern('taxonomy_term', 'name', Language::LANGCODE_NOT_SPECIFIED, 'bundle');
}
function testNoExistingPathAliases() {
$this->config('pathauto.settings')
->set('punctuation.period', PathautoGeneratorInterface::PUNCTUATION_DO_NOTHING)
->save();
$this->nodePattern
->setPattern('[node:title]')
->save();
// Check that Pathauto does not create an alias of '/admin'.
$node = $this->drupalCreateNode(array('title' => 'Admin', 'type' => 'page'));
$this->assertEntityAlias($node, '/admin-0');
// Check that Pathauto does not create an alias of '/modules'.
$node->setTitle('Modules');
$node->save();
$this->assertEntityAlias($node, '/modules-0');
// Check that Pathauto does not create an alias of '/index.php'.
$node->setTitle('index.php');
$node->save();
$this->assertEntityAlias($node, '/index.php-0');
// Check that a safe value gets an automatic alias. This is also a control
// to ensure the above tests work properly.
$node->setTitle('Safe value');
$node->save();
$this->assertEntityAlias($node, '/safe-value');
}
/**
* Test programmatic entity creation for aliases.
*/
function testProgrammaticEntityCreation() {
$this->createPattern('taxonomy_term', '/[term:vocabulary]/[term:name]');
$node = $this->drupalCreateNode(array('title' => 'Test node', 'path' => array('pathauto' => TRUE)));
$this->assertEntityAlias($node, '/content/test-node');
$vocabulary = $this->addVocabulary(array('name' => 'Tags'));
$term = $this->addTerm($vocabulary, array('name' => 'Test term', 'path' => array('pathauto' => TRUE)));
$this->assertEntityAlias($term, '/tags/test-term');
$edit['name'] = 'Test user';
$edit['mail'] = 'test-user@example.com';
$edit['pass'] = user_password();
$edit['path'] = array('pathauto' => TRUE);
$edit['status'] = 1;
$account = User::create($edit);
$account->save();
$this->assertEntityAlias($account, '/users/test-user');
}
/**
* Tests word safe alias truncating.
*/
function testPathAliasUniquifyWordsafe() {
$this->config('pathauto.settings')
->set('max_length', 26)
->save();
$node_1 = $this->drupalCreateNode(array('title' => 'thequick brownfox jumpedover thelazydog', 'type' => 'page'));
$node_2 = $this->drupalCreateNode(array('title' => 'thequick brownfox jumpedover thelazydog', 'type' => 'page'));
// Check that alias uniquifying is truncating with $wordsafe param set to
// TRUE.
// If it doesn't path alias result would be content/thequick-brownf-0
$this->assertEntityAlias($node_1, '/content/thequick-brownfox');
$this->assertEntityAlias($node_2, '/content/thequick-0');
}
/**
* Test if aliases are (not) generated with enabled/disabled patterns.
*/
function testPatternStatus() {
// Create a node to get an alias for.
$title = 'Pattern enabled';
$alias = '/content/pattern-enabled';
$node1 = $this->drupalCreateNode(['title' => $title, 'type' => 'page']);
$this->assertEntityAlias($node1, $alias);
// Disable the pattern, save the node again and make sure the alias is still
// working.
$this->nodePattern->setStatus(FALSE)->save();
$node1->save();
$this->assertEntityAlias($node1, $alias);
// Create a new node with disabled pattern and make sure there is no new
// alias created.
$title = 'Pattern disabled';
$node2 = $this->drupalCreateNode(['title' => $title, 'type' => 'page']);
$this->assertNoEntityAlias($node2);
}
/**
* Tests that enabled entity types genrates the necessary fields and plugins.
*/
public function testSettingChangeInvalidatesCache() {
$this->installConfig(['pathauto']);
$this->enableModules(['entity_test']);
$definitions = \Drupal::service('plugin.manager.alias_type')->getDefinitions();
$this->assertFalse(isset($definitions['canonical_entities:entity_test']));
$fields = \Drupal::service('entity_field.manager')->getBaseFieldDefinitions('entity_test');
$this->assertFalse(isset($fields['path']));
$this->config('pathauto.settings')
->set('enabled_entity_types', ['user', 'entity_test'])
->save();
$definitions = \Drupal::service('plugin.manager.alias_type')->getDefinitions();
$this->assertTrue(isset($definitions['canonical_entities:entity_test']));
$fields = \Drupal::service('entity_field.manager')->getBaseFieldDefinitions('entity_test');
$this->assertTrue(isset($fields['path']));
}
/**
* Tests that aliases are only generated for default revisions.
*/
public function testDefaultRevision() {
$node1 = $this->drupalCreateNode(['title' => 'Default revision', 'type' => 'page']);
$this->assertEntityAlias($node1, '/content/default-revision');
$node1->setNewRevision(TRUE);
$node1->isDefaultRevision(FALSE);
$node1->setTitle('New non-default-revision');
$node1->save();
$this->assertEntityAlias($node1, '/content/default-revision');
}
/**
* 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 = array()) {
// Populate defaults array.
$settings += array(
'title' => $this->randomMachineName(8),
'type' => 'page',
);
$node = Node::create($settings);
$node->save();
return $node;
}
}
@@ -0,0 +1,78 @@
<?php
namespace Drupal\Tests\pathauto\Kernel;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests tokens provided by Pathauto.
*
* @group pathauto
*/
class PathautoTokenTest extends KernelTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('system', 'token', 'pathauto');
public function testPathautoTokens() {
$this->installConfig(array('pathauto'));
$array = array(
'test first arg',
'The Array / value',
);
$tokens = array(
'join-path' => 'test-first-arg/array-value',
);
$data['array'] = $array;
$replacements = $this->assertTokens('array', $data, $tokens);
// Ensure that the cleanTokenValues() method does not alter this token value.
/* @var \Drupal\pathauto\AliasCleanerInterface $alias_cleaner */
$alias_cleaner = \Drupal::service('pathauto.alias_cleaner');
$alias_cleaner->cleanTokenValues($replacements, $data, array());
$this->assertEqual($replacements['[array:join-path]'], 'test-first-arg/array-value');
}
/**
* Function copied from TokenTestHelper::assertTokens().
*/
public function assertTokens($type, array $data, array $tokens, array $options = array()) {
$input = $this->mapTokenNames($type, array_keys($tokens));
$bubbleable_metadata = new BubbleableMetadata();
$replacements = \Drupal::token()->generate($type, $input, $data, $options, $bubbleable_metadata);
foreach ($tokens as $name => $expected) {
$token = $input[$name];
if (!isset($expected)) {
$this->assertTrue(!isset($values[$token]), t("Token value for @token was not generated.", array('@type' => $type, '@token' => $token)));
}
elseif (!isset($replacements[$token])) {
$this->fail(t("Token value for @token was not generated.", array('@type' => $type, '@token' => $token)));
}
elseif (!empty($options['regex'])) {
$this->assertTrue(preg_match('/^' . $expected . '$/', $replacements[$token]), t("Token value for @token was '@actual', matching regular expression pattern '@expected'.", array('@type' => $type, '@token' => $token, '@actual' => $replacements[$token], '@expected' => $expected)));
}
else {
$this->assertIdentical($replacements[$token], $expected, t("Token value for @token was '@actual', expected value '@expected'.", array('@type' => $type, '@token' => $token, '@actual' => $replacements[$token], '@expected' => $expected)));
}
}
return $replacements;
}
public function mapTokenNames($type, array $tokens = array()) {
$return = array();
foreach ($tokens as $token) {
$return[$token] = "[$type:$token]";
}
return $return;
}
}
@@ -0,0 +1,59 @@
<?php
namespace Drupal\Tests\pathauto\Unit {
use Drupal\pathauto\VerboseMessenger;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\pathauto\VerboseMessenger
* @group pathauto
*/
class VerboseMessengerTest extends UnitTestCase {
/**
* The messenger under test.
*
* @var \Drupal\pathauto\VerboseMessenger
*/
protected $messenger;
/**
* {@inheritdoc}
*/
protected function setUp() {
$config_factory = $this->getConfigFactoryStub(array('pathauto.settings' => array('verbose' => TRUE)));
$account = $this->getMock('\Drupal\Core\Session\AccountInterface');
$account->expects($this->once())
->method('hasPermission')
->withAnyParameters()
->willReturn(TRUE);
$this->messenger = new VerboseMessenger($config_factory, $account);
}
/**
* Tests add messages.
* @covers ::addMessage
*/
public function testAddMessage() {
$this->assertTrue($this->messenger->addMessage("Test message"), "The message was added");
}
/**
* @covers ::addMessage
*/
public function testDoNotAddMessageWhileBulkupdate() {
$this->assertFalse($this->messenger->addMessage("Test message", "bulkupdate"), "The message was NOT added");
}
}
}
namespace {
// @todo Delete after https://drupal.org/node/1858196 is in.
if (!function_exists('drupal_set_message')) {
function drupal_set_message() {
}
}
}
@@ -0,0 +1,2 @@
vendor
composer.lock
@@ -0,0 +1,41 @@
language: php
php:
- 5.6
- 7.0
env:
global:
- PATH=$PATH:/home/travis/.composer/vendor/bin
install:
- composer self-update
before_script:
# Set sendmail so drush doesn't throw an error during site install.
- echo "sendmail_path=`which true`" >> `php --ini | grep "Loaded Configuration" | awk '{print $4}'`
# Create database.
- mysql -e 'create database drupal'
# Install Drupal 8 target site.
- cd tests
- mkdir -p themes modules profiles
- composer install --prefer-dist
- ./vendor/bin/drush si standard -y --db-url=mysql://travis:@127.0.0.1/drupal
# Test latest commit on current branch.
- git clone --branch=$TRAVIS_BRANCH https://github.com/$TRAVIS_REPO_SLUG.git modules/config_filter
- ./vendor/bin/drush en config_filter -y
# Run Drush web server.
- ./vendor/bin/drush --debug runserver :8888 > ~/debug.txt 2>&1 &
- sleep 4s
- chmod -R ug+w sites
script:
- cd $TRAVIS_BUILD_DIR
- cd tests
- ./vendor/bin/phpcs --config-set installed_paths ../../drupal/coder/coder_sniffer
- ./vendor/bin/phpcs --config-set show_progress 1
- ./vendor/bin/phpcs --warning-severity=0 --standard=Drupal,DrupalPractice ../src
- ./vendor/bin/phpunit
notifications:
email: false
@@ -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.
@@ -0,0 +1,50 @@
# Config Filter
[![Build Status](https://travis-ci.org/nuvoleweb/config_filter.svg?branch=8.x-1.x)](https://travis-ci.org/nuvoleweb/config_filter)
[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/nuvoleweb/config_filter/badges/quality-score.png?b=8.x-1.x)](https://scrutinizer-ci.com/g/nuvoleweb/config_filter/?branch=8.x-1.x)
## Introduction
Modules such as Config Split want to modify the configuration when it is
synchronized between the database and the exported yaml files.
This module provides the API to do so but does not influence a sites operation.
## How it works
Configuration Filter swaps the config.storage.sync service from Drupal 8 core.
The new service wraps the file storage and applies filters to it.
This allows other modules to change the configuration as it gets imported or
exported both in the Drupal UI and with drush.
## What is a ConfigFilter
A ConfigFilter is a plugin. This module provides the plugin definition, the
plugin manager and the storage factory.
A ConfigFilter can have the following annotation:
```php
/**
* @ConfigFilter(
* id = "may_plugin_id",
* label = @Translation("An example configuration filter"),
* weight = 0,
* status = TRUE,
* storages = {"config.storage.sync"},
* )
*/
```
See `\Drupal\config_filter\Annotation\ConfigFilter`.
The weight allows the filters to be sorted. The status allows the filter to be
active or inactive, the `ConfigFilterManagerInterface::getFiltersForStorages`
will only take active filters into consideration. The weight, status and
storages are optional and the above values are the default.
## Alternative Config Filter Managers
Plugins are only available from enabled modules. If you want to provide a
config filter from a php library, all you have to do is implement the
`\Drupal\config_filter\ConfigFilterManagerInterface` and add it to the
service container with a `config.filter` tag.
Services with higher priority will have their filters added first.
@@ -0,0 +1,31 @@
{
"name": "drupal/config_filter",
"type": "drupal-module",
"description": "Config Filter allows other modules to interact with a ConfigStorage through filter plugins.",
"keywords": ["Drupal", "configuration", "configuration management"],
"authors": [
{
"name": "Fabian Bircher",
"email": "opensource@fabianbircher.com",
"homepage": "https://www.drupal.org/u/bircher",
"role": "Maintainer"
},
{
"name": "Nuvole Web",
"email": "info@nuvole.org",
"homepage": "http://nuvole.org",
"role": "Maintainer"
}
],
"homepage": "https://www.drupal.org/project/config_filter",
"support": {
"issues": "https://www.drupal.org/project/issues/config_filter",
"irc": "irc://irc.freenode.org/drupal-contribute",
"source": "http://cgit.drupalcode.org/config_filter"
},
"license": "GPL-2.0+",
"require": {},
"suggest": {
"drupal/config_split": "Split site configuration for different environments."
}
}
@@ -0,0 +1,11 @@
name: Config Filter
type: module
description: Config Filter allows other modules to interact with a ConfigStorage through filter plugins.
# core: 8.x
package: Config
# Information added by Drupal.org packaging script on 2017-11-27
version: '8.x-1.1'
core: '8.x'
project: 'config_filter'
datestamp: 1511779987
@@ -0,0 +1,16 @@
services:
plugin.manager.config_filter:
class: Drupal\config_filter\Plugin\ConfigFilterPluginManager
parent: default_plugin_manager
tags:
- { name: config.filter }
config_filter.storage.sync:
class: Drupal\config_filter\Config\FilteredStorage
factory: config_filter.storage_factory:getSync
decorates: config.storage.staging
public: false
config_filter.storage_factory:
class: Drupal\config_filter\ConfigFilterStorageFactory
arguments: ['@config_filter.storage.sync.inner']
tags:
- { name: service_collector, tag: 'config.filter', call: addConfigFilterManager }
@@ -0,0 +1,62 @@
<?php
namespace Drupal\config_filter\Annotation;
use Drupal\Component\Annotation\Plugin;
/**
* Defines a Config filter plugin item annotation object.
*
* @see \Drupal\config_filter\Plugin\ConfigFilterPluginManager
* @see plugin_api
*
* @Annotation
*/
class ConfigFilter extends Plugin {
/**
* The plugin ID.
*
* @var string
*/
public $id;
/**
* The label of the plugin.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $label;
/**
* The plugin weight.
*
* The higher the weight the later in the filter order the plugin will be.
*
* @var int
*/
public $weight = 0;
/**
* The status of the plugin.
*
* This is an easy way to turn off plugins.
*
* @var bool
*/
public $status = TRUE;
/**
* The storages the plugin filters on.
*
* If it is left empty ['config.storage.sync'] will be assumed.
* The only storage which is currently filtered is 'config.storage.sync'.
*
* @var string[]
*/
public $storages = [];
}
@@ -0,0 +1,248 @@
<?php
namespace Drupal\config_filter\Config;
use Drupal\Core\Config\StorageInterface;
/**
* Class FilteredStorage.
*
* This class wraps another storage.
* It filters the arguments before passing them on to the storage for write
* operations and filters the result of read operations before returning them.
*
* @package Drupal\config_filter\Config
*/
class FilteredStorage implements FilteredStorageInterface {
/**
* The storage container that we are wrapping.
*
* @var \Drupal\Core\Config\StorageInterface
*/
protected $storage;
/**
* The storage filters.
*
* @var \Drupal\config_filter\Config\StorageFilterInterface[]
*/
protected $filters;
/**
* Create a FilteredStorage with some storage and a filter.
*
* @param \Drupal\Core\Config\StorageInterface $storage
* The decorated storage.
* @param \Drupal\config_filter\Config\StorageFilterInterface[] $filters
* The filters to apply in the given order.
*/
public function __construct(StorageInterface $storage, array $filters) {
$this->storage = $storage;
$this->filters = $filters;
// Set the storage to all the filters.
foreach ($this->filters as $filter) {
$filter->setSourceStorage(new ReadOnlyStorage($storage));
$filter->setFilteredStorage($this);
}
}
/**
* {@inheritdoc}
*/
public function exists($name) {
$exists = $this->storage->exists($name);
foreach ($this->filters as $filter) {
$exists = $filter->filterExists($name, $exists);
}
return $exists;
}
/**
* {@inheritdoc}
*/
public function read($name) {
$data = $this->storage->read($name);
foreach ($this->filters as $filter) {
$data = $filter->filterRead($name, $data);
}
return $data;
}
/**
* {@inheritdoc}
*/
public function readMultiple(array $names) {
$data = $this->storage->readMultiple($names);
foreach ($this->filters as $filter) {
$data = $filter->filterReadMultiple($names, $data);
}
ksort($data);
return $data;
}
/**
* {@inheritdoc}
*/
public function write($name, array $data) {
foreach ($this->filters as $filter) {
if ($data) {
$data = $filter->filterWrite($name, $data);
}
else {
// The filterWrite has an array type hint in the interface.
$data = $filter->filterWrite($name, []);
}
}
if ($data) {
return $this->storage->write($name, $data);
}
// The data has been unset, check if it should be deleted.
if ($this->storage->exists($name)) {
foreach ($this->filters as $filter) {
if ($filter->filterWriteEmptyIsDelete($name)) {
return $this->storage->delete($name);
}
}
}
// The data was not written, but it is not an error.
return TRUE;
}
/**
* {@inheritdoc}
*/
public function delete($name) {
$success = TRUE;
foreach ($this->filters as $filter) {
$success = $filter->filterDelete($name, $success);
}
if ($success) {
$success = $this->storage->delete($name);
}
return $success;
}
/**
* {@inheritdoc}
*/
public function rename($name, $new_name) {
$success = TRUE;
foreach ($this->filters as $filter) {
$success = $filter->filterRename($name, $new_name, $success);
}
if ($success) {
$success = $this->storage->rename($name, $new_name);
}
return $success;
}
/**
* {@inheritdoc}
*/
public function encode($data) {
return $this->storage->encode($data);
}
/**
* {@inheritdoc}
*/
public function decode($raw) {
return $this->storage->decode($raw);
}
/**
* {@inheritdoc}
*/
public function listAll($prefix = '') {
$data = $this->storage->listAll($prefix);
foreach ($this->filters as $filter) {
$data = $filter->filterListAll($prefix, $data);
}
sort($data);
return $data;
}
/**
* {@inheritdoc}
*/
public function deleteAll($prefix = '') {
$delete = TRUE;
foreach ($this->filters as $filter) {
$delete = $filter->filterDeleteAll($prefix, $delete);
}
if ($delete) {
return $this->storage->deleteAll($prefix);
}
// The filters returned FALSE for $delete, so we delete the names
// individually and allow filters to prevent deleting the config.
foreach ($this->storage->listAll($prefix) as $name) {
$this->delete($name);
}
// The filters wanted to prevent deleting all and were called to delete the
// individual config name, is this a success? Let us say it is.
return TRUE;
}
/**
* {@inheritdoc}
*/
public function createCollection($collection) {
$filters = [];
foreach ($this->filters as $key => $filter) {
$filter = $filter->filterCreateCollection($collection);
if ($filter) {
$filters[$key] = $filter;
}
}
$storage = $this->storage->createCollection($collection);
$filtered = new static($storage, $filters);
// Set the storage to all the filters.
foreach ($filters as $filter) {
$filter->setSourceStorage(new ReadOnlyStorage($storage));
$filter->setFilteredStorage($filtered);
}
return $filtered;
}
/**
* {@inheritdoc}
*/
public function getAllCollectionNames() {
$collections = $this->storage->getAllCollectionNames();
foreach ($this->filters as $filter) {
$collections = $filter->filterGetAllCollectionNames($collections);
}
sort($collections);
return $collections;
}
/**
* {@inheritdoc}
*/
public function getCollectionName() {
$collection = $this->storage->getCollectionName();
foreach ($this->filters as $filter) {
$collection = $filter->filterGetCollectionName($collection);
}
return $collection;
}
}
@@ -0,0 +1,12 @@
<?php
namespace Drupal\config_filter\Config;
use Drupal\Core\Config\StorageInterface;
/**
* Essentially the StorageInterface, but knowing that config_filter is used.
*/
interface FilteredStorageInterface extends StorageInterface {
}
@@ -0,0 +1,125 @@
<?php
namespace Drupal\config_filter\Config;
use Drupal\Core\Config\StorageInterface;
use Drupal\config_filter\Exception\UnsupportedMethod;
/**
* Class ReadOnlyStorage.
*
* This is like any other StorageInterface, except it does not allow writing.
*
* @package Drupal\config_filter\Config
*/
class ReadOnlyStorage implements StorageInterface {
/**
* The config storage that we are decorating.
*
* @var \Drupal\Core\Config\StorageInterface
*/
protected $storage;
/**
* Create a ReadOnlyStorage decorating another storage.
*
* @param \Drupal\Core\Config\StorageInterface $storage
* The decorated storage.
*/
public function __construct(StorageInterface $storage) {
$this->storage = $storage;
}
/**
* {@inheritdoc}
*/
public function exists($name) {
return $this->storage->exists($name);
}
/**
* {@inheritdoc}
*/
public function read($name) {
return $this->storage->read($name);
}
/**
* {@inheritdoc}
*/
public function readMultiple(array $names) {
return $this->storage->readMultiple($names);
}
/**
* {@inheritdoc}
*/
public function write($name, array $data) {
throw new UnsupportedMethod(__METHOD__ . ' is not allowed on a ReadOnlyStorage');
}
/**
* {@inheritdoc}
*/
public function delete($name) {
throw new UnsupportedMethod(__METHOD__ . ' is not allowed on a ReadOnlyStorage');
}
/**
* {@inheritdoc}
*/
public function rename($name, $new_name) {
throw new UnsupportedMethod(__METHOD__ . ' is not allowed on a ReadOnlyStorage');
}
/**
* {@inheritdoc}
*/
public function encode($data) {
return $this->storage->encode($data);
}
/**
* {@inheritdoc}
*/
public function decode($raw) {
return $this->storage->decode($raw);
}
/**
* {@inheritdoc}
*/
public function listAll($prefix = '') {
return $this->storage->listAll($prefix);
}
/**
* {@inheritdoc}
*/
public function deleteAll($prefix = '') {
throw new UnsupportedMethod(__METHOD__ . ' is not allowed on a ReadOnlyStorage');
}
/**
* {@inheritdoc}
*/
public function createCollection($collection) {
return new static($this->storage->createCollection($collection));
}
/**
* {@inheritdoc}
*/
public function getAllCollectionNames() {
return $this->storage->getAllCollectionNames();
}
/**
* {@inheritdoc}
*/
public function getCollectionName() {
return $this->storage->getCollectionName();
}
}
@@ -0,0 +1,223 @@
<?php
namespace Drupal\config_filter\Config;
use Drupal\Core\Config\StorageInterface;
/**
* Interface StorageFilterInterface.
*
* This interface defines a config storage filter as the FilteredStorage expects
* to use when filtering the operations on the storage. The ConfigFilter plugin
* interface extends this interface, together with plugin related interfaces.
*
* A well-behaved filter does not perform any write operation in a read method.
*
* @package Drupal\config_split\Config
*/
interface StorageFilterInterface {
/**
* Sets the source config storage on which the operation is performed.
*
* The storage is given to the filter when the storage wrapper is set up,
* to avoid passing the storage to each of the filters so that they can read
* from it before writing filtered config. The storage is read-only, use the
* decorated storage to allow all filters to work for write operations.
*
* @param \Drupal\Core\Config\StorageInterface $storage
* The storage on which the operation is performed.
*/
public function setSourceStorage(StorageInterface $storage);
/**
* Sets the wrapped config storage which is using the filter.
*
* This storage is available to the filter in order to inspect how the end
* result looks like. This is useful for reading configuration from the
* storage as drupal will. Beware of recursive calls to the filter.
*
* @param \Drupal\Core\Config\StorageInterface $storage
* The storage which has the filters applied.
*/
public function setFilteredStorage(StorageInterface $storage);
/**
* Filters configuration data after it is read from the storage.
*
* @param string $name
* The name of a configuration object to load.
* @param array|bool $data
* The configuration data to filter.
*
* @return array|bool
* The filtered data.
*/
public function filterRead($name, $data);
/**
* Filter configuration data before it is written to the storage.
*
* @param string $name
* The name of a configuration object to save.
* @param array $data
* The configuration data to filter.
*
* @return array|null
* The filtered data.
*/
public function filterWrite($name, array $data);
/**
* Let the filter decide whether not-writing data should mean delete.
*
* Filters can return NULL for `filterWrite($name, $data)` which means to
* not write the data to the source storage, but it can also mean deleting it.
*
* @param string $name
* The name of a configuration object to save.
*
* @return bool|null
* True to delete at the end of a filtered write action.
*/
public function filterWriteEmptyIsDelete($name);
/**
* Filters whether a configuration object exists.
*
* @param string $name
* The name of a configuration object to test.
* @param bool $exists
* The previous result to alter.
*
* @return bool
* TRUE if the configuration object exists, FALSE otherwise.
*/
public function filterExists($name, $exists);
/**
* Deletes a configuration object from the storage.
*
* @param string $name
* The name of a configuration object to delete.
* @param bool $delete
* Whether the previous filter allows to delete.
*
* @return bool
* TRUE to allow deletion, FALSE otherwise.
*/
public function filterDelete($name, $delete);
/**
* Filters read configuration data from the storage.
*
* @param array $names
* List of names of the configuration objects to load.
* @param array $data
* A list of the configuration data stored for the configuration object name
* that could be loaded for the passed list of names.
*
* @return array
* A list of the configuration data stored for the configuration object name
* that could be loaded for the passed list of names.
*/
public function filterReadMultiple(array $names, array $data);
/**
* Filters renaming a configuration object in the storage.
*
* @param string $name
* The name of a configuration object to rename.
* @param string $new_name
* The new name of a configuration object.
* @param bool $rename
* Allowing renaming by previous filters.
*
* @return bool
* TRUE to allow renaming, FALSE otherwise.
*/
public function filterRename($name, $new_name, $rename);
/**
* Filters what listAll should return.
*
* @param string $prefix
* The prefix to search for. If omitted, all configuration object
* names that exist are returned.
* @param array $data
* The data returned by the storage.
*
* @return array
* The filtered configuration set.
*/
public function filterListAll($prefix, array $data);
/**
* Deletes configuration objects whose names start with a given prefix.
*
* Given the following configuration object names:
* - node.type.article
* - node.type.page.
*
* Passing the prefix 'node.type.' will delete the above configuration
* objects.
*
* @param string $prefix
* The prefix to search for. If omitted, all configuration
* objects that exist will be deleted.
* @param bool $delete
* Whether to delete all or not.
*
* @return bool
* TRUE to allow deleting all, FALSE to trigger individual deletion.
*/
public function filterDeleteAll($prefix, $delete);
/**
* Allows the filter to react on creating a collection on the storage.
*
* A configuration storage can contain multiple sets of configuration objects
* in partitioned collections. The collection name identifies the current
* collection used.
*
* @param string $collection
* The collection name. Valid collection names conform to the following
* regex [a-zA-Z_.]. A storage does not need to have a collection set.
* However, if a collection is set, then the storage should use it to store
* configuration in a way that allows retrieval of configuration for a
* particular collection.
*
* @return \Drupal\config_filter\Config\StorageFilterInterface|null
* Return a filter that should participate in the collection. This allows
* filters to act on different collections. Note that a new instance of the
* filter should be created rather than returning $this directly.
*/
public function filterCreateCollection($collection);
/**
* Filter getting the existing collections.
*
* A configuration storage can contain multiple sets of configuration objects
* in partitioned collections. The collection key name identifies the current
* collection used.
*
* @param string[] $collections
* The array of existing collection names.
*
* @return array
* An array of existing collection names.
*/
public function filterGetAllCollectionNames(array $collections);
/**
* Filter the name of the current collection the storage is using.
*
* @param string $collection
* The collection found by the storage.
*
* @return string
* The current collection name.
*/
public function filterGetCollectionName($collection);
}
@@ -0,0 +1,34 @@
<?php
namespace Drupal\config_filter;
/**
* Interface ConfigFilterManagerInterface.
*/
interface ConfigFilterManagerInterface {
/**
* Get the applicable filters for given storage names.
*
* @param string[] $storage_names
* The names of the storage plugins apply to.
* @param string[] $excluded
* The ids of filters to exclude.
*
* @return \Drupal\config_filter\Config\StorageFilterInterface[]
* The configured filter instances, keyed by filter id.
*/
public function getFiltersForStorages(array $storage_names, array $excluded = []);
/**
* Get a configured filter instance by (plugin) id.
*
* @param string $id
* The plugin id of the filter to load.
*
* @return \Drupal\config_filter\Config\StorageFilterInterface
* The ConfigFilter.
*/
public function getFilterInstance($id);
}
@@ -0,0 +1,93 @@
<?php
namespace Drupal\config_filter;
use Drupal\config_filter\Config\FilteredStorage;
use Drupal\Core\Config\StorageInterface;
/**
* Class ConfigFilterFactory.
*/
class ConfigFilterStorageFactory {
/**
* The decorated sync config storage.
*
* @var \Drupal\Core\Config\StorageInterface
*/
protected $sync;
/**
* The filter managers to load the filters from.
*
* @var \Drupal\config_filter\ConfigFilterManagerInterface[]
*/
protected $managers = [];
/**
* ConfigFilterFactory constructor.
*
* @param \Drupal\Core\Config\StorageInterface $sync
* The original sync storage which is decorated by our filtered storage.
*/
public function __construct(StorageInterface $sync) {
$this->sync = $sync;
}
/**
* Add a config filter manager.
*
* @param \Drupal\config_filter\ConfigFilterManagerInterface $manager
* The ConfigFilter plugin manager.
*/
public function addConfigFilterManager(ConfigFilterManagerInterface $manager) {
$this->managers[] = $manager;
}
/**
* Get the sync storage Drupal uses.
*
* @return \Drupal\config_filter\Config\FilteredStorageInterface
* The decorated sync config storage.
*/
public function getSync() {
return $this->getFilteredStorage($this->sync, ['config.storage.sync']);
}
/**
* Get the sync storage Drupal uses and exclude some plugins.
*
* @param string[] $excluded
* The ids of filters to exclude.
*
* @return \Drupal\config_filter\Config\FilteredStorageInterface
* The decorated sync config storage.
*/
public function getSyncWithoutExcluded(array $excluded) {
return $this->getFilteredStorage($this->sync, ['config.storage.sync'], $excluded);
}
/**
* Get a decorated storage with filters applied.
*
* @param \Drupal\Core\Config\StorageInterface $storage
* The storage to decorate.
* @param string[] $storage_names
* The names of the storage, so the correct filters can be applied.
* @param string[] $excluded
* The ids of filters to exclude.
*
* @return \Drupal\config_filter\Config\FilteredStorageInterface
* The decorated storage with the filters applied.
*/
public function getFilteredStorage(StorageInterface $storage, array $storage_names, array $excluded = []) {
$filters = [];
foreach ($this->managers as $manager) {
// Filters from managers that come first will not be overwritten by
// filters from lower priority managers.
$filters = $filters + $manager->getFiltersForStorages($storage_names, $excluded);
}
return new FilteredStorage($storage, $filters);
}
}
@@ -0,0 +1,9 @@
<?php
namespace Drupal\config_filter\Exception;
/**
* Thrown when calling an unsupported method on a ConfigStorage object.
*/
class UnsupportedMethod extends \BadMethodCallException {
}
@@ -0,0 +1,145 @@
<?php
namespace Drupal\config_filter\Plugin;
use Drupal\Component\Plugin\PluginBase;
use Drupal\Core\Config\StorageInterface;
/**
* Base class for Config filter plugin plugins.
*/
abstract class ConfigFilterBase extends PluginBase implements ConfigFilterInterface {
/**
* The read-only source storage on which the filter operations are performed.
*
* @var \Drupal\Core\Config\StorageInterface
*/
protected $source;
/**
* The wrapped storage which calls the filter.
*
* @var \Drupal\Core\Config\StorageInterface
*/
protected $filtered;
/**
* {@inheritdoc}
*/
public function setSourceStorage(StorageInterface $storage) {
$this->source = $storage;
}
/**
* {@inheritdoc}
*/
public function setFilteredStorage(StorageInterface $storage) {
$this->filtered = $storage;
}
/**
* Get the read-only source Storage.
*
* @return \Drupal\Core\Config\StorageInterface
* The source storage.
*/
protected function getSourceStorage() {
return $this->source;
}
/**
* Get the decorator storage which applies the filters.
*
* @return \Drupal\Core\Config\StorageInterface
* The filtered decorator storage.
*/
protected function getFilteredStorage() {
return $this->filtered;
}
/**
* {@inheritdoc}
*/
public function filterRead($name, $data) {
return $data;
}
/**
* {@inheritdoc}
*/
public function filterWrite($name, array $data) {
return $data;
}
/**
* {@inheritdoc}
*/
public function filterWriteEmptyIsDelete($name) {
return NULL;
}
/**
* {@inheritdoc}
*/
public function filterExists($name, $exists) {
return $exists;
}
/**
* {@inheritdoc}
*/
public function filterDelete($name, $delete) {
return $delete;
}
/**
* {@inheritdoc}
*/
public function filterReadMultiple(array $names, array $data) {
return $data;
}
/**
* {@inheritdoc}
*/
public function filterRename($name, $new_name, $rename) {
return $rename;
}
/**
* {@inheritdoc}
*/
public function filterListAll($prefix, array $data) {
return $data;
}
/**
* {@inheritdoc}
*/
public function filterDeleteAll($prefix, $delete) {
return $delete;
}
/**
* {@inheritdoc}
*/
public function filterCreateCollection($collection) {
return clone $this;
}
/**
* {@inheritdoc}
*/
public function filterGetAllCollectionNames(array $collections) {
return $collections;
}
/**
* {@inheritdoc}
*/
public function filterGetCollectionName($collection) {
return $collection;
}
}
@@ -0,0 +1,13 @@
<?php
namespace Drupal\config_filter\Plugin;
use Drupal\Component\Plugin\PluginInspectionInterface;
use Drupal\config_filter\Config\StorageFilterInterface;
/**
* Defines an interface for Config filter plugin plugins.
*/
interface ConfigFilterInterface extends PluginInspectionInterface, StorageFilterInterface {
}
@@ -0,0 +1,80 @@
<?php
namespace Drupal\config_filter\Plugin;
use Drupal\config_filter\ConfigFilterManagerInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
/**
* Provides the Config filter plugin plugin manager.
*/
class ConfigFilterPluginManager extends DefaultPluginManager implements ConfigFilterManagerInterface {
/**
* Constructor for ConfigFilterPluginManager objects.
*
* @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/ConfigFilter', $namespaces, $module_handler, 'Drupal\config_filter\Plugin\ConfigFilterInterface', 'Drupal\config_filter\Annotation\ConfigFilter');
$this->alterInfo('config_filter_info');
$this->setCacheBackend($cache_backend, 'config_filter_plugins');
}
/**
* {@inheritdoc}
*/
public function getFiltersForStorages(array $storage_names, array $excluded = []) {
$definitions = $this->getDefinitions();
$filters = [];
foreach ($definitions as $id => $definition) {
if ($definition['status'] && array_intersect($storage_names, $definition['storages']) && !in_array($id, $excluded)) {
$filters[$id] = $this->createInstance($id, $definition);
}
}
return $filters;
}
/**
* {@inheritdoc}
*/
public function getFilterInstance($id) {
$definitions = $this->getDefinitions();
if (array_key_exists($id, $definitions)) {
return $this->createInstance($id, $definitions[$id]);
}
return NULL;
}
/**
* {@inheritdoc}
*/
protected function findDefinitions() {
$definitions = array_map(function ($definition) {
if (empty($definition['storages'])) {
// The sync storage is the default.
$definition['storages'] = ['config.storage.sync'];
}
return $definition;
}, parent::findDefinitions());
// Sort the definitions by weight.
uasort($definitions, function ($a, $b) {
return strcmp($a['weight'], $b['weight']);
});
return $definitions;
}
}
@@ -0,0 +1,298 @@
<?php
namespace Drupal\config_filter\Tests;
use Drupal\config_filter\Config\FilteredStorage;
use Drupal\config_filter\Config\ReadOnlyStorage;
use Drupal\config_filter\Config\StorageFilterInterface;
use Drupal\Core\Config\CachedStorage;
use Drupal\Core\Config\StorageInterface;
use Drupal\KernelTests\Core\Config\Storage\CachedStorageTest;
use Prophecy\Argument;
/**
* Tests StorageWrapper operations using the CachedStorage.
*
* @group config_filter
*/
class FilteredStorageTest extends CachedStorageTest {
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
// The storage is a wrapper with a transparent filter.
// So all inherited tests should still pass.
$this->storage = new FilteredStorage($this->storage, [new TransparentFilter()]);
}
/**
* Test that the storage is set on the filters.
*/
public function testSettingStorages() {
$filterReflection = new \ReflectionClass(FilteredStorage::class);
$filtersProperty = $filterReflection->getProperty('filters');
$filtersProperty->setAccessible(TRUE);
/** @var \Drupal\config_filter\Tests\TransparentFilter[] $filters */
$filters = $filtersProperty->getValue($this->storage);
foreach ($filters as $filter) {
// Test that the source storage is a ReadonlyStorage and wraps the cached
// storage from the inherited test.
$readonly = $filter->getPrivateSourceStorage();
$this->assertInstanceOf(ReadOnlyStorage::class, $readonly);
$readonlyReflection = new \ReflectionClass(ReadOnlyStorage::class);
$storageProperty = $readonlyReflection->getProperty('storage');
$storageProperty->setAccessible(TRUE);
$source = $storageProperty->getValue($readonly);
$this->assertInstanceOf(CachedStorage::class, $source);
// Assert that the filter gets the storage.
$this->assertEquals($this->storage, $filter->getPrivateFilteredStorage());
}
}
/**
* Test the read methods invokes the correct filter methods.
*
* @dataProvider readFilterProvider
*/
public function testReadFilter($name, $storageMethod, $filterMethod, $data, $expected) {
$source = $this->prophesize(StorageInterface::class);
$filterA = $this->prophesizeFilter();
$filterB = $this->prophesizeFilter();
$source->$storageMethod($name)->willReturn($data);
$interim = $this->randomArray();
$filterA->$filterMethod($name, $data)->willReturn($interim);
$filterB->$filterMethod($name, $interim)->willReturn($expected);
$storage = new FilteredStorage($source->reveal(), [$filterA->reveal(), $filterB->reveal()]);
$this->assertEquals($expected, $storage->$storageMethod($name));
}
/**
* Data provider for testReadFilter.
*/
public function readFilterProvider() {
// @codingStandardsIgnoreStart
return [
[$this->randomString(), 'exists', 'filterExists', TRUE, TRUE],
[$this->randomString(), 'exists', 'filterExists', TRUE, FALSE],
[$this->randomString(), 'exists', 'filterExists', FALSE, TRUE],
[$this->randomString(), 'exists', 'filterExists', FALSE, FALSE],
[$this->randomString(), 'read', 'filterRead', $this->randomArray(), $this->randomArray()],
[$this->randomString(), 'read', 'filterRead', NULL, $this->randomArray()],
[$this->randomString(), 'read', 'filterRead', $this->randomArray(), NULL],
[
[$this->randomString(), $this->randomString()],
'readMultiple',
'filterReadMultiple',
[$this->randomArray(), $this->randomArray()],
[$this->randomArray(), $this->randomArray()],
],
[
[$this->randomString(), $this->randomString()],
'readMultiple',
'filterReadMultiple',
[$this->randomArray(), FALSE],
[$this->randomArray(), $this->randomArray()],
],
[
$this->randomString(),
'listAll',
'filterListAll',
['a' . $this->randomString(), 'b' . $this->randomString()],
['a' . $this->randomString(), 'b' . $this->randomString()],
],
];
// @codingStandardsIgnoreEnd
}
/**
* Test the write method invokes the filterWrite in filters.
*
* @dataProvider writeFilterProvider
*/
public function testWriteFilter($interim, $expected, $exists = TRUE) {
$name = $this->randomString();
$data = $this->randomArray();
$source = $this->prophesize(StorageInterface::class);
$filterA = $this->prophesizeFilter();
$filterB = $this->prophesizeFilter();
$filterA->filterWrite($name, $data)->willReturn($interim);
$interim = is_array($interim) ? $interim : [];
$filterB->filterWrite($name, $interim)->willReturn($expected);
if ($expected) {
$source->write($name, $expected)->willReturn(TRUE);
}
else {
$source->write(Argument::any())->shouldNotBeCalled();
$source->exists($name)->willReturn($exists);
if ($exists) {
$filterA->filterWriteEmptyIsDelete($name)->willReturn(TRUE);
$source->delete($name)->willReturn(TRUE);
}
}
$storage = new FilteredStorage($source->reveal(), [$filterA->reveal(), $filterB->reveal()]);
$this->assertTrue($storage->write($name, $data));
}
/**
* Data provider for testWriteFilter.
*/
public function writeFilterProvider() {
return [
[$this->randomArray(), $this->randomArray()],
[NULL, $this->randomArray()],
[[], $this->randomArray()],
[$this->randomArray(), NULL, FALSE],
[$this->randomArray(), [], FALSE],
[$this->randomArray(), NULL, TRUE],
];
}
/**
* Test the delete method invokes the filterDelete in filters.
*
* @dataProvider deleteFilterProvider
*/
public function testDeleteFilter($interim, $expected) {
$name = $this->randomString();
$source = $this->prophesize(StorageInterface::class);
$filterA = $this->prophesizeFilter();
$filterB = $this->prophesizeFilter();
$filterA->filterDelete($name, TRUE)->willReturn($interim);
$filterB->filterDelete($name, $interim)->willReturn($expected);
if ($expected) {
$source->delete($name)->willReturn(TRUE);
}
else {
$source->delete(Argument::any())->shouldNotBeCalled();
}
$storage = new FilteredStorage($source->reveal(), [$filterA->reveal(), $filterB->reveal()]);
$this->assertEquals($expected, $storage->delete($name));
}
/**
* Data provider for testDeleteFilter.
*/
public function deleteFilterProvider() {
return [
[TRUE, TRUE],
[FALSE, TRUE],
[TRUE, FALSE],
[FALSE, FALSE],
];
}
/**
* Test the rename method invokes the filterRename in filters.
*
* @dataProvider renameFilterProvider
*/
public function testRenameFilter($interim, $expected) {
$name = $this->randomString();
$name2 = $this->randomString();
$source = $this->prophesize(StorageInterface::class);
$filterA = $this->prophesizeFilter();
$filterB = $this->prophesizeFilter();
$filterA->filterRename($name, $name2, TRUE)->willReturn($interim);
$filterB->filterRename($name, $name2, $interim)->willReturn($expected);
if ($expected) {
$source->rename($name, $name2)->willReturn(TRUE);
}
else {
$source->rename(Argument::any())->shouldNotBeCalled();
}
$storage = new FilteredStorage($source->reveal(), [$filterA->reveal(), $filterB->reveal()]);
$this->assertEquals($expected, $storage->rename($name, $name2));
}
/**
* Data provider for testRenameFilter.
*/
public function renameFilterProvider() {
return [
[TRUE, TRUE],
[FALSE, TRUE],
[TRUE, FALSE],
[FALSE, FALSE],
];
}
/**
* Test the deleteAll method invokes the filterDeleteAll in filters.
*
* @dataProvider deleteAllFilterProvider
*/
public function testDeleteAllFilter($interim, $expected) {
$name = $this->randomString();
$source = $this->prophesize(StorageInterface::class);
$filterA = $this->prophesizeFilter();
$filterB = $this->prophesizeFilter();
$filterA->filterDeleteAll($name, TRUE)->willReturn($interim);
$filterB->filterDeleteAll($name, $interim)->willReturn($expected);
if ($expected) {
$source->deleteAll($name)->willReturn(TRUE);
}
else {
$source->deleteAll(Argument::any())->shouldNotBeCalled();
$all = [$this->randomString(), $this->randomString()];
$source->listAll($name)->willReturn($all);
foreach ($all as $item) {
$filterA->filterDelete($item, TRUE)->willReturn(TRUE);
$filterB->filterDelete($item, TRUE)->willReturn(FALSE);
}
}
$storage = new FilteredStorage($source->reveal(), [$filterA->reveal(), $filterB->reveal()]);
$this->assertTrue($storage->deleteAll($name));
}
/**
* Data provider for testDeleteAllFilter.
*/
public function deleteAllFilterProvider() {
return [
[TRUE, TRUE],
[FALSE, TRUE],
[TRUE, FALSE],
[FALSE, FALSE],
];
}
/**
* Prophesize a StorageFilter.
*/
protected function prophesizeFilter() {
$filter = $this->prophesize(StorageFilterInterface::class);
$filter->setSourceStorage(Argument::type(ReadOnlyStorage::class))->shouldBeCalledTimes(1);
$filter->setFilteredStorage(Argument::type(FilteredStorage::class))->shouldBeCalledTimes(1);
return $filter;
}
/**
* Create a random array.
*/
protected function randomArray($size = 4) {
return (array) $this->randomObject($size);
}
}
@@ -0,0 +1,138 @@
<?php
namespace Drupal\config_filter\Tests;
use Drupal\config_filter\Config\ReadOnlyStorage;
use Drupal\config_filter\Exception\UnsupportedMethod;
use Drupal\Core\Config\StorageInterface;
use Drupal\Tests\UnitTestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\MethodProphecy;
/**
* Tests ReadonlyStorage operations.
*
* @group config_filter
*/
class ReadonlyStorageTest extends UnitTestCase {
/**
* Wrap a given storage.
*
* This is useful when testing a subclass of ReadonlyStorage.
*
* @param \Drupal\Core\Config\StorageInterface $source
* The storage to decorate.
*
* @return \Drupal\Core\Config\StorageInterface
* The storage wrapping the source.
*/
protected function getStorage(StorageInterface $source) {
return new ReadOnlyStorage($source);
}
/**
* Test methods that should be transparent.
*
* @dataProvider readMethodsProvider
*/
public function testReadOperations($method, $arguments, $returnValue) {
$source = $this->prophesize(StorageInterface::class);
$methodProhecy = new MethodProphecy($source, $method, $arguments);
$methodProhecy->shouldBeCalledTimes(1);
$methodProhecy->willReturn($returnValue);
$source->addMethodProphecy($methodProhecy);
$storage = $this->getStorage($source->reveal());
$actual = call_user_func_array([$storage, $method], $arguments);
$this->assertEquals($actual, $returnValue);
}
/**
* Provide the methods that should continue to work.
*
* @return array
* The data.
*/
public function readMethodsProvider() {
return [
['exists', [$this->randomMachineName()], $this->randomMachineName()],
['read', [$this->randomMachineName()], $this->randomArray()],
['readMultiple', [$this->randomArray()], $this->randomArray()],
['encode', [$this->randomArray()], $this->randomMachineName()],
['decode', [$this->randomMachineName()], $this->randomArray()],
['listAll', [$this->randomMachineName()], $this->randomArray()],
['getAllCollectionNames', [], $this->randomArray()],
['getCollectionName', [], $this->randomMachineName()],
];
}
/**
* Test creating a collection.
*
* Creating collections returns a new instance, make sure it decorates the
* new instance of the source.
*/
public function testCreateCollection() {
$name = $this->randomMachineName();
$source = $this->prophesize(StorageInterface::class);
$collectionSource = $this->prophesize(StorageInterface::class)->reveal();
$source->createCollection($name)->willReturn($collectionSource);
$storage = $this->getStorage($source->reveal());
$collectionStorage = $storage->createCollection($name);
$this->assertInstanceOf(ReadOnlyStorage::class, $collectionStorage);
$readonlyReflection = new \ReflectionClass(ReadOnlyStorage::class);
$storageProperty = $readonlyReflection->getProperty('storage');
$storageProperty->setAccessible(TRUE);
$actualSource = $storageProperty->getValue($collectionStorage);
$this->assertEquals($collectionSource, $actualSource);
}
/**
* Test the operations that should throw an error.
*
* @dataProvider writeMethodsProvider
*/
public function testWriteOperations($method, $arguments) {
$source = $this->prophesize(StorageInterface::class);
$source->$method(Argument::any())->shouldNotBeCalled();
$storage = $this->getStorage($source->reveal());
try {
call_user_func_array([$storage, $method], $arguments);
$this->fail();
}
catch (UnsupportedMethod $exception) {
$this->assertEquals(ReadOnlyStorage::class . '::' . $method . ' is not allowed on a ReadOnlyStorage', $exception->getMessage());
}
}
/**
* Provide the methods that should throw an exception.
*
* @return array
* The data
*/
public function writeMethodsProvider() {
return [
['write', [$this->randomMachineName(), $this->randomArray()]],
['delete', [$this->randomMachineName()]],
['rename', [$this->randomMachineName(), $this->randomMachineName()]],
['deleteAll', [$this->randomMachineName()]],
];
}
/**
* Get a random array.
*
* @return array
* A random array used for data testing.
*/
protected function randomArray() {
return (array) $this->getRandomGenerator()->object();
}
}
@@ -0,0 +1,40 @@
<?php
namespace Drupal\config_filter\Tests;
use Drupal\config_filter\Config\StorageFilterInterface;
use Drupal\config_filter\Plugin\ConfigFilterBase;
/**
* Class TransparentFilter.
*/
class TransparentFilter extends ConfigFilterBase implements StorageFilterInterface {
/**
* TransparentFilter constructor.
*/
public function __construct() {
parent::__construct([], 'transparent_test', []);
}
/**
* Get the read-only source Storage.
*
* @return \Drupal\Core\Config\StorageInterface
* The source storage.
*/
public function getPrivateSourceStorage() {
return $this->getSourceStorage();
}
/**
* Get the decorator storage which applies the filters.
*
* @return \Drupal\Core\Config\StorageInterface
* The filtered decorator storage.
*/
public function getPrivateFilteredStorage() {
return $this->getFilteredStorage();
}
}
@@ -0,0 +1,24 @@
{
"name": "nuvoleweb/config-filter-test-site",
"description": "Config Filter test site.",
"type": "project",
"require": {
"composer/installers": "^1.2",
"drupal-composer/drupal-scaffold": "^2.2",
"drupal/core": "~8",
"drush/drush": "~8.0",
"drupal/coder": "^8.2",
"squizlabs/php_codesniffer": "^2.8",
"phpunit/phpunit": "5.5.*",
"bovigo/assert": "~1.7",
"mikey179/vfsStream": "*"
},
"minimum-stability": "dev",
"prefer-stable": true,
"repositories": [
{"type": "composer", "url": "https://packages.drupal.org/8"}
],
"conflict": {
"drupal/drupal": "*"
}
}
@@ -0,0 +1,40 @@
# Config Filter Split test
This is a module for testing the proper functioning of config_filter.
But it also demonstrates how a filter can be used to read and write to a
different config storage. As such it is an instructive example.
But it also has a practical purpose: We can use it to read migrate config
directly from files. Of course it is in general a bad idea to directly
edit the active configuration since Drupal does many checks and database
alterations when synchronizing configuration.
This is just here for the brave. You have been warned.
## Developing with migrate configuration read from the files.
Migrations are configurations, by default active configuration is stored in the
database. So when developing, the configuration has to be synced all the time.
To avoid this we can split out the migrate configuration to read directly
from the files.
Enable the config_filter_split_test testing module from config_filter.
To swap out the active storage, add the following to your services.local.yml:
```yaml
services:
config.storage:
class: Drupal\config_filter_split_test\Config\ActiveMigrateStorage
arguments:
- '@config.storage.active'
- '@cache.config'
- '../config/migrate_active' # The path to the folder.
```
Migrations are plugins, and plugin definitions are cached. The configuration
storage is also cached.
So either you have to clear the caches after you edit the migration or you add
the following to your settings.php (assuming you have the null cache set too):
```php
$settings['cache']['bins']['config'] = 'cache.backend.null';
$settings['cache']['bins']['discovery'] = 'cache.backend.null';
```
This has an obvious performance implication.
@@ -0,0 +1,12 @@
name: 'Simple split filter'
type: module
# core: 8.x
package: Testing
dependencies:
- drupal:config_filter
# Information added by Drupal.org packaging script on 2017-11-27
version: '8.x-1.1'
core: '8.x'
project: 'config_filter'
datestamp: 1511779987
@@ -0,0 +1,34 @@
<?php
namespace Drupal\config_filter_migrate_test\Config;
use Drupal\config_filter\Config\FilteredStorage;
use Drupal\config_filter_split_test\Plugin\ConfigFilter\TestSplitFilter;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Config\CachedStorage;
use Drupal\Core\Config\FileStorage;
use Drupal\Core\Config\StorageInterface;
/**
* Class ActiveMigrateStorage.
*/
class ActiveMigrateStorage extends CachedStorage {
/**
* Create an ActiveMigrateStorage.
*
* @param \Drupal\Core\Config\StorageInterface $storage
* The decorated storage.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache
* A cache backend used to store configuration.
* @param string $migrate_folder
* The migrate directory.
*/
public function __construct(StorageInterface $storage, CacheBackendInterface $cache, $migrate_folder) {
// Create the filter directly, the plugin manager is not yet available.
$filter = new TestSplitFilter(new FileStorage($migrate_folder), 'migrate_plus.migration');
// Wrap the storage with the the filtered storage.
parent::__construct(new FilteredStorage($storage, [$filter]), $cache);
}
}
@@ -0,0 +1,167 @@
<?php
namespace Drupal\config_filter_split_test\Plugin\ConfigFilter;
use Drupal\config_filter\Plugin\ConfigFilterBase;
use Drupal\Core\Config\DatabaseStorage;
use Drupal\Core\Config\StorageInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a TestSplitFilter.
*
* This is a very basic split filter to test that config_filter applies the
* filters correctly. For more advanced and configurable split filters use the
* Configuration Split (config_split) module.
*
* @ConfigFilter(
* id = "config_filter_split_test",
* label = @Translation("Filter Split test"),
* storages = {"test_storage"},
* )
*/
class TestSplitFilter extends ConfigFilterBase implements ContainerFactoryPluginInterface {
/**
* The File storage to read the migrations from.
*
* @var \Drupal\Core\Config\StorageInterface
*/
protected $storage;
/**
* The name prefix to split.
*
* @var string
*/
protected $name;
/**
* Constructs a new TestSplitFilter.
*
* @param \Drupal\Core\Config\StorageInterface $storage
* The migrate storage.
* @param string $name
* The config name prefix to split.
*/
public function __construct(StorageInterface $storage, $name) {
parent::__construct([], 'config_filter_split_test', []);
$this->storage = $storage;
$this->name = $name;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(new DatabaseStorage($container->get('database'), 'config_filter_split_test'), 'core.');
}
/**
* Decide to split the config off or not.
*
* @param string $name
* The name of the configuration to check.
*
* @return bool
* Whether the configuration is supposed to be split.
*/
protected function isSplitConfig($name) {
return (strpos($name, $this->name) === 0);
}
/**
* {@inheritdoc}
*/
public function filterRead($name, $data) {
if ($this->isSplitConfig($name)) {
if ($this->storage->exists($name)) {
$data = $this->storage->read($name);
}
}
return $data;
}
/**
* {@inheritdoc}
*/
public function filterExists($name, $exists) {
if ($this->isSplitConfig($name) && !$exists) {
$exists = $this->storage->exists($name);
}
return $exists;
}
/**
* {@inheritdoc}
*/
public function filterReadMultiple(array $names, array $data) {
return array_merge($data, $this->storage->readMultiple($names));
}
/**
* {@inheritdoc}
*/
public function filterListAll($prefix, array $data) {
return array_unique(array_merge($data, $this->storage->listAll($prefix)));
}
/**
* {@inheritdoc}
*/
public function filterWrite($name, array $data) {
if ($this->isSplitConfig($name)) {
$this->storage->write($name, $data);
return NULL;
}
return $data;
}
/**
* {@inheritdoc}
*/
public function filterWriteEmptyIsDelete($name) {
return ($this->isSplitConfig($name) ? TRUE : NULL);
}
/**
* {@inheritdoc}
*/
public function filterDelete($name, $delete) {
if ($delete && $this->storage->exists($name)) {
// Call delete on the secondary storage anyway.
$this->storage->delete($name);
}
return $delete;
}
/**
* {@inheritdoc}
*/
public function filterDeleteAll($prefix, $delete) {
if ($delete && $this->storage) {
try {
$this->storage->deleteAll($prefix);
}
catch (\UnexpectedValueException $exception) {
// The file storage tries to remove directories of collections. But this
// fails if the directory doesn't exist. So everything is actually fine.
}
}
return $delete;
}
/**
* {@inheritdoc}
*/
public function filterCreateCollection($collection) {
return new static($this->storage->createCollection($collection), $this->name);
}
}
@@ -0,0 +1,12 @@
name: Config Filter Test
type: module
# core: 8.x
package: Testing
dependencies:
- drupal:config_filter
# Information added by Drupal.org packaging script on 2017-11-27
version: '8.x-1.1'
core: '8.x'
project: 'config_filter'
datestamp: 1511779987
@@ -0,0 +1,40 @@
<?php
namespace Drupal\config_filter_test\plugin\ConfigFilter;
use Drupal\config_filter\Plugin\ConfigFilterBase;
/**
* Provides a pirate filter that adds "Arrr" to the site name.
*
* @ConfigFilter(
* id = "pirate_filter",
* label = "More pirates! Arrr",
* weight = 10
* )
*/
class PirateFilter extends ConfigFilterBase {
/**
* {@inheritdoc}
*/
public function filterRead($name, $data) {
if ($name == 'system.site') {
$data['name'] = $data['name'] . ' Arrr';
}
return $data;
}
/**
* {@inheritdoc}
*/
public function filterReadMultiple(array $names, array $data) {
if (in_array('system.site', $names)) {
$data['system.site'] = $this->filterRead('system.site', $data['system.site']);
}
return $data;
}
}
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="core/tests/bootstrap.php" backupGlobals="false" colors="true" >
<php>
<ini name="error_reporting" value="32767"/>
<ini name="memory_limit" value="-1"/>
<env name="SIMPLETEST_DB" value="mysql://travis:@127.0.0.1/drupal"/>
</php>
<testsuites>
<testsuite name="tests">
<directory>./modules/config_filter/</directory>
</testsuite>
</testsuites>
</phpunit>

Some files were not shown because too many files have changed in this diff Show More