Pathauto and dependencies

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

View file

@ -0,0 +1,96 @@
language: php
cache:
bundler: true
directories:
- $HOME/tmp/drush
- $HOME/.bundle
apt: true
php:
- 5.4
- 5.5
env:
- PATH=$PATH:/home/travis/.composer/vendor/bin
# This will create the database
mysql:
database: drupal
username: root
encoding: utf8
# To be able to run a webbrowser
# If we need anything more powerful
# than e.g. phantomjs
before_install:
- "export DISPLAY=:99.0"
- "sh -e /etc/init.d/xvfb start"
install:
# Grab Drush
- composer global require drush/drush:dev-master --prefer-source
- cd /home/travis/.composer/vendor/drush/drush && cd -
# Make sure we don't fail when checking out projects
- echo -e "Host github.com\n\tStrictHostKeyChecking no\n" >> ~/.ssh/config
# LAMP package installation (mysql is already started)
- sudo apt-get update
- sudo apt-get install apache2 libapache2-mod-fastcgi
# enable php-fpm, travis does not support any other method with php and apache
- sudo cp ~/.phpenv/versions/$(phpenv version-name)/etc/php-fpm.conf.default ~/.phpenv/versions/$(phpenv version-name)/etc/php-fpm.conf
- sudo a2enmod rewrite actions fastcgi alias
- echo "cgi.fix_pathinfo = 1" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini
- ~/.phpenv/versions/$(phpenv version-name)/sbin/php-fpm
# Make sure the apache root is in our wanted directory
- echo "$(curl -fsSL https://gist.githubusercontent.com/nickveenhof/11386315/raw/b8abaf9304fe12b5cc7752d39c29c1edae8ac2e6/gistfile1.txt)" | sed -e "s,PATH,$TRAVIS_BUILD_DIR/../drupal,g" | sudo tee /etc/apache2/sites-available/default > /dev/null
# Set sendmail so drush doesn't throw an error during site install.
- echo "sendmail_path='true'" >> `php --ini | grep "Loaded Configuration" | awk '{print $4}'`
# Forward the errors to the syslog so we can print them
- echo "error_log=syslog" >> `php --ini | grep "Loaded Configuration" | awk '{print $4}'`
# Get latest drupal 8 core
- cd $TRAVIS_BUILD_DIR/..
- git clone --depth 1 --branch 8.0.x http://git.drupal.org/project/drupal.git
# Restart apache and test it
- sudo service apache2 restart
- curl -v "http://localhost"
# Re-enable when trying to get CodeSniffer doesn't return a 403 anymore.
#- composer global require drupal/coder:\>7
before_script:
- cd $TRAVIS_BUILD_DIR/../drupal
# Update drupal core
- git pull origin 8.0.x
# Install the site
- drush -v site-install minimal --db-url=mysql://root:@localhost/drupal --yes
- drush en --yes simpletest
- drush cr
- phpenv rehash
script:
# go to our Drupal module directory
- mkdir $TRAVIS_BUILD_DIR/../drupal/modules/token
- cp -R $TRAVIS_BUILD_DIR/* $TRAVIS_BUILD_DIR/../drupal/modules/token/
# go to our Drupal main directory
- cd $TRAVIS_BUILD_DIR/../drupal
- ls -la $TRAVIS_BUILD_DIR/../drupal/sites/default
# Run the tests
- php core/scripts/run-tests.sh --verbose --color --concurrency 4 --php `which php` --url http://localhost "token" | tee /tmp/test.txt; TEST_EXIT=${PIPESTATUS[0]}
- echo $TEST_EXIT
# Check if we had fails in the run-tests.sh script
# Exit with the inverted value, because if there are no fails found, it will exit with 1 and for us that\
# is a good thing so invert it to 0. Travis has some issues with the exclamation mark in front so we have to fiddle a
# bit.
# Also make the grep case insensitive and fail on run-tests.sh regular fails as well on fatal errors.
- TEST_OUTPUT=$(! egrep -i "([0-9]+ fails)|(PHP Fatal error)|([0-9]+ exceptions)" /tmp/test.txt > /dev/null)$?
- echo $TEST_OUTPUT
- cd $TRAVIS_BUILD_DIR/../drupal/core
- ./vendor/bin/phpunit --verbose --debug ../modules/token/; TEST_PHPUNIT=$?
- echo $TEST_PHPUNIT
# if the TEST_EXIT status is 0 AND the TEST_OUTPUT status is also 0 it means we succeeded, in all other cases we
# failed.
# Re-enable when trying to get CodeSniffer doesn't return a 403 anymore.
#- /home/travis/.composer/vendor/bin/phpcs --standard=/home/travis/.composer/vendor/drupal/coder/coder_sniffer/Drupal --extensions=php,inc,test,module,install --ignore=css/ $TRAVIS_BUILD_DIR/../drupal/modules/search_api
- php -i | grep 'php.ini'
- sudo cat /var/log/apache2/error.log
- sudo cat /var/log/syslog | grep 'php' | cat # Suppress grep exit status 1
# Exit the build
- if [ $TEST_EXIT -eq 0 ] && [ $TEST_OUTPUT -eq 0 ] && [ $TEST_PHPUNIT -eq 0 ]; then exit 0; else exit 1; fi

View file

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

View file

@ -0,0 +1,33 @@
INTRODUCTION
------------
Provides common and resuable token UI elements and missing core tokens.
* For a full description of the module, visit the project page:
https://drupal.org/project/token
* To submit bug reports and feature suggestions, or to track changes:
https://drupal.org/project/issues/token
INSTALLATION
------------
Install as usual, see
https://www.drupal.org/docs/8/extending-drupal-8/installing-contributed-modules-find-import-enable-configure-drupal-8 for further
information.
TROUBLESHOOTING
---------------
Token module doesn't provide any visible functions to the user on its own, it
just provides token handling services for other modules.
MAINTAINERS
-----------
Current maintainers:
* Dave Reid (https://drupal.org/user/53892)

View file

@ -0,0 +1,28 @@
table.treetable span.indenter {
display: inline-block;
margin: 0;
padding: 0;
text-align: right;
/* Disable text selection of nodes (for better D&D UX) */
user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-o-user-select: none;
-webkit-user-select: none;
/* Force content-box box model for indenter (Bootstrap compatibility) */
-webkit-box-sizing: content-box;
-moz-box-sizing: content-box;
box-sizing: content-box;
width: 19px;
}
table.treetable span.indenter a {
background-position: left center;
background-repeat: no-repeat;
display: inline-block;
text-decoration: none;
width: 19px;
}

View file

@ -0,0 +1,32 @@
.token-tree-dialog {
border: 1px solid #6b6b6b;
border-radius: 5px 5px 0 0;
box-shadow: 0 0 10px #6b6b6b;
}
.token-tree {
font-size: 0.85em;
margin-left: 19px;
}
.ui-dialog-content .token-tree {
margin-left: 0;
}
.token-tree td, .token-tree th {
padding-top: 0;
padding-bottom: 0;
}
.token-group {
font-weight: bold;
}
.js .token-group {
font-weight: normal;
}
/* Prevent the token columns from being wrapped. */
.token-tree td.token-key {
white-space: nowrap;
}

View file

@ -0,0 +1,50 @@
table.treetable {
}
table.treetable caption {
}
table.treetable thead {
}
table.treetable thead tr th {
}
table.treetable tbody tr td {
cursor: default;
}
table.treetable span {
background-position: center left;
background-repeat: no-repeat;
padding: .2em 0 .2em 1.5em;
}
table.treetable tr.collapsed span.indenter a {
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAsTAAALEwEAmpwYAAAKT2lDQ1BQaG90b3Nob3AgSUNDIHByb2ZpbGUAAHjanVNnVFPpFj333vRCS4iAlEtvUhUIIFJCi4AUkSYqIQkQSoghodkVUcERRUUEG8igiAOOjoCMFVEsDIoK2AfkIaKOg6OIisr74Xuja9a89+bN/rXXPues852zzwfACAyWSDNRNYAMqUIeEeCDx8TG4eQuQIEKJHAAEAizZCFz/SMBAPh+PDwrIsAHvgABeNMLCADATZvAMByH/w/qQplcAYCEAcB0kThLCIAUAEB6jkKmAEBGAYCdmCZTAKAEAGDLY2LjAFAtAGAnf+bTAICd+Jl7AQBblCEVAaCRACATZYhEAGg7AKzPVopFAFgwABRmS8Q5ANgtADBJV2ZIALC3AMDOEAuyAAgMADBRiIUpAAR7AGDIIyN4AISZABRG8lc88SuuEOcqAAB4mbI8uSQ5RYFbCC1xB1dXLh4ozkkXKxQ2YQJhmkAuwnmZGTKBNA/g88wAAKCRFRHgg/P9eM4Ors7ONo62Dl8t6r8G/yJiYuP+5c+rcEAAAOF0ftH+LC+zGoA7BoBt/qIl7gRoXgugdfeLZrIPQLUAoOnaV/Nw+H48PEWhkLnZ2eXk5NhKxEJbYcpXff5nwl/AV/1s+X48/Pf14L7iJIEyXYFHBPjgwsz0TKUcz5IJhGLc5o9H/LcL//wd0yLESWK5WCoU41EScY5EmozzMqUiiUKSKcUl0v9k4t8s+wM+3zUAsGo+AXuRLahdYwP2SycQWHTA4vcAAPK7b8HUKAgDgGiD4c93/+8//UegJQCAZkmScQAAXkQkLlTKsz/HCAAARKCBKrBBG/TBGCzABhzBBdzBC/xgNoRCJMTCQhBCCmSAHHJgKayCQiiGzbAdKmAv1EAdNMBRaIaTcA4uwlW4Dj1wD/phCJ7BKLyBCQRByAgTYSHaiAFiilgjjggXmYX4IcFIBBKLJCDJiBRRIkuRNUgxUopUIFVIHfI9cgI5h1xGupE7yAAygvyGvEcxlIGyUT3UDLVDuag3GoRGogvQZHQxmo8WoJvQcrQaPYw2oefQq2gP2o8+Q8cwwOgYBzPEbDAuxsNCsTgsCZNjy7EirAyrxhqwVqwDu4n1Y8+xdwQSgUXACTYEd0IgYR5BSFhMWE7YSKggHCQ0EdoJNwkDhFHCJyKTqEu0JroR+cQYYjIxh1hILCPWEo8TLxB7iEPENyQSiUMyJ7mQAkmxpFTSEtJG0m5SI+ksqZs0SBojk8naZGuyBzmULCAryIXkneTD5DPkG+Qh8lsKnWJAcaT4U+IoUspqShnlEOU05QZlmDJBVaOaUt2ooVQRNY9aQq2htlKvUYeoEzR1mjnNgxZJS6WtopXTGmgXaPdpr+h0uhHdlR5Ol9BX0svpR+iX6AP0dwwNhhWDx4hnKBmbGAcYZxl3GK+YTKYZ04sZx1QwNzHrmOeZD5lvVVgqtip8FZHKCpVKlSaVGyovVKmqpqreqgtV81XLVI+pXlN9rkZVM1PjqQnUlqtVqp1Q61MbU2epO6iHqmeob1Q/pH5Z/YkGWcNMw09DpFGgsV/jvMYgC2MZs3gsIWsNq4Z1gTXEJrHN2Xx2KruY/R27iz2qqaE5QzNKM1ezUvOUZj8H45hx+Jx0TgnnKKeX836K3hTvKeIpG6Y0TLkxZVxrqpaXllirSKtRq0frvTau7aedpr1Fu1n7gQ5Bx0onXCdHZ4/OBZ3nU9lT3acKpxZNPTr1ri6qa6UbobtEd79up+6Ynr5egJ5Mb6feeb3n+hx9L/1U/W36p/VHDFgGswwkBtsMzhg8xTVxbzwdL8fb8VFDXcNAQ6VhlWGX4YSRudE8o9VGjUYPjGnGXOMk423GbcajJgYmISZLTepN7ppSTbmmKaY7TDtMx83MzaLN1pk1mz0x1zLnm+eb15vft2BaeFostqi2uGVJsuRaplnutrxuhVo5WaVYVVpds0atna0l1rutu6cRp7lOk06rntZnw7Dxtsm2qbcZsOXYBtuutm22fWFnYhdnt8Wuw+6TvZN9un2N/T0HDYfZDqsdWh1+c7RyFDpWOt6azpzuP33F9JbpL2dYzxDP2DPjthPLKcRpnVOb00dnF2e5c4PziIuJS4LLLpc+Lpsbxt3IveRKdPVxXeF60vWdm7Obwu2o26/uNu5p7ofcn8w0nymeWTNz0MPIQ+BR5dE/C5+VMGvfrH5PQ0+BZ7XnIy9jL5FXrdewt6V3qvdh7xc+9j5yn+M+4zw33jLeWV/MN8C3yLfLT8Nvnl+F30N/I/9k/3r/0QCngCUBZwOJgUGBWwL7+Hp8Ib+OPzrbZfay2e1BjKC5QRVBj4KtguXBrSFoyOyQrSH355jOkc5pDoVQfujW0Adh5mGLw34MJ4WHhVeGP45wiFga0TGXNXfR3ENz30T6RJZE3ptnMU85ry1KNSo+qi5qPNo3ujS6P8YuZlnM1VidWElsSxw5LiquNm5svt/87fOH4p3iC+N7F5gvyF1weaHOwvSFpxapLhIsOpZATIhOOJTwQRAqqBaMJfITdyWOCnnCHcJnIi/RNtGI2ENcKh5O8kgqTXqS7JG8NXkkxTOlLOW5hCepkLxMDUzdmzqeFpp2IG0yPTq9MYOSkZBxQqohTZO2Z+pn5mZ2y6xlhbL+xW6Lty8elQfJa7OQrAVZLQq2QqboVFoo1yoHsmdlV2a/zYnKOZarnivN7cyzytuQN5zvn//tEsIS4ZK2pYZLVy0dWOa9rGo5sjxxedsK4xUFK4ZWBqw8uIq2Km3VT6vtV5eufr0mek1rgV7ByoLBtQFr6wtVCuWFfevc1+1dT1gvWd+1YfqGnRs+FYmKrhTbF5cVf9go3HjlG4dvyr+Z3JS0qavEuWTPZtJm6ebeLZ5bDpaql+aXDm4N2dq0Dd9WtO319kXbL5fNKNu7g7ZDuaO/PLi8ZafJzs07P1SkVPRU+lQ27tLdtWHX+G7R7ht7vPY07NXbW7z3/T7JvttVAVVN1WbVZftJ+7P3P66Jqun4lvttXa1ObXHtxwPSA/0HIw6217nU1R3SPVRSj9Yr60cOxx++/p3vdy0NNg1VjZzG4iNwRHnk6fcJ3/ceDTradox7rOEH0x92HWcdL2pCmvKaRptTmvtbYlu6T8w+0dbq3nr8R9sfD5w0PFl5SvNUyWna6YLTk2fyz4ydlZ19fi753GDborZ752PO32oPb++6EHTh0kX/i+c7vDvOXPK4dPKy2+UTV7hXmq86X23qdOo8/pPTT8e7nLuarrlca7nuer21e2b36RueN87d9L158Rb/1tWeOT3dvfN6b/fF9/XfFt1+cif9zsu72Xcn7q28T7xf9EDtQdlD3YfVP1v+3Njv3H9qwHeg89HcR/cGhYPP/pH1jw9DBY+Zj8uGDYbrnjg+OTniP3L96fynQ89kzyaeF/6i/suuFxYvfvjV69fO0ZjRoZfyl5O/bXyl/erA6xmv28bCxh6+yXgzMV70VvvtwXfcdx3vo98PT+R8IH8o/2j5sfVT0Kf7kxmTk/8EA5jz/GMzLdsAAAAgY0hSTQAAeiUAAICDAAD5/wAAgOkAAHUwAADqYAAAOpgAABdvkl/FRgAAAHlJREFUeNrcU1sNgDAQ6wgmcAM2MICGGlg1gJnNzWQcvwQGy1j4oUl/7tH0mpwzM7SgQyO+EZAUWh2MkkzSWhJwuRAlHYsJwEwyvs1gABDuzqoJcTw5qxaIJN0bgQRgIjnlmn1heSO5PE6Y2YXe+5Cr5+h++gs12AcAS6FS+7YOsj4AAAAASUVORK5CYII=);
}
table.treetable tr.expanded span.indenter a {
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAsTAAALEwEAmpwYAAAKT2lDQ1BQaG90b3Nob3AgSUNDIHByb2ZpbGUAAHjanVNnVFPpFj333vRCS4iAlEtvUhUIIFJCi4AUkSYqIQkQSoghodkVUcERRUUEG8igiAOOjoCMFVEsDIoK2AfkIaKOg6OIisr74Xuja9a89+bN/rXXPues852zzwfACAyWSDNRNYAMqUIeEeCDx8TG4eQuQIEKJHAAEAizZCFz/SMBAPh+PDwrIsAHvgABeNMLCADATZvAMByH/w/qQplcAYCEAcB0kThLCIAUAEB6jkKmAEBGAYCdmCZTAKAEAGDLY2LjAFAtAGAnf+bTAICd+Jl7AQBblCEVAaCRACATZYhEAGg7AKzPVopFAFgwABRmS8Q5ANgtADBJV2ZIALC3AMDOEAuyAAgMADBRiIUpAAR7AGDIIyN4AISZABRG8lc88SuuEOcqAAB4mbI8uSQ5RYFbCC1xB1dXLh4ozkkXKxQ2YQJhmkAuwnmZGTKBNA/g88wAAKCRFRHgg/P9eM4Ors7ONo62Dl8t6r8G/yJiYuP+5c+rcEAAAOF0ftH+LC+zGoA7BoBt/qIl7gRoXgugdfeLZrIPQLUAoOnaV/Nw+H48PEWhkLnZ2eXk5NhKxEJbYcpXff5nwl/AV/1s+X48/Pf14L7iJIEyXYFHBPjgwsz0TKUcz5IJhGLc5o9H/LcL//wd0yLESWK5WCoU41EScY5EmozzMqUiiUKSKcUl0v9k4t8s+wM+3zUAsGo+AXuRLahdYwP2SycQWHTA4vcAAPK7b8HUKAgDgGiD4c93/+8//UegJQCAZkmScQAAXkQkLlTKsz/HCAAARKCBKrBBG/TBGCzABhzBBdzBC/xgNoRCJMTCQhBCCmSAHHJgKayCQiiGzbAdKmAv1EAdNMBRaIaTcA4uwlW4Dj1wD/phCJ7BKLyBCQRByAgTYSHaiAFiilgjjggXmYX4IcFIBBKLJCDJiBRRIkuRNUgxUopUIFVIHfI9cgI5h1xGupE7yAAygvyGvEcxlIGyUT3UDLVDuag3GoRGogvQZHQxmo8WoJvQcrQaPYw2oefQq2gP2o8+Q8cwwOgYBzPEbDAuxsNCsTgsCZNjy7EirAyrxhqwVqwDu4n1Y8+xdwQSgUXACTYEd0IgYR5BSFhMWE7YSKggHCQ0EdoJNwkDhFHCJyKTqEu0JroR+cQYYjIxh1hILCPWEo8TLxB7iEPENyQSiUMyJ7mQAkmxpFTSEtJG0m5SI+ksqZs0SBojk8naZGuyBzmULCAryIXkneTD5DPkG+Qh8lsKnWJAcaT4U+IoUspqShnlEOU05QZlmDJBVaOaUt2ooVQRNY9aQq2htlKvUYeoEzR1mjnNgxZJS6WtopXTGmgXaPdpr+h0uhHdlR5Ol9BX0svpR+iX6AP0dwwNhhWDx4hnKBmbGAcYZxl3GK+YTKYZ04sZx1QwNzHrmOeZD5lvVVgqtip8FZHKCpVKlSaVGyovVKmqpqreqgtV81XLVI+pXlN9rkZVM1PjqQnUlqtVqp1Q61MbU2epO6iHqmeob1Q/pH5Z/YkGWcNMw09DpFGgsV/jvMYgC2MZs3gsIWsNq4Z1gTXEJrHN2Xx2KruY/R27iz2qqaE5QzNKM1ezUvOUZj8H45hx+Jx0TgnnKKeX836K3hTvKeIpG6Y0TLkxZVxrqpaXllirSKtRq0frvTau7aedpr1Fu1n7gQ5Bx0onXCdHZ4/OBZ3nU9lT3acKpxZNPTr1ri6qa6UbobtEd79up+6Ynr5egJ5Mb6feeb3n+hx9L/1U/W36p/VHDFgGswwkBtsMzhg8xTVxbzwdL8fb8VFDXcNAQ6VhlWGX4YSRudE8o9VGjUYPjGnGXOMk423GbcajJgYmISZLTepN7ppSTbmmKaY7TDtMx83MzaLN1pk1mz0x1zLnm+eb15vft2BaeFostqi2uGVJsuRaplnutrxuhVo5WaVYVVpds0atna0l1rutu6cRp7lOk06rntZnw7Dxtsm2qbcZsOXYBtuutm22fWFnYhdnt8Wuw+6TvZN9un2N/T0HDYfZDqsdWh1+c7RyFDpWOt6azpzuP33F9JbpL2dYzxDP2DPjthPLKcRpnVOb00dnF2e5c4PziIuJS4LLLpc+Lpsbxt3IveRKdPVxXeF60vWdm7Obwu2o26/uNu5p7ofcn8w0nymeWTNz0MPIQ+BR5dE/C5+VMGvfrH5PQ0+BZ7XnIy9jL5FXrdewt6V3qvdh7xc+9j5yn+M+4zw33jLeWV/MN8C3yLfLT8Nvnl+F30N/I/9k/3r/0QCngCUBZwOJgUGBWwL7+Hp8Ib+OPzrbZfay2e1BjKC5QRVBj4KtguXBrSFoyOyQrSH355jOkc5pDoVQfujW0Adh5mGLw34MJ4WHhVeGP45wiFga0TGXNXfR3ENz30T6RJZE3ptnMU85ry1KNSo+qi5qPNo3ujS6P8YuZlnM1VidWElsSxw5LiquNm5svt/87fOH4p3iC+N7F5gvyF1weaHOwvSFpxapLhIsOpZATIhOOJTwQRAqqBaMJfITdyWOCnnCHcJnIi/RNtGI2ENcKh5O8kgqTXqS7JG8NXkkxTOlLOW5hCepkLxMDUzdmzqeFpp2IG0yPTq9MYOSkZBxQqohTZO2Z+pn5mZ2y6xlhbL+xW6Lty8elQfJa7OQrAVZLQq2QqboVFoo1yoHsmdlV2a/zYnKOZarnivN7cyzytuQN5zvn//tEsIS4ZK2pYZLVy0dWOa9rGo5sjxxedsK4xUFK4ZWBqw8uIq2Km3VT6vtV5eufr0mek1rgV7ByoLBtQFr6wtVCuWFfevc1+1dT1gvWd+1YfqGnRs+FYmKrhTbF5cVf9go3HjlG4dvyr+Z3JS0qavEuWTPZtJm6ebeLZ5bDpaql+aXDm4N2dq0Dd9WtO319kXbL5fNKNu7g7ZDuaO/PLi8ZafJzs07P1SkVPRU+lQ27tLdtWHX+G7R7ht7vPY07NXbW7z3/T7JvttVAVVN1WbVZftJ+7P3P66Jqun4lvttXa1ObXHtxwPSA/0HIw6217nU1R3SPVRSj9Yr60cOxx++/p3vdy0NNg1VjZzG4iNwRHnk6fcJ3/ceDTradox7rOEH0x92HWcdL2pCmvKaRptTmvtbYlu6T8w+0dbq3nr8R9sfD5w0PFl5SvNUyWna6YLTk2fyz4ydlZ19fi753GDborZ752PO32oPb++6EHTh0kX/i+c7vDvOXPK4dPKy2+UTV7hXmq86X23qdOo8/pPTT8e7nLuarrlca7nuer21e2b36RueN87d9L158Rb/1tWeOT3dvfN6b/fF9/XfFt1+cif9zsu72Xcn7q28T7xf9EDtQdlD3YfVP1v+3Njv3H9qwHeg89HcR/cGhYPP/pH1jw9DBY+Zj8uGDYbrnjg+OTniP3L96fynQ89kzyaeF/6i/suuFxYvfvjV69fO0ZjRoZfyl5O/bXyl/erA6xmv28bCxh6+yXgzMV70VvvtwXfcdx3vo98PT+R8IH8o/2j5sfVT0Kf7kxmTk/8EA5jz/GMzLdsAAAAgY0hSTQAAeiUAAICDAAD5/wAAgOkAAHUwAADqYAAAOpgAABdvkl/FRgAAAHFJREFUeNpi/P//PwMlgImBQsA44C6gvhfa29v3MzAwOODRc6CystIRbxi0t7fjDJjKykpGYrwwi1hxnLHQ3t7+jIGBQRJJ6HllZaUUKYEYRYBPOB0gBShKwKGA////48VtbW3/8clTnBIH3gCKkzJgAGvBX0dDm0sCAAAAAElFTkSuQmCC);
}
table.treetable tr.branch {
background-color: #f9f9f9;
}
table.treetable tr.selected {
background-color: #3875d7;
color: #fff;
}
table.treetable tr span.indenter {
margin-left: -19px;
}
table.treetable tr span.indenter a {
outline: none; /* Expander shows outline after upgrading to 3.0 (#141) */
}
table.treetable tr.accept {
background-color: #a3bce4;
color: #fff
}

View file

@ -0,0 +1,629 @@
/*
* jQuery treetable Plugin 3.2.0
* http://ludo.cubicphuse.nl/jquery-treetable
*
* Copyright 2013, Ludo van den Boom
* Dual licensed under the MIT or GPL Version 2 licenses.
*/
(function($) {
var Node, Tree, methods;
Node = (function() {
function Node(row, tree, settings) {
var parentId;
this.row = row;
this.tree = tree;
this.settings = settings;
// TODO Ensure id/parentId is always a string (not int)
this.id = this.row.data(this.settings.nodeIdAttr);
// TODO Move this to a setParentId function?
parentId = this.row.data(this.settings.parentIdAttr);
if (parentId != null && parentId !== "") {
this.parentId = parentId;
}
this.treeCell = $(this.row.children(this.settings.columnElType)[this.settings.column]);
this.expander = $(this.settings.expanderTemplate);
this.indenter = $(this.settings.indenterTemplate);
this.children = [];
this.initialized = false;
this.treeCell.prepend(this.indenter);
}
Node.prototype.addChild = function(child) {
return this.children.push(child);
};
Node.prototype.ancestors = function() {
var ancestors, node;
node = this;
ancestors = [];
while (node = node.parentNode()) {
ancestors.push(node);
}
return ancestors;
};
Node.prototype.collapse = function() {
if (this.collapsed()) {
return this;
}
this.row.removeClass("expanded").addClass("collapsed");
this._hideChildren();
this.expander.attr("title", this.settings.stringExpand);
if (this.initialized && this.settings.onNodeCollapse != null) {
this.settings.onNodeCollapse.apply(this);
}
return this;
};
Node.prototype.collapsed = function() {
return this.row.hasClass("collapsed");
};
// TODO destroy: remove event handlers, expander, indenter, etc.
Node.prototype.expand = function() {
if (this.expanded()) {
return this;
}
this.row.removeClass("collapsed").addClass("expanded");
if (this.initialized && this.settings.onNodeExpand != null) {
this.settings.onNodeExpand.apply(this);
}
if ($(this.row).is(":visible")) {
this._showChildren();
}
this.expander.attr("title", this.settings.stringCollapse);
return this;
};
Node.prototype.expanded = function() {
return this.row.hasClass("expanded");
};
Node.prototype.hide = function() {
this._hideChildren();
this.row.hide();
return this;
};
Node.prototype.isBranchNode = function() {
if(this.children.length > 0 || this.row.data(this.settings.branchAttr) === true) {
return true;
} else {
return false;
}
};
Node.prototype.updateBranchLeafClass = function(){
this.row.removeClass('branch');
this.row.removeClass('leaf');
this.row.addClass(this.isBranchNode() ? 'branch' : 'leaf');
};
Node.prototype.level = function() {
return this.ancestors().length;
};
Node.prototype.parentNode = function() {
if (this.parentId != null) {
return this.tree[this.parentId];
} else {
return null;
}
};
Node.prototype.removeChild = function(child) {
var i = $.inArray(child, this.children);
return this.children.splice(i, 1)
};
Node.prototype.render = function() {
var handler,
settings = this.settings,
target;
if (settings.expandable === true && this.isBranchNode()) {
handler = function(e) {
$(this).parents("table").treetable("node", $(this).parents("tr").data(settings.nodeIdAttr)).toggle();
return e.preventDefault();
};
this.indenter.html(this.expander);
target = settings.clickableNodeNames === true ? this.treeCell : this.expander;
target.off("click.treetable").on("click.treetable", handler);
target.off("keydown.treetable").on("keydown.treetable", function(e) {
if (e.keyCode == 13) {
handler.apply(this, [e]);
}
});
}
this.indenter[0].style.paddingLeft = "" + (this.level() * settings.indent) + "px";
return this;
};
Node.prototype.reveal = function() {
if (this.parentId != null) {
this.parentNode().reveal();
}
return this.expand();
};
Node.prototype.setParent = function(node) {
if (this.parentId != null) {
this.tree[this.parentId].removeChild(this);
}
this.parentId = node.id;
this.row.data(this.settings.parentIdAttr, node.id);
return node.addChild(this);
};
Node.prototype.show = function() {
if (!this.initialized) {
this._initialize();
}
this.row.show();
if (this.expanded()) {
this._showChildren();
}
return this;
};
Node.prototype.toggle = function() {
if (this.expanded()) {
this.collapse();
} else {
this.expand();
}
return this;
};
Node.prototype._hideChildren = function() {
var child, _i, _len, _ref, _results;
_ref = this.children;
_results = [];
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
child = _ref[_i];
_results.push(child.hide());
}
return _results;
};
Node.prototype._initialize = function() {
var settings = this.settings;
this.render();
if (settings.expandable === true && settings.initialState === "collapsed") {
this.collapse();
} else {
this.expand();
}
if (settings.onNodeInitialized != null) {
settings.onNodeInitialized.apply(this);
}
return this.initialized = true;
};
Node.prototype._showChildren = function() {
var child, _i, _len, _ref, _results;
_ref = this.children;
_results = [];
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
child = _ref[_i];
_results.push(child.show());
}
return _results;
};
return Node;
})();
Tree = (function() {
function Tree(table, settings) {
this.table = table;
this.settings = settings;
this.tree = {};
// Cache the nodes and roots in simple arrays for quick access/iteration
this.nodes = [];
this.roots = [];
}
Tree.prototype.collapseAll = function() {
var node, _i, _len, _ref, _results;
_ref = this.nodes;
_results = [];
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
node = _ref[_i];
_results.push(node.collapse());
}
return _results;
};
Tree.prototype.expandAll = function() {
var node, _i, _len, _ref, _results;
_ref = this.nodes;
_results = [];
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
node = _ref[_i];
_results.push(node.expand());
}
return _results;
};
Tree.prototype.findLastNode = function (node) {
if (node.children.length > 0) {
return this.findLastNode(node.children[node.children.length - 1]);
} else {
return node;
}
};
Tree.prototype.loadRows = function(rows) {
var node, row, i;
if (rows != null) {
for (i = 0; i < rows.length; i++) {
row = $(rows[i]);
if (row.data(this.settings.nodeIdAttr) != null) {
node = new Node(row, this.tree, this.settings);
this.nodes.push(node);
this.tree[node.id] = node;
if (node.parentId != null && this.tree[node.parentId]) {
this.tree[node.parentId].addChild(node);
} else {
this.roots.push(node);
}
}
}
}
for (i = 0; i < this.nodes.length; i++) {
node = this.nodes[i].updateBranchLeafClass();
}
return this;
};
Tree.prototype.move = function(node, destination) {
// Conditions:
// 1: +node+ should not be inserted as a child of +node+ itself.
// 2: +destination+ should not be the same as +node+'s current parent (this
// prevents +node+ from being moved to the same location where it already
// is).
// 3: +node+ should not be inserted in a location in a branch if this would
// result in +node+ being an ancestor of itself.
var nodeParent = node.parentNode();
if (node !== destination && destination.id !== node.parentId && $.inArray(node, destination.ancestors()) === -1) {
node.setParent(destination);
this._moveRows(node, destination);
// Re-render parentNode if this is its first child node, and therefore
// doesn't have the expander yet.
if (node.parentNode().children.length === 1) {
node.parentNode().render();
}
}
if(nodeParent){
nodeParent.updateBranchLeafClass();
}
if(node.parentNode()){
node.parentNode().updateBranchLeafClass();
}
node.updateBranchLeafClass();
return this;
};
Tree.prototype.removeNode = function(node) {
// Recursively remove all descendants of +node+
this.unloadBranch(node);
// Remove node from DOM (<tr>)
node.row.remove();
// Remove node from parent children list
if (node.parentId != null) {
node.parentNode().removeChild(node);
}
// Clean up Tree object (so Node objects are GC-ed)
delete this.tree[node.id];
this.nodes.splice($.inArray(node, this.nodes), 1);
return this;
}
Tree.prototype.render = function() {
var root, _i, _len, _ref;
_ref = this.roots;
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
root = _ref[_i];
// Naming is confusing (show/render). I do not call render on node from
// here.
root.show();
}
return this;
};
Tree.prototype.sortBranch = function(node, sortFun) {
// First sort internal array of children
node.children.sort(sortFun);
// Next render rows in correct order on page
this._sortChildRows(node);
return this;
};
Tree.prototype.unloadBranch = function(node) {
// Use a copy of the children array to not have other functions interfere
// with this function if they manipulate the children array
// (eg removeNode).
var children = node.children.slice(0),
i;
for (i = 0; i < children.length; i++) {
this.removeNode(children[i]);
}
// Reset node's collection of children
node.children = [];
node.updateBranchLeafClass();
return this;
};
Tree.prototype._moveRows = function(node, destination) {
var children = node.children, i;
node.row.insertAfter(destination.row);
node.render();
// Loop backwards through children to have them end up on UI in correct
// order (see #112)
for (i = children.length - 1; i >= 0; i--) {
this._moveRows(children[i], node);
}
};
// Special _moveRows case, move children to itself to force sorting
Tree.prototype._sortChildRows = function(parentNode) {
return this._moveRows(parentNode, parentNode);
};
return Tree;
})();
// jQuery Plugin
methods = {
init: function(options, force) {
var settings;
settings = $.extend({
branchAttr: "ttBranch",
clickableNodeNames: false,
column: 0,
columnElType: "td", // i.e. 'td', 'th' or 'td,th'
expandable: false,
expanderTemplate: "<a href='#'>&nbsp;</a>",
indent: 19,
indenterTemplate: "<span class='indenter'></span>",
initialState: "collapsed",
nodeIdAttr: "ttId", // maps to data-tt-id
parentIdAttr: "ttParentId", // maps to data-tt-parent-id
stringExpand: "Expand",
stringCollapse: "Collapse",
// Events
onInitialized: null,
onNodeCollapse: null,
onNodeExpand: null,
onNodeInitialized: null
}, options);
return this.each(function() {
var el = $(this), tree;
if (force || el.data("treetable") === undefined) {
tree = new Tree(this, settings);
tree.loadRows(this.rows).render();
el.addClass("treetable").data("treetable", tree);
if (settings.onInitialized != null) {
settings.onInitialized.apply(tree);
}
}
return el;
});
},
destroy: function() {
return this.each(function() {
return $(this).removeData("treetable").removeClass("treetable");
});
},
collapseAll: function() {
this.data("treetable").collapseAll();
return this;
},
collapseNode: function(id) {
var node = this.data("treetable").tree[id];
if (node) {
node.collapse();
} else {
throw new Error("Unknown node '" + id + "'");
}
return this;
},
expandAll: function() {
this.data("treetable").expandAll();
return this;
},
expandNode: function(id) {
var node = this.data("treetable").tree[id];
if (node) {
if (!node.initialized) {
node._initialize();
}
node.expand();
} else {
throw new Error("Unknown node '" + id + "'");
}
return this;
},
loadBranch: function(node, rows) {
var settings = this.data("treetable").settings,
tree = this.data("treetable").tree;
// TODO Switch to $.parseHTML
rows = $(rows);
if (node == null) { // Inserting new root nodes
this.append(rows);
} else {
var lastNode = this.data("treetable").findLastNode(node);
rows.insertAfter(lastNode.row);
}
this.data("treetable").loadRows(rows);
// Make sure nodes are properly initialized
rows.filter("tr").each(function() {
tree[$(this).data(settings.nodeIdAttr)].show();
});
if (node != null) {
// Re-render parent to ensure expander icon is shown (#79)
node.render().expand();
}
return this;
},
move: function(nodeId, destinationId) {
var destination, node;
node = this.data("treetable").tree[nodeId];
destination = this.data("treetable").tree[destinationId];
this.data("treetable").move(node, destination);
return this;
},
node: function(id) {
return this.data("treetable").tree[id];
},
removeNode: function(id) {
var node = this.data("treetable").tree[id];
if (node) {
this.data("treetable").removeNode(node);
} else {
throw new Error("Unknown node '" + id + "'");
}
return this;
},
reveal: function(id) {
var node = this.data("treetable").tree[id];
if (node) {
node.reveal();
} else {
throw new Error("Unknown node '" + id + "'");
}
return this;
},
sortBranch: function(node, columnOrFunction) {
var settings = this.data("treetable").settings,
prepValue,
sortFun;
columnOrFunction = columnOrFunction || settings.column;
sortFun = columnOrFunction;
if ($.isNumeric(columnOrFunction)) {
sortFun = function(a, b) {
var extractValue, valA, valB;
extractValue = function(node) {
var val = node.row.find("td:eq(" + columnOrFunction + ")").text();
// Ignore trailing/leading whitespace and use uppercase values for
// case insensitive ordering
return $.trim(val).toUpperCase();
}
valA = extractValue(a);
valB = extractValue(b);
if (valA < valB) return -1;
if (valA > valB) return 1;
return 0;
};
}
this.data("treetable").sortBranch(node, sortFun);
return this;
},
unloadBranch: function(node) {
this.data("treetable").unloadBranch(node);
return this;
}
};
$.fn.treetable = function(method) {
if (methods[method]) {
return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
} else if (typeof method === 'object' || !method) {
return methods.init.apply(this, arguments);
} else {
return $.error("Method " + method + " does not exist on jQuery.treetable");
}
};
// Expose classes to world
this.TreeTable || (this.TreeTable = {});
this.TreeTable.Node = Node;
this.TreeTable.Tree = Tree;
})(jQuery);

View file

@ -0,0 +1,58 @@
(function ($) {
'use strict';
Drupal.behaviors.tokenTree = {
attach: function (context, settings) {
$('table.token-tree', context).once('token-tree').each(function () {
$(this).treetable({ expandable: true });
});
}
};
Drupal.behaviors.tokenInsert = {
attach: function (context, settings) {
// Keep track of which textfield was last selected/focused.
$('textarea, input[type="text"]', context).focus(function () {
drupalSettings.tokenFocusedField = this;
});
$('.token-click-insert .token-key', context).once('token-click-insert').each(function () {
var newThis = $('<a href="javascript:void(0);" title="' + Drupal.t('Insert this token into your form') + '">' + $(this).html() + '</a>').click(function () {
if (typeof drupalSettings.tokenFocusedField == 'undefined') {
alert(Drupal.t('First click a text field to insert your tokens into.'));
}
else {
var myField = drupalSettings.tokenFocusedField;
var myValue = $(this).text();
// IE support.
if (document.selection) {
myField.focus();
var sel = document.selection.createRange();
sel.text = myValue;
}
// MOZILLA/NETSCAPE support.
else if (myField.selectionStart || myField.selectionStart === '0') {
var startPos = myField.selectionStart;
var endPos = myField.selectionEnd;
myField.value = myField.value.substring(0, startPos)
+ myValue
+ myField.value.substring(endPos, myField.value.length);
}
else {
myField.value += myValue;
}
$('html,body').animate({scrollTop: $(myField).offset().top}, 500);
}
return false;
});
$(this).html(newThis);
});
}
};
})(jQuery, drupalSettings);

View file

@ -0,0 +1,78 @@
<?php
namespace Drupal\token\Controller;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Controller\ControllerBase;
use Drupal\token\TreeBuilderInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
/**
* Returns autocomplete responses for tokens.
*/
class TokenAutocompleteController extends ControllerBase {
/**
* @var \Drupal\token\TreeBuilderInterface
*/
protected $treeBuilder;
public function __construct(TreeBuilderInterface $tree_builder) {
$this->treeBuilder = $tree_builder;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('token.tree_builder')
);
}
/**
* Retrieves suggestions for block category autocompletion.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The current request.
* @param string $token_type
* The token type.
* @param string $filter
* The autocomplete filter.
*
* @return \Symfony\Component\HttpFoundation\JsonResponse
* A JSON response containing autocomplete suggestions.
*/
public function autocomplete($token_type, $filter, Request $request) {
$filter = substr($filter, strrpos($filter, '['));
$matches = array();
if (!Unicode::strlen($filter)) {
$matches["[{$token_type}:"] = 0;
}
else {
$depth = max(1, substr_count($filter, ':'));
$tree = $this->treeBuilder->buildTree($token_type, ['flat' => TRUE, 'depth' => $depth]);
foreach (array_keys($tree) as $token) {
if (strpos($token, $filter) === 0) {
$matches[$token] = levenshtein($token, $filter);
if (isset($tree[$token]['children'])) {
$token = rtrim($token, ':]') . ':';
$matches[$token] = levenshtein($token, $filter);
}
}
}
}
asort($matches);
$keys = array_keys($matches);
$matches = array_combine($keys, $keys);
return new JsonResponse($matches);
}
}

View file

@ -0,0 +1,21 @@
<?php
namespace Drupal\token\Controller;
use Drupal\Core\Controller\ControllerBase;
/**
* Clears cache for tokens.
*/
class TokenCacheController extends ControllerBase {
/**
* Clear caches and redirect back to the frontpage.
*/
public function flush() {
token_clear_cache();
drupal_set_message(t('Token registry caches cleared.'));
return $this->redirect('<front>');
}
}

View file

@ -0,0 +1,109 @@
<?php
namespace Drupal\token\Controller;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\token\TokenEntityMapperInterface;
use Drupal\token\TreeBuilderInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Devel integration for tokens.
*/
class TokenDevelController extends ControllerBase {
/**
* @var \Drupal\token\TreeBuilderInterface
*/
protected $treeBuilder;
/**
* @var \Drupal\token\TokenEntityMapperInterface
*/
protected $entityMapper;
public function __construct(TreeBuilderInterface $tree_builder, TokenEntityMapperInterface $entity_mapper) {
$this->treeBuilder = $tree_builder;
$this->entityMapper = $entity_mapper;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('token.tree_builder'),
$container->get('token.entity_mapper')
);
}
/**
* Prints the loaded structure of the current entity.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* A RouteMatch object.
*
* @return array
* Array of page elements to render.
*/
public function entityTokens(RouteMatchInterface $route_match) {
$output = [];
$parameter_name = $route_match->getRouteObject()->getOption('_token_entity_type_id');
$entity = $route_match->getParameter($parameter_name);
if ($entity && $entity instanceof EntityInterface) {
$output = $this->renderTokenTree($entity);
}
return $output;
}
/**
* Render the token tree for the specified entity.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity for which the token tree should be rendered.
*
* @return array
* Render array of the token tree for the $entity.
*
* @see static::entityLoad
*/
protected function renderTokenTree(EntityInterface $entity) {
$this->moduleHandler()->loadInclude('token', 'pages.inc');
$entity_type = $entity->getEntityTypeId();
$token_type = $this->entityMapper->getTokenTypeForEntityType($entity_type);
$options = [
'flat' => TRUE,
'values' => TRUE,
'data' => [$token_type => $entity],
];
$token_tree = [
$token_type => [
'tokens' => $this->treeBuilder->buildTree($token_type, $options),
],
];
// foreach ($tree as $token => $token_info) {
// if (!isset($token_info['value']) && !empty($token_info['parent']) && !isset($tree[$token_info['parent']]['value'])) {
// continue;
// }
// }
$build['tokens'] = [
'#type' => 'token_tree_table',
'#show_restricted' => FALSE,
'#show_nested' => FALSE,
'#skip_empty_values' => TRUE,
'#token_tree' => $token_tree,
'#columns' => ['token', 'value'],
'#empty' => $this->t('No tokens available.'),
];
return $build;
}
}

View file

@ -0,0 +1,56 @@
<?php
namespace Drupal\token\Controller;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Controller\ControllerBase;
use Drupal\token\TreeBuilderInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Returns tree responses for tokens.
*/
class TokenTreeController extends ControllerBase {
/**
* @var \Drupal\token\TreeBuilderInterface
*/
protected $treeBuilder;
public function __construct(TreeBuilderInterface $tree_builder) {
$this->treeBuilder = $tree_builder;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('token.tree_builder')
);
}
/**
* Page callback to output a token tree as an empty page.
*/
function outputTree(Request $request) {
$options = $request->query->has('options') ? Json::decode($request->query->get('options')) : [];
// The option token_types may only be an array OR 'all'. If it is not set,
// we assume that only global token types are requested.
$token_types = !empty($options['token_types']) ? $options['token_types'] : [];
if ($token_types == 'all') {
$build = $this->treeBuilder->buildAllRenderable($options);
}
else {
$build = $this->treeBuilder->buildRenderable($token_types, $options);
}
$build['#cache']['contexts'][] = 'url.query_args:options';
$build['#title'] = $this->t('Available tokens');
return $build;
}
}

View file

@ -0,0 +1,155 @@
<?php
namespace Drupal\token\Element;
use Drupal\Component\Utility\Html;
use Drupal\Core\Render\Element\Table;
/**
* Provides a render element for a token tree table.
*
* @RenderElement("token_tree_table")
*/
class TokenTreeTable extends Table {
protected static $cssFilter = [' ' => '-', '_' => '-', '/' => '-', '[' => '-', ']' => '', ':' => '--', '?' => '', '<' => '-', '>' => '-'];
/**
* {@inheritdoc}
*/
public function getInfo() {
$class = get_class($this);
return [
'#header' => [],
'#rows' => [],
'#token_tree' => [],
'#columns' => ['name', 'token', 'description'],
'#empty' => '',
'#show_restricted' => FALSE,
'#show_nested' => FALSE,
'#skip_empty_values' => FALSE,
'#click_insert' => TRUE,
'#sticky' => FALSE,
'#responsive' => TRUE,
'#input' => FALSE,
'#pre_render' => [
[$class, 'preRenderTokenTree'],
[$class, 'preRenderTable'],
],
'#theme' => 'table__token_tree',
'#attached' => [
'library' => [
'token/token',
],
],
];
}
/**
* Pre-render the token tree to transform rows in the token tree.
*
* @param array $element
*
* @return array
* The processed element.
*/
public static function preRenderTokenTree($element) {
$multiple_token_types = count($element['#token_tree']) > 1;
foreach ($element['#token_tree'] as $token_type => $type_info) {
// Do not show nested tokens.
if (!empty($type_info['nested']) && empty($element['#show_nested'])) {
continue;
}
if ($multiple_token_types) {
$row = static::formatRow($token_type, $type_info, $element['#columns'], TRUE);
$element['#rows'][] = $row;
}
foreach ($type_info['tokens'] as $token => $token_info) {
if (!empty($token_info['restricted']) && empty($element['#show_restricted'])) {
continue;
}
if ($element['#skip_empty_values'] && empty($token_info['value']) && !empty($token_info['parent']) && !isset($tree[$token_info['parent']]['value'])) {
continue;
}
if ($multiple_token_types && !isset($token_info['parent'])) {
$token_info['parent'] = $token_type;
}
$row = static::formatRow($token, $token_info, $element['#columns']);
$element['#rows'][] = $row;
}
}
if (!empty($element['#rows'])) {
$element['#attached']['library'][] = 'token/jquery.treeTable';
}
// Fill headers if one is not specified.
if (empty($element['#header'])) {
$column_map = [
'name' => t('Name'),
'token' => t('Token'),
'value' => t('Value'),
'description' => t('Description'),
];
foreach ($element['#columns'] as $col) {
$element['#header'][] = $column_map[$col];
}
}
$element['#attributes']['class'][] = 'token-tree';
if ($element['#click_insert']) {
$element['#caption'] = t('Click a token to insert it into the field you\'ve last clicked.');
$element['#attributes']['class'][] = 'token-click-insert';
}
return $element;
}
protected static function cleanCssIdentifier($id) {
return 'token-' . Html::cleanCssIdentifier(trim($id, '[]'), static::$cssFilter);
}
protected static function formatRow($token, $token_info, $columns, $is_group = FALSE) {
$row = [
'id' => static::cleanCssIdentifier($token),
'data-tt-id' => static::cleanCssIdentifier($token),
'class' => [],
'data' => [],
];
foreach ($columns as $col) {
switch ($col) {
case 'name':
$row['data'][$col] = $token_info['name'];
break;
case 'token':
$row['data'][$col]['data'] = $token;
$row['data'][$col]['class'][] = 'token-key';
break;
case 'description':
$row['data'][$col] = isset($token_info['description']) ? $token_info['description'] : '';
break;
case 'value':
$row['data'][$col] = !$is_group && isset($token_info['value']) ? $token_info['value'] : '';
break;
}
}
if ($is_group) {
// This is a token type/group.
$row['class'][] = 'token-group';
}
elseif (!empty($token_info['parent'])) {
$row['data-tt-parent-id'] = static::cleanCssIdentifier($token_info['parent']);
unset($row['parent']);
}
return $row;
}
}

View file

@ -0,0 +1,56 @@
<?php
namespace Drupal\token\Plugin\Derivative;
use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
class DevelLocalTask extends DeriverBase implements ContainerDeriverInterface {
use StringTranslationTrait;
protected $entityTypeManager;
public function __construct(EntityTypeManagerInterface $entity_type_manager, TranslationInterface $string_translation) {
$this->entityTypeManager = $entity_type_manager;
$this->stringTranslation = $string_translation;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, $base_plugin_id) {
return new static(
$container->get('entity_type.manager'),
$container->get('string_translation')
);
}
/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
$this->derivatives = [];
foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) {
if ($entity_type->hasLinkTemplate('token-devel')) {
$this->derivatives["$entity_type_id.token_devel_tab"] = [
'route_name' => "entity.$entity_type_id.token_devel",
'weight' => 110,
'title' => $this->t('Tokens'),
'parent_id' => "devel.entities:$entity_type_id.devel_tab",
];
}
}
foreach ($this->derivatives as &$entry) {
$entry += $base_plugin_definition;
}
return $this->derivatives;
}
}

View file

@ -0,0 +1,74 @@
<?php
namespace Drupal\token\Routing;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Routing\RouteSubscriberBase;
use Drupal\Core\Routing\RoutingEvents;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
/**
* Subscriber for Devel routes.
*/
class RouteSubscriber extends RouteSubscriberBase {
/**
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
public function __construct(EntityTypeManagerInterface $entity_type_manager, ModuleHandlerInterface $module_handler) {
$this->entityTypeManager = $entity_type_manager;
$this->moduleHandler = $module_handler;
}
/**
* {@inheritdoc}
*/
protected function alterRoutes(RouteCollection $collection) {
foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) {
if ($devel_render = $entity_type->getLinkTemplate('token-devel')) {
$options = [
'_admin_route' => TRUE,
'_token_entity_type_id' => $entity_type_id,
'parameters' => [
$entity_type_id => [
'type' => 'entity:' . $entity_type_id,
],
],
];
$route = new Route(
$devel_render,
[
'_controller' => '\Drupal\token\Controller\TokenDevelController::entityTokens',
'_title' => 'Devel Tokens',
],
[
'_permission' => 'access devel information',
'_module_dependencies' => 'devel',
],
$options
);
$collection->add("entity.$entity_type_id.token_devel", $route);
}
}
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
$events = parent::getSubscribedEvents();
$events[RoutingEvents::ALTER] = array('onAlterRoutes', 100);
return $events;
}
}

View file

@ -0,0 +1,72 @@
<?php
namespace Drupal\token\Tests;
use Drupal\block_content\Entity\BlockContent;
use Drupal\block_content\Entity\BlockContentType;
/**
* Tests block tokens.
*
* @group token
*/
class TokenBlockTest extends TokenTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['block', 'node', 'views', 'block_content'];
/**
* {@inheritdoc}
*/
public function setUp($modules = array()) {
parent::setUp();
$this->admin_user = $this->drupalCreateUser(array('access content', 'administer blocks'));
$this->drupalLogin($this->admin_user);
}
public function testBlockTitleTokens() {
$label = 'tokenblock';
$bundle = BlockContentType::create(array(
'id' => $label,
'label' => $label,
'revision' => FALSE
));
$bundle->save();
$block_content = BlockContent::create(array(
'type' => $label,
'label' => '[current-page:title] block title',
'info' => 'Test token title block',
'body[value]' => 'This is the test token title block.',
));
$block_content->save();
$block = $this->drupalPlaceBlock('block_content:' . $block_content->uuid(), array(
'label' => '[user:name]',
));
$this->drupalGet($block->urlInfo());
// Ensure that the link to available tokens is present and correctly
// positioned.
$this->assertLink('Browse available tokens.');
$this->assertText('This field supports tokens. Browse available tokens.');
$this->drupalPostForm(NULL, array(), t('Save block'));
// Ensure token validation is working on the block.
$this->assertText('Title is using the following invalid tokens: [user:name].');
// Create the block for real now with a valid title.
$settings = $block->get('settings');
$settings['label'] = '[current-page:title] block title';
$block->set('settings', $settings);
$block->save();
// Ensure that tokens are not double-escaped when output as a block title.
$this->drupalCreateContentType(array('type' => 'page'));
$node = $this->drupalCreateNode(array('title' => "Site's first node"));
$this->drupalGet('node/' . $node->id());
// The apostraphe should only be escaped once.
$this->assertRaw("Site&#039;s first node block title");
}
}

View file

@ -0,0 +1,66 @@
<?php
namespace Drupal\token\Tests;
use Drupal\Core\Url;
/**
* Test the [current-page:*] tokens.
*
* @group token
*/
class TokenCurrentPageTest extends TokenTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('node');
function testCurrentPageTokens() {
$tokens = array(
'[current-page:title]' => t('Log in'),
'[current-page:url]' => Url::fromRoute('user.login', [], array('absolute' => TRUE))->toString(),
'[current-page:url:absolute]' => Url::fromRoute('user.login', [], array('absolute' => TRUE))->toString(),
'[current-page:url:relative]' => Url::fromRoute('user.login')->toString(),
'[current-page:url:path]' => '/user/login',
'[current-page:url:args:value:0]' => 'user',
'[current-page:url:args:value:1]' => 'login',
'[current-page:url:args:value:2]' => NULL,
'[current-page:url:unaliased]' => Url::fromRoute('user.login', [], array('absolute' => TRUE, 'alias' => TRUE))->toString(),
'[current-page:page-number]' => 1,
'[current-page:query:foo]' => NULL,
'[current-page:query:bar]' => NULL,
// Deprecated tokens
'[current-page:arg:0]' => 'user',
'[current-page:arg:1]' => 'login',
'[current-page:arg:2]' => NULL,
);
$this->assertPageTokens('user/login', $tokens);
$this->drupalCreateContentType(array('type' => 'page'));
$node = $this->drupalCreateNode(array('title' => 'Node title', 'path' => array('alias' => '/node-alias')));
$tokens = array(
'[current-page:title]' => 'Node title',
'[current-page:url]' => $node->url('canonical', array('absolute' => TRUE)),
'[current-page:url:absolute]' => $node->url('canonical', array('absolute' => TRUE)),
'[current-page:url:relative]' => $node->url(),
'[current-page:url:alias]' => '/node-alias',
'[current-page:url:args:value:0]' => 'node-alias',
'[current-page:url:args:value:1]' => NULL,
'[current-page:url:unaliased]' => $node->url('canonical', array('absolute' => TRUE, 'alias' => TRUE)),
'[current-page:url:unaliased:args:value:0]' => 'node',
'[current-page:url:unaliased:args:value:1]' => $node->id(),
'[current-page:url:unaliased:args:value:2]' => NULL,
'[current-page:page-number]' => 1,
'[current-page:query:foo]' => 'bar',
'[current-page:query:bar]' => NULL,
// Deprecated tokens
'[current-page:arg:0]' => 'node',
'[current-page:arg:1]' => 1,
'[current-page:arg:2]' => NULL,
);
$this->assertPageTokens("/node/{$node->id()}", $tokens, array(), array('url_options' => array('query' => array('foo' => 'bar'))));
}
}

View file

@ -0,0 +1,280 @@
<?php
namespace Drupal\token\Tests;
use Drupal\node\Entity\NodeType;
use Drupal\node\Entity\Node;
use Drupal\file\Entity\File;
use Drupal\image\Entity\ImageStyle;
/**
* Tests field ui.
*
* @group token
*/
class TokenFieldUiTest extends TokenTestBase {
/**
* @var \Drupal\Core\Session\AccountInterface
*/
protected $adminUser;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['field_ui', 'node', 'image'];
/**
* {@inheritdoc}
*/
public function setUp($modules = []) {
parent::setUp();
$this->adminUser = $this->drupalCreateUser(['administer content types', 'administer node fields']);
$this->drupalLogin($this->adminUser);
$node_type = NodeType::create([
'type' => 'article',
'name' => 'Article',
'description' => "Use <em>articles</em> for time-sensitive content like news, press releases or blog posts.",
]);
$node_type->save();
entity_create('field_storage_config', array(
'field_name' => 'field_body',
'entity_type' => 'node',
'type' => 'text_with_summary',
))->save();
entity_create('field_config', array(
'field_name' => 'field_body',
'label' => 'Body',
'entity_type' => 'node',
'bundle' => 'article',
))->save();
entity_create('field_storage_config', array(
'field_name' => 'field_image',
'entity_type' => 'node',
'type' => 'image',
))->save();
entity_create('field_config', array(
'field_name' => 'field_image',
'label' => 'Image',
'entity_type' => 'node',
'bundle' => 'article',
))->save();
entity_create('field_storage_config', array(
'field_name' => 'field_image_2',
'entity_type' => 'node',
'type' => 'image',
))->save();
entity_create('field_config', array(
'field_name' => 'field_image_2',
'label' => 'Image 2',
'entity_type' => 'node',
'bundle' => 'article',
))->save();
entity_create('field_storage_config', array(
'field_name' => 'multivalued_field_image',
'entity_type' => 'node',
'type' => 'image',
))->save();
entity_create('field_config', array(
'field_name' => 'multivalued_field_image',
'label' => 'Multivalued field image',
'entity_type' => 'node',
'bundle' => 'article',
))->save();
entity_get_form_display('node', 'article', 'default')
->setComponent('field_body', [
'type' => 'text_textarea_with_summary',
'settings' => [
'rows' => '9',
'summary_rows' => '3',
],
'weight' => 5,
])
->save();
}
public function testFileFieldUi() {
$this->drupalGet('admin/structure/types/manage/article/fields/node.article.field_image');
// Ensure the 'Browse available tokens' link is present and correct.
$this->assertLink('Browse available tokens.');
$this->assertLinkByHref('token/tree');
// Ensure that the default file directory value validates correctly.
$this->drupalPostForm(NULL, [], t('Save settings'));
$this->assertText(t('Saved Image configuration.'));
}
public function testFieldDescriptionTokens() {
$edit = [
'description' => 'The site is called [site:name].',
];
$this->drupalPostForm('admin/structure/types/manage/article/fields/node.article.field_body', $edit, 'Save settings');
$this->drupalGet('node/add/article');
$this->assertText('The site is called Drupal.');
}
/**
* Test that tokens are correctly provided and replaced for the image fields.
*/
public function testImageFieldTokens() {
// Generate 2 different test images.
file_unmanaged_copy(\Drupal::root() . '/core/misc/druplicon.png', 'public://example1.png');
file_unmanaged_copy(\Drupal::root() . '/core/misc/loading.gif', 'public://example2.gif');
// Resize the test images so that they will be scaled down during token
// replacement.
$image1 = \Drupal::service('image.factory')->get('public://example1.png');
$image1->resize(500, 500);
$image1->save();
$image2 = \Drupal::service('image.factory')->get('public://example2.gif');
$image2->resize(500, 500);
$image2->save();
/** @var \Drupal\file\Entity\File $image1 */
$image1 = File::create(['uri' => 'public://example1.png']);
$image1->save();
/** @var \Drupal\file\Entity\File $image2 */
$image2 = File::create(['uri' => 'public://example2.gif']);
$image2->save();
$node = Node::create([
'title' => 'Test node title',
'type' => 'article',
'field_image' => [
[
'target_id' => $image1->id(),
],
],
'field_image_2' => [
[
'target_id' => $image2->id(),
],
],
'multivalued_field_image' => [
['target_id' => $image1->id()],
['target_id' => $image2->id()],
],
]);
$node->save();
// Obtain the file size and dimension of the images that will be scaled
// down during token replacement by applying the styles here.
$style_thumbnail = ImageStyle::load('thumbnail');
$style_thumbnail->createDerivative('public://example1.png', 'public://styles/thumbnail/public/example1-test.png');
$style_thumbnail->createDerivative('public://example2.gif', 'public://styles/thumbnail/public/example2-test.gif');
$image_1_thumbnail = \Drupal::service('image.factory')->get('public://styles/thumbnail/public/example1-test.png');
$image_2_thumbnail = \Drupal::service('image.factory')->get('public://styles/thumbnail/public/example2-test.gif');
$style_medium = ImageStyle::load('medium');
$style_medium->createDerivative('public://example1.png', 'public://styles/medium/public/example1-test.png');
$style_medium->createDerivative('public://example2.gif', 'public://styles/medium/public/example2-test.gif');
$image_1_medium = \Drupal::service('image.factory')->get('public://styles/medium/public/example1-test.png');
$image_2_medium = \Drupal::service('image.factory')->get('public://styles/medium/public/example2-test.gif');
$style_large = ImageStyle::load('large');
$style_large->createDerivative('public://example1.png', 'public://styles/large/public/example1-test.png');
$style_large->createDerivative('public://example2.gif', 'public://styles/large/public/example2-test.gif');
$image_1_large = \Drupal::service('image.factory')->get('public://styles/large/public/example1-test.png');
$image_2_large = \Drupal::service('image.factory')->get('public://styles/large/public/example2-test.gif');
// Delete the image derivatives, to make sure they are re-created.
unlink('public://styles/thumbnail/public/example1-test.png');
unlink('public://styles/medium/public/example1-test.png');
unlink('public://styles/large/public/example1-test.png');
unlink('public://styles/thumbnail/public/example2-test.gif');
unlink('public://styles/medium/public/example2-test.gif');
unlink('public://styles/large/public/example2-test.gif');
$tokens = [
// field_image
'field_image:thumbnail:mimetype' => 'image/png',
'field_image:medium:mimetype' => 'image/png',
'field_image:large:mimetype' => 'image/png',
'field_image:thumbnail:filesize' => $image_1_thumbnail->getFileSize(),
'field_image:medium:filesize' => $image_1_medium->getFileSize(),
'field_image:large:filesize' => $image_1_large->getFileSize(),
'field_image:thumbnail:height' => '100',
'field_image:medium:height' => '220',
'field_image:large:height' => '480',
'field_image:thumbnail:width' => '100',
'field_image:medium:width' => '220',
'field_image:large:width' => '480',
'field_image:thumbnail:uri' => 'public://styles/thumbnail/public/example1.png',
'field_image:medium:uri' => 'public://styles/medium/public/example1.png',
'field_image:large:uri' => 'public://styles/large/public/example1.png',
'field_image:thumbnail:url' => $style_thumbnail->buildUrl('public://example1.png'),
'field_image:medium:url' => $style_medium->buildUrl('public://example1.png'),
'field_image:large:url' => $style_large->buildUrl('public://example1.png'),
'field_image:thumbnail' => $style_thumbnail->buildUrl('public://example1.png'),
'field_image:medium' => $style_medium->buildUrl('public://example1.png'),
'field_image:large' => $style_large->buildUrl('public://example1.png'),
// field_image_2
'field_image_2:thumbnail:mimetype' => 'image/gif',
'field_image_2:medium:mimetype' => 'image/gif',
'field_image_2:large:mimetype' => 'image/gif',
'field_image_2:thumbnail:filesize' => $image_2_thumbnail->getFileSize(),
'field_image_2:medium:filesize' => $image_2_medium->getFileSize(),
'field_image_2:large:filesize' => $image_2_large->getFileSize(),
'field_image_2:thumbnail:height' => '100',
'field_image_2:medium:height' => '220',
'field_image_2:large:height' => '480',
'field_image_2:thumbnail:width' => '100',
'field_image_2:medium:width' => '220',
'field_image_2:large:width' => '480',
'field_image_2:thumbnail:uri' => 'public://styles/thumbnail/public/example2.gif',
'field_image_2:medium:uri' => 'public://styles/medium/public/example2.gif',
'field_image_2:large:uri' => 'public://styles/large/public/example2.gif',
'field_image_2:thumbnail:url' => $style_thumbnail->buildUrl('public://example2.gif'),
'field_image_2:medium:url' => $style_medium->buildUrl('public://example2.gif'),
'field_image_2:large:url' => $style_large->buildUrl('public://example2.gif'),
'field_image_2:thumbnail' => $style_thumbnail->buildUrl('public://example2.gif'),
'field_image_2:medium' => $style_medium->buildUrl('public://example2.gif'),
'field_image_2:large' => $style_large->buildUrl('public://example2.gif'),
// multivalued_field_image:0, test for thumbnail image style only.
'multivalued_field_image:0:thumbnail:mimetype' => 'image/png',
'multivalued_field_image:0:thumbnail:filesize' => $image_1_thumbnail->getFileSize(),
'multivalued_field_image:0:thumbnail:height' => '100',
'multivalued_field_image:0:thumbnail:width' => '100',
'multivalued_field_image:0:thumbnail:uri' => 'public://styles/thumbnail/public/example1.png',
'multivalued_field_image:0:thumbnail:url' => $style_thumbnail->buildUrl('public://example1.png'),
'multivalued_field_image:0:thumbnail' => $style_thumbnail->buildUrl('public://example1.png'),
// multivalued_field_image:1, test for medium image style only.
'multivalued_field_image:1:medium:mimetype' => 'image/gif',
'multivalued_field_image:1:medium:filesize' => $image_2_medium->getFileSize(),
'multivalued_field_image:1:medium:height' => '220',
'multivalued_field_image:1:medium:width' => '220',
'multivalued_field_image:1:medium:uri' => 'public://styles/medium/public/example2.gif',
'multivalued_field_image:1:medium:url' => $style_medium->buildUrl('public://example2.gif'),
'multivalued_field_image:1:medium' => $style_medium->buildUrl('public://example2.gif'),
];
$this->assertTokens('node', ['node' => $node], $tokens);
/** @var \Drupal\token\Token $token_service */
$token_service = \Drupal::service('token');
// Test one of the image style's token info for cardinality 1 image field.
$token_info = $token_service->getTokenInfo('node-field_image', 'thumbnail');
$this->assertEqual('Thumbnail (100×100)', $token_info['name']);
$this->assertEqual('Represents the image in the given image style.', $token_info['description']);
// Test one of the image style's token info for a multivalued image field.
$token_info = $token_service->getTokenInfo('node-multivalued_field_image', 'medium');
$this->assertEqual('Medium (220×220)', $token_info['name']);
$this->assertEqual('Represents the image in the given image style.', $token_info['description']);
// Test few of the image styles' properties token info.
$token_info = $token_service->getTokenInfo('image_with_image_style', 'mimetype');
$this->assertEqual('MIME type', $token_info['name']);
$this->assertEqual('The MIME type (image/png, image/bmp, etc.) of the image.', $token_info['description']);
$token_info = $token_service->getTokenInfo('image_with_image_style', 'filesize');
$this->assertEqual('File size', $token_info['name']);
$this->assertEqual('The file size of the image.', $token_info['description']);
}
}

View file

@ -0,0 +1,459 @@
<?php
namespace Drupal\token\Tests;
use Drupal\node\Entity\Node;
use Drupal\Core\Url;
use Drupal\node\Entity\NodeType;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\Core\Language\LanguageInterface;
use Drupal\system\Entity\Menu;
use Drupal\menu_link_content\Entity\MenuLinkContent;
/**
* Tests menu tokens.
*
* @group token
*/
class TokenMenuTest extends TokenTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = [
'menu_ui',
'node',
'block',
'language',
'block_content',
'content_translation',
];
function testMenuTokens() {
// Make sure we have a body field on the node type.
$this->drupalCreateContentType(['type' => 'page']);
// Add a menu.
$menu = entity_create('menu', array(
'id' => 'main-menu',
'label' => 'Main menu',
'description' => 'The <em>Main</em> menu is used on many sites to show the major sections of the site, often in a top navigation bar.',
));
$menu->save();
// Place the menu block.
$this->drupalPlaceBlock('system_menu_block:main-menu');
// Add a root link.
/** @var \Drupal\menu_link_content\Plugin\Menu\MenuLinkContent $root_link */
$root_link = entity_create('menu_link_content', array(
'link' => ['uri' => 'internal:/admin'],
'title' => 'Administration',
'menu_name' => 'main-menu',
));
$root_link->save();
// Add another link with the root link as the parent.
/** @var \Drupal\menu_link_content\Plugin\Menu\MenuLinkContent $parent_link */
$parent_link = entity_create('menu_link_content', array(
'link' => ['uri' => 'internal:/admin/config'],
'title' => 'Configuration',
'menu_name' => 'main-menu',
'parent' => $root_link->getPluginId(),
));
$parent_link->save();
// Test menu link tokens.
$tokens = array(
'id' => $parent_link->getPluginId(),
'title' => 'Configuration',
'menu' => 'Main menu',
'menu:name' => 'Main menu',
'menu:machine-name' => $menu->id(),
'menu:description' => 'The <em>Main</em> menu is used on many sites to show the major sections of the site, often in a top navigation bar.',
'menu:menu-link-count' => '2',
'menu:edit-url' => Url::fromRoute('entity.menu.edit_form', ['menu' => 'main-menu'], array('absolute' => TRUE))->toString(),
'url' => Url::fromRoute('system.admin_config', [], array('absolute' => TRUE))->toString(),
'url:absolute' => Url::fromRoute('system.admin_config', [], array('absolute' => TRUE))->toString(),
'url:relative' => Url::fromRoute('system.admin_config', [], array('absolute' => FALSE))->toString(),
'url:path' => '/admin/config',
'url:alias' => '/admin/config',
'edit-url' => Url::fromRoute('entity.menu_link_content.canonical', ['menu_link_content' => $parent_link->id()], array('absolute' => TRUE))->toString(),
'parent' => 'Administration',
'parent:id' => $root_link->getPluginId(),
'parent:title' => 'Administration',
'parent:menu' => 'Main menu',
'parent:parent' => NULL,
'parents' => 'Administration',
'parents:count' => 1,
'parents:keys' => $root_link->getPluginId(),
'root' => 'Administration',
'root:id' => $root_link->getPluginId(),
'root:parent' => NULL,
'root:root' => NULL,
);
$this->assertTokens('menu-link', array('menu-link' => $parent_link), $tokens);
// Add a node.
$node = $this->drupalCreateNode();
// Allow main menu for this node type.
//$this->config('menu.entity.node.' . $node->getType())->set('available_menus', array('main-menu'))->save();
// Add a node menu link.
/** @var \Drupal\menu_link_content\Plugin\Menu\MenuLinkContent $node_link */
$node_link = entity_create('menu_link_content', array(
'link' => ['uri' =>'entity:node/' . $node->id()],
'title' => 'Node link',
'parent' => $parent_link->getPluginId(),
'menu_name' => 'main-menu',
));
$node_link->save();
// Test [node:menu] tokens.
$tokens = array(
'menu-link' => 'Node link',
'menu-link:id' => $node_link->getPluginId(),
'menu-link:title' => 'Node link',
'menu-link:menu' => 'Main menu',
'menu-link:url' => $node->url('canonical', ['absolute' => TRUE]),
'menu-link:url:path' => '/node/' . $node->id(),
'menu-link:edit-url' => $node_link->url('edit-form', ['absolute' => TRUE]),
'menu-link:parent' => 'Configuration',
'menu-link:parent:id' => $parent_link->getPluginId(),
'menu-link:parents' => 'Administration, Configuration',
'menu-link:parents:count' => 2,
'menu-link:parents:keys' => $root_link->getPluginId() . ', ' . $parent_link->getPluginId(),
'menu-link:root' => 'Administration',
'menu-link:root:id' => $root_link->getPluginId(),
);
$this->assertTokens('node', array('node' => $node), $tokens);
// Reload the node which will not have $node->menu defined and re-test.
$loaded_node = Node::load($node->id());
$this->assertTokens('node', array('node' => $loaded_node), $tokens);
// Regression test for http://drupal.org/node/1317926 to ensure the
// original node object is not changed when calling menu_node_prepare().
$this->assertTrue(!isset($loaded_node->menu), t('The $node->menu property was not modified during token replacement.'), 'Regression');
// Now add a node with a menu-link from the UI and ensure it works.
$this->drupalLogin($this->drupalCreateUser([
'create page content',
'edit any page content',
'administer menu',
'administer nodes',
'administer content types',
'access administration pages',
]));
// Setup node type menu options.
$edit = array(
'menu_options[main-menu]' => 1,
'menu_options[main]' => 1,
'menu_parent' => 'main-menu:',
);
$this->drupalPostForm('admin/structure/types/manage/page', $edit, t('Save content type'));
// Use a menu-link token in the body.
$this->drupalGet('node/add/page');
$this->drupalPostForm(NULL, [
// This should get replaced on save.
// @see token_module_test_node_presave()
'title[0][value]' => 'Node menu title test',
'body[0][value]' => 'This is a [node:menu-link:title] token to the menu link title',
'menu[enabled]' => 1,
'menu[title]' => 'Test preview',
], t('Save and publish'));
$node = $this->drupalGetNodeByTitle('Node menu title test');
$this->assertEqual('This is a Test preview token to the menu link title', $node->body->value);
// Disable the menu link, save the node and verify that the menu link is
// no longer displayed.
$link = menu_ui_get_menu_link_defaults($node);
$this->drupalPostForm('admin/structure/menu/manage/main-menu', ['links[menu_plugin_id:' . $link['id'] . '][enabled]' => FALSE], t('Save'));
$this->assertText('Menu Main menu has been updated.');
$this->drupalPostForm('node/' . $node->id() . '/edit', [], t('Save and keep published'));
$this->assertNoLink('Test preview');
// Now test a parent link and token.
$this->drupalGet('node/add/page');
// Make sure that the previous node save didn't result in two menu-links
// being created by the computed menu-link ER field.
// @see token_entity_base_field_info()
// @see token_node_menu_link_submit()
$selects = $this->cssSelect('select[name="menu[menu_parent]"]');
$select = reset($selects);
$options = $this->getAllOptions($select);
// Filter to items with title containing 'Test preview'.
$options = array_filter($options, function(\SimpleXMLElement $item) {
return strpos((string) $item[0], 'Test preview') !== FALSE;
});
$this->assertEqual(1, count($options));
$this->drupalPostForm(NULL, [
'title[0][value]' => 'Node menu title parent path test',
'body[0][value]' => 'This is a [node:menu-link:parent:url:path] token to the menu link parent',
'menu[enabled]' => 1,
'menu[title]' => 'Child link',
'menu[menu_parent]' => 'main-menu:' . $parent_link->getPluginId(),
], t('Save and publish'));
$node = $this->drupalGetNodeByTitle('Node menu title parent path test');
$this->assertEqual('This is a /admin/config token to the menu link parent', $node->body->value);
// Now edit the node and update the parent and title.
$this->drupalPostForm('node/' . $node->id() . '/edit', [
'menu[menu_parent]' => 'main-menu:' . $node_link->getPluginId(),
'title[0][value]' => 'Node menu title edit parent path test',
'body[0][value]' => 'This is a [node:menu-link:parent:url:path] token to the menu link parent',
], t('Save and keep published'));
$node = $this->drupalGetNodeByTitle('Node menu title edit parent path test', TRUE);
$this->assertEqual(sprintf('This is a /node/%d token to the menu link parent', $loaded_node->id()), $node->body->value);
// Make sure that the previous node edit didn't result in two menu-links
// being created by the computed menu-link ER field.
// @see token_entity_base_field_info()
// @see token_node_menu_link_submit()
$this->drupalGet('node/add/page');
$selects = $this->cssSelect('select[name="menu[menu_parent]"]');
$select = reset($selects);
$options = $this->getAllOptions($select);
// Filter to items with title containing 'Test preview'.
$options = array_filter($options, function(\SimpleXMLElement $item) {
return strpos((string) $item[0], 'Child link') !== FALSE;
});
$this->assertEqual(1, count($options));
// Now add a new node with no menu.
$this->drupalGet('node/add/page');
$this->drupalPostForm(NULL, [
'title[0][value]' => 'Node menu adding menu later test',
'body[0][value]' => 'Going to add a menu link on edit',
'menu[enabled]' => 0,
], t('Save and publish'));
$node = $this->drupalGetNodeByTitle('Node menu adding menu later test');
// Now edit it and add a menu item.
$this->drupalGet('node/' . $node->id() . '/edit');
$this->drupalPostForm(NULL, [
'title[0][value]' => 'Node menu adding menu later test',
'body[0][value]' => 'This is a [node:menu-link:parent:url:path] token to the menu link parent',
'menu[enabled]' => 1,
'menu[title]' => 'Child link',
'menu[menu_parent]' => 'main-menu:' . $parent_link->getPluginId(),
], t('Save and keep published'));
$node = $this->drupalGetNodeByTitle('Node menu adding menu later test', TRUE);
$this->assertEqual('This is a /admin/config token to the menu link parent', $node->body->value);
// And make sure the menu link exists with the right URI.
$link = menu_ui_get_menu_link_defaults($node);
$this->assertTrue(!empty($link['entity_id']));
$query = \Drupal::entityQuery('menu_link_content')
->condition('link.uri', 'entity:node/' . $node->id())
->sort('id', 'ASC')
->range(0, 1);
$result = $query->execute();
$this->assertTrue($result);
// Create a node with a menu link and create 2 menu links linking to this
// node after. Verify that the menu link provided by the node has priority.
$node_title = $this->randomMachineName();
$edit = [
'title[0][value]' => $node_title,
'menu[enabled]' => 1,
'menu[title]' => 'menu link provided by node',
];
$this->drupalPostForm('node/add/page', $edit, t('Save and publish'));
$this->assertText('page ' . $node_title . ' has been created');
$node = $this->drupalGetNodeByTitle($node_title);
$menu_ui_link1 = entity_create('menu_link_content', [
'link' => ['uri' => 'entity:node/' . $node->id()],
'title' => 'menu link 1 provided by menu ui',
'menu_name' => 'main-menu',
]);
$menu_ui_link1->save();
$menu_ui_link2 = entity_create('menu_link_content', [
'link' => ['uri' => 'entity:node/' . $node->id()],
'title' => 'menu link 2 provided by menu ui',
'menu_name' => 'main-menu',
]);
$menu_ui_link2->save();
$tokens = [
'menu-link' => 'menu link provided by node',
'menu-link:title' => 'menu link provided by node',
];
$this->assertTokens('node', ['node' => $node], $tokens);
}
/**
* Tests that the module doesn't affect integrity of the menu, when
* translating them and that menu links tokens are correct.
*/
function testMultilingualMenu() {
// Place the menu block.
$this->drupalPlaceBlock('system_menu_block:main');
// Add a second language.
$language = ConfigurableLanguage::create([
'id' => 'de',
'label' => 'German',
]);
$language->save();
// Create the article content type.
$node_type = NodeType::create([
'type' => 'article',
]);
$node_type->save();
$permissions = array(
'access administration pages',
'administer content translation',
'administer content types',
'administer languages',
'create content translations',
'create article content',
'edit any article content',
'translate any entity',
'administer menu',
);
$this->drupalLogin($this->drupalCreateUser($permissions));
// Enable translation for articles and menu links.
$this->drupalGet('admin/config/regional/content-language');
$edit = array(
'entity_types[node]' => TRUE,
'entity_types[menu_link_content]' => TRUE,
'settings[node][article][translatable]' => TRUE,
'settings[node][article][fields][title]' => TRUE,
'settings[menu_link_content][menu_link_content][translatable]' => TRUE,
);
$this->drupalPostForm(NULL, $edit, t('Save configuration'));
$this->assertText('Settings successfully updated.');
// Create an english node with an english menu.
$this->drupalGet('/node/add/article');
$edit = [
'title[0][value]' => 'English test node with menu',
'menu[enabled]' => TRUE,
'menu[title]' => 'English menu title',
];
$this->drupalPostForm('/node/add/article', $edit, t('Save'));
$this->assertText('English test node with menu has been created.');
// Add a german translation.
$this->drupalGet('node/1/translations');
$this->clickLink('Add');
$edit = [
'title[0][value]' => 'German test node with menu',
'menu[enabled]' => TRUE,
'menu[title]' => 'German menu title',
];
$this->drupalPostForm(NULL, $edit, t('Save (this translation)'));
$this->assertText('German test node with menu has been updated.');
// Verify that the menu links are correct.
$this->drupalGet('node/1');
$this->assertLink('English menu title');
$this->drupalGet('de/node/1');
$this->assertLink('German menu title');
// Verify that tokens are correct.
$node = Node::load(1);
$this->assertTokens('node', ['node' => $node], ['menu-link' => 'English menu title']);
$this->assertTokens('node', ['node' => $node], [
'menu-link' => 'German menu title',
'menu-link:title' => 'German menu title',
], ['langcode' => 'de']);
// Get the menu link and create a child menu link to assert parent and root
// tokens.
$url = $node->toUrl();
/** @var \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager */
$menu_link_manager = \Drupal::service('plugin.manager.menu.link');
$links = $menu_link_manager->loadLinksByRoute($url->getRouteName(), $url->getRouteParameters());
$link = reset($links);
$base_options = [
'provider' => 'menu_test',
'menu_name' => 'menu_test',
];
$child_1 = $base_options + [
'title' => 'child_1 title EN',
'link' => ['uri' => 'internal:/menu-test/hierarchy/parent/child_1'],
'parent' => $link->getPluginId(),
'langcode' => 'en',
];
$child_1 = MenuLinkContent::create($child_1);
$child_1->save();
// Add the german translation.
$child_1->addTranslation('de', ['title' => 'child_1 title DE'] + $child_1->toArray());
$child_1->save();
$this->assertTokens('menu-link', ['menu-link' => $child_1], [
'title' => 'child_1 title EN',
'parents' => 'English menu title',
'root' => 'English menu title',
]);
$this->assertTokens('menu-link', ['menu-link' => $child_1], [
'title' => 'child_1 title DE',
'parents' => 'German menu title',
'root' => 'German menu title',
], ['langcode' => 'de']);
}
/**
* Tests menu link parents token.
*/
public function testMenuLinkParentsToken() {
// Create a menu with a simple link hierarchy :
// - parent
// - child-1
// - child-1-1
Menu::create(array(
'id' => 'menu_test',
'label' => 'Test menu',
))->save();
$base_options = [
'provider' => 'menu_test',
'menu_name' => 'menu_test',
];
$parent = $base_options + [
'title' => 'parent title',
'link' => ['uri' => 'internal:/menu-test/hierarchy/parent'],
];
$parent = MenuLinkContent::create($parent);
$parent->save();
$child_1 = $base_options + [
'title' => 'child_1 title',
'link' => ['uri' => 'internal:/menu-test/hierarchy/parent/child_1'],
'parent' => $parent->getPluginId(),
];
$child_1 = MenuLinkContent::create($child_1);
$child_1->save();
$child_1_1 = $base_options + [
'title' => 'child_1_1 title',
'link' => ['uri' => 'internal:/menu-test/hierarchy/parent/child_1/child_1_1'],
'parent' => $child_1->getPluginId(),
];
$child_1_1 = MenuLinkContent::create($child_1_1);
$child_1_1->save();
$this->assertTokens('menu-link', ['menu-link' => $child_1_1], ['parents' => 'parent title, child_1 title']);
// Change the parent of child_1_1 to 'parent' at the entity level.
$child_1_1->parent->value = $parent->getPluginId();
$child_1_1->save();
$this->assertTokens('menu-link', ['menu-link' => $child_1_1], ['parents' => 'parent title']);
// Change the parent of child_1_1 to 'main', at the entity level.
$child_1_1->parent->value = '';
$child_1_1->save();
// The token shouldn't have been generated; the menu link has no parent.
$this->assertNoTokens('menu-link', ['menu-link' => $child_1_1], ['parents']);
}
}

View file

@ -0,0 +1,21 @@
<?php
namespace Drupal\token\Tests;
use Drupal\simpletest\WebTestBase;
/**
* Helper test class with some added functions for testing.
*/
abstract class TokenTestBase extends WebTestBase {
use TokenTestTrait;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('path', 'token', 'token_module_test');
}

View file

@ -0,0 +1,111 @@
<?php
namespace Drupal\token\Tests;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Language\Language;
use Drupal\Core\Render\BubbleableMetadata;
/**
* Helper test trait with some added functions for testing.
*/
trait TokenTestTrait {
function assertToken($type, array $data, $token, $expected, array $options = array()) {
return $this->assertTokens($type, $data, array($token => $expected), $options);
}
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($replacements[$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->assertEqual($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;
}
function mapTokenNames($type, array $tokens = array()) {
$return = array();
foreach ($tokens as $token) {
$return[$token] = "[$type:$token]";
}
return $return;
}
function assertNoTokens($type, array $data, array $tokens, array $options = array()) {
$input = $this->mapTokenNames($type, $tokens);
$bubbleable_metadata = new BubbleableMetadata();
$replacements = \Drupal::token()->generate($type, $input, $data, $options, $bubbleable_metadata);
foreach ($tokens as $name) {
$token = $input[$name];
$this->assertTrue(!isset($replacements[$token]), t("Token value for @token was not generated.", array('@type' => $type, '@token' => $token)));
}
}
function saveAlias($source, $alias, $language = Language::LANGCODE_NOT_SPECIFIED) {
$alias = array(
'source' => $source,
'alias' => $alias,
'language' => $language,
);
\Drupal::service('path.alias_storage')->save($alias['source'], $alias['alias']);
return $alias;
}
function saveEntityAlias($entity_type, EntityInterface $entity, $alias, $language = Language::LANGCODE_NOT_SPECIFIED) {
$uri = $entity->toUrl()->toArray();
return $this->saveAlias($uri['path'], $alias, $language);
}
/**
* Make a page request and test for token generation.
*/
function assertPageTokens($url, array $tokens, array $data = array(), array $options = array()) {
if (empty($tokens)) {
return TRUE;
}
$token_page_tokens = array(
'tokens' => $tokens,
'data' => $data,
'options' => $options,
);
\Drupal::state()->set('token_page_tokens', $token_page_tokens);
$options += array('url_options' => array());
$this->drupalGet($url, $options['url_options']);
$this->refreshVariables();
$result = \Drupal::state()->get('token_page_tokens', array());
if (!isset($result['values']) || !is_array($result['values'])) {
return $this->fail('Failed to generate tokens.');
}
foreach ($tokens as $token => $expected) {
if (!isset($expected)) {
$this->assertTrue(!isset($result['values'][$token]) || $result['values'][$token] === $token, t("Token value for @token was not generated.", array('@token' => $token)));
}
elseif (!isset($result['values'][$token])) {
$this->fail(t('Failed to generate token @token.', array('@token' => $token)));
}
else {
$this->assertIdentical($result['values'][$token], (string) $expected, t("Token value for @token was '@actual', expected value '@expected'.", array('@token' => $token, '@actual' => $result['values'][$token], '@expected' => $expected)));
}
}
}
}

View file

@ -0,0 +1,51 @@
<?php
namespace Drupal\token\Tests;
use Drupal\Core\Url;
/**
* Tests url tokens.
*
* @group token
*/
class TokenURLTest extends TokenTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('node');
/**
* {@inheritdoc}
*/
public function setUp() {
parent::setUp();
$this->saveAlias('/node/1', '/first-node');
}
function testURLTokens() {
$url = new Url('entity.node.canonical', array('node' => 1));
$tokens = array(
'absolute' => $url->setAbsolute()->toString(),
'relative' => $url->setAbsolute(FALSE)->toString(),
'path' => '/first-node',
'brief' => preg_replace(array('!^https?://!', '!/$!'), '', $url->setAbsolute()->toString()),
'args:value:0' => 'first-node',
'args:value:1' => NULL,
'args:value:N' => NULL,
'unaliased' => $url->setAbsolute()->setOption('alias', TRUE)->toString(),
'unaliased:relative' => $url->setAbsolute(FALSE)->setOption('alias', TRUE)->toString(),
'unaliased:path' => '/node/1',
'unaliased:brief' => preg_replace(array('!^https?://!', '!/$!'), '', $url->setAbsolute()->setOption('alias', TRUE)->toString()),
'unaliased:args:value:0' => 'node',
'unaliased:args:value:1' => '1',
'unaliased:args:value:2' => NULL,
// Deprecated tokens.
'alias' => '/first-node',
);
$this->assertTokens('url', array('url' => new Url('entity.node.canonical', array('node' => 1))), $tokens);
}
}

View file

@ -0,0 +1,116 @@
<?php
namespace Drupal\token\Tests;
use Drupal\Core\Session\AnonymousUserSession;
use Drupal\field\Entity\FieldStorageConfig;
/**
* Tests user tokens.
*
* @group token
*/
class TokenUserTest extends TokenTestBase {
/**
* The user account.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $account = NULL;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('token_user_picture');
/**
* {@inheritdoc}
*/
public function setUp() {
parent::setUp();
$this->account = $this->drupalCreateUser(['administer users', 'administer account settings']);
$this->drupalLogin($this->account);
}
public function testUserTokens() {
// Enable user pictures.
\Drupal::state()->set('user_pictures', 1);
\Drupal::state()->set('user_picture_file_size', '');
// Set up the pictures directory.
$picture_path = file_default_scheme() . '://' . \Drupal::state()->get('user_picture_path', 'pictures');
if (!file_prepare_directory($picture_path, FILE_CREATE_DIRECTORY)) {
$this->fail('Could not create directory ' . $picture_path . '.');
}
// Add a user picture to the account.
$image = current($this->drupalGetTestFiles('image'));
$edit = array('files[user_picture_0]' => drupal_realpath($image->uri));
$this->drupalPostForm('user/' . $this->account->id() . '/edit', $edit, t('Save'));
$storage = \Drupal::entityTypeManager()->getStorage('user');
// Load actual user data from database.
$storage->resetCache();
$this->account = $storage->load($this->account->id());
$this->assertTrue(!empty($this->account->user_picture->target_id), 'User picture uploaded.');
$picture = [
'#theme' => 'user_picture',
'#account' => $this->account,
];
/** @var \Drupal\Core\Render\RendererInterface $renderer */
$renderer = \Drupal::service('renderer');
$user_tokens = array(
'picture' => $renderer->renderPlain($picture),
'picture:fid' => $this->account->user_picture->target_id,
'picture:size-raw' => 125,
'ip-address' => NULL,
'roles' => implode(', ', $this->account->getRoles()),
);
$this->assertTokens('user', array('user' => $this->account), $user_tokens);
// Remove the simpletest-created user role.
$roles = $this->account->getRoles();
$this->account->removeRole(end($roles));
$this->account->save();
// Remove the user picture field and reload the user.
FieldStorageConfig::loadByName('user', 'user_picture')->delete();
$storage->resetCache();
$this->account = $storage->load($this->account->id());
$user_tokens = array(
'picture' => NULL,
'picture:fid' => NULL,
'ip-address' => NULL,
'roles' => 'authenticated',
'roles:keys' => (string) DRUPAL_AUTHENTICATED_RID,
);
$this->assertTokens('user', array('user' => $this->account), $user_tokens);
// The ip address token should work for the current user token type.
$tokens = array(
'ip-address' => \Drupal::request()->getClientIp(),
);
$this->assertTokens('current-user', array(), $tokens);
$anonymous = new AnonymousUserSession();
$tokens = array(
'roles' => 'anonymous',
'roles:keys' => (string) DRUPAL_ANONYMOUS_RID,
);
$this->assertTokens('user', array('user' => $anonymous), $tokens);
}
public function testUserAccountSettings() {
$this->drupalGet('admin/config/people/accounts');
$this->assertText('The list of available tokens that can be used in e-mails is provided below.');
$this->assertLink('Browse available tokens.');
$this->assertLinkByHref('token/tree');
}
}

View file

@ -0,0 +1,61 @@
<?php
namespace Drupal\token\Tests\Tree;
use Drupal\token\Tests\TokenTestBase;
/**
* Test token autocomplete.
*
* @group token
*/
class AutocompleteTest extends TokenTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['node'];
/**
* Tests autocomplete for node tokens.
*/
public function testNodeAutocomplete() {
$url_prefix = "token/autocomplete/node/";
$url = $url_prefix . 'Title of [nod';
$response = $this->drupalGetJSON($url);
$this->assertTrue(isset($response['[node:nid]']));
$this->assertTrue(isset($response['[node:author]']));
$this->assertTrue(isset($response['[node:url]']));
$this->assertTrue(isset($response['[node:url:']));
$url = $url_prefix . 'Title of [node:url:';
$response = $this->drupalGetJSON($url);
$this->assertTrue(isset($response['[node:url:path]']));
$this->assertTrue(isset($response['[node:url:absolute]']));
}
/**
* Tests autocomplete for user tokens.
*/
public function testUserAutocomplete() {
$url_prefix = "token/autocomplete/user/";
$url = $url_prefix . 'Name of the [us';
$response = $this->drupalGetJSON($url);
$this->assertTrue(isset($response['[user:uid]']));
$this->assertTrue(isset($response['[user:original]']));
$this->assertTrue(isset($response['[user:url]']));
$this->assertTrue(isset($response['[user:url:']));
$url = $url_prefix . 'Title of [user:original:';
$response = $this->drupalGetJSON($url);
$this->assertTrue(isset($response['[user:original:uid]']));
}
}

View file

@ -0,0 +1,69 @@
<?php
namespace Drupal\token\Tests\Tree;
use Drupal\token\Tests\TokenTestBase;
/**
* Tests token tree on help page.
*
* @group token
*/
class HelpPageTest extends TokenTestBase {
use TokenTreeTestTrait;
/**
* @var \Drupal\Core\Session\AccountInterface
*/
protected $account;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['help'];
public function setUp() {
parent::setUp();
$this->account = $this->drupalCreateUser(['access administration pages']);
$this->drupalLogin($this->account);
}
/**
* Tests the token browser on the token help page.
*/
public function testHelpPageTree() {
$this->drupalGet('admin/help/token');
$this->assertText('The list of the currently available tokens on this site are shown below.');
$this->assertTokenGroup('Current date');
$this->assertTokenGroup('Site information');
$this->assertTokenInTree('[current-date:html_date]', 'current-date');
$this->assertTokenInTree('[current-date:html_week]', 'current-date');
$this->assertTokenInTree('[date:html_date]', 'date');
$this->assertTokenInTree('[date:html_week]', 'date');
$this->assertTokenInTree('[current-user:account-name]', 'current-user');
$this->assertTokenInTree('[user:account-name]', 'user');
$this->assertTokenInTree('[current-page:url:unaliased]', 'current-page--url');
$this->assertTokenInTree('[current-page:url:unaliased:args]', 'current-page--url--unaliased');
$this->assertTokenInTree('[user:original:account-name]', 'user--original');
// Assert some of the restricted tokens to ensure they are shown.
$this->assertTokenInTree('[user:one-time-login-url]', 'user');
$this->assertTokenInTree('[user:original:cancel-url]', 'user--original');
// The Array token is marked as nested, so it should not show up as a top
// level token, only nested under another token. For instance, user:roles
// is of type Array and tokens of type Array have 'nested' setting true.
$this->assertTokenNotGroup('Array');
$this->assertTokenNotGroup('user:roles');
$this->assertTokenInTree('[user:roles]', 'user');
}
}

View file

@ -0,0 +1,131 @@
<?php
namespace Drupal\token\Tests\Tree;
/**
* Helper trait to assert tokens in token tree browser.
*/
trait TokenTreeTestTrait {
/**
* Get an array of token groups from the last retrieved page.
*
* @return array
* Array of token group names.
*/
protected function getTokenGroups() {
$groups = $this->xpath('//tr[contains(@class, "token-group")]/td[1]');
return array_map(function ($item) {
return (string) $item;
}, $groups);
}
/**
* Check to see if the specified token group is present in the token browser.
*
* @param string $token_group
* The name of the token group.
* @param string $message
* (optional) A message to display with the assertion.
* @param string $group
* (optional) The group this message is in, which is displayed in a column
* in test output.
*/
protected function assertTokenGroup($token_group, $message = '', $group = 'Other') {
$groups = $this->getTokenGroups();
if (!$message) {
$message = "Token group $token_group found.";
}
$this->assertTrue(in_array($token_group, $groups), $message, $group);
}
/**
* Check to see if the specified token group is not present in the token
* browser.
*
* @param string $token_group
* The name of the token group.
* @param string $message
* (optional) A message to display with the assertion.
* @param string $group
* (optional) The group this message is not in, which is displayed in a
* column in test output.
*/
protected function assertTokenNotGroup($token_group, $message = '', $group = 'Other') {
$groups = $this->getTokenGroups();
if (!$message) {
$message = "Token group $token_group not found.";
}
$this->assertFalse(in_array($token_group, $groups), $message, $group);
}
/**
* Check to see if the specified token is present in the token browser.
*
* @param $token
* The token name with the surrounding square brackets [].
* @param string $parent
* (optional) The parent CSS identifier of this token.
* @param string $message
* (optional) A message to display with the assertion.
* @param string $group
* (optional) The group this message is in, which is displayed in a column
* in test output.
*/
protected function assertTokenInTree($token, $parent = '', $message = '', $group = 'Other') {
$xpath = $this->getXpathForTokenInTree($token, $parent);
if (!$message) {
$message = "Token $token found.";
}
$this->assertIdentical(1, count($this->xpath($xpath)), $message, $group);
}
/**
* Check to see if the specified token is present in the token browser.
*
* @param $token
* The token name with the surrounding square brackets [].
* @param string $parent
* (optional) The parent CSS identifier of this token.
* @param string $message
* (optional) A message to display with the assertion.
* @param string $group
* (optional) The group this message is in, which is displayed in a column
* in test output.
*/
protected function assertTokenNotInTree($token, $parent = '', $message = '', $group = 'Other') {
$xpath = $this->getXpathForTokenInTree($token, $parent);
if (!$message) {
$message = "Token $token not found.";
}
$this->assertIdentical(0, count($this->xpath($xpath)), $message, $group);
}
/**
* Get xpath to check for token in tree.
*
* @param $token
* The token name with the surrounding square brackets [].
* @param string $parent
* (optional) The parent CSS identifier of this token.
*
* @return string
* The xpath to check for the token and parent.
*/
protected function getXpathForTokenInTree($token, $parent = '') {
$xpath = "//tr";
if ($parent) {
$xpath .= '[@data-tt-parent-id="token-' . $parent . '"]';
}
$xpath .= '/td[contains(@class, "token-key") and text() = "' . $token . '"]';
return $xpath;
}
}

View file

@ -0,0 +1,148 @@
<?php
namespace Drupal\token\Tests\Tree;
use Drupal\Component\Serialization\Json;
use Drupal\token\Tests\TokenTestBase;
/**
* Tests token tree page.
*
* @group token
*/
class TreeTest extends TokenTestBase {
use TokenTreeTestTrait;
/**
* @var \Drupal\Core\Session\AccountInterface
*/
protected $account;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['node'];
public function setUp() {
parent::setUp();
$this->account = $this->drupalCreateUser(['administer account settings']);
$this->drupalLogin($this->account);
}
/**
* Test various tokens that are possible on the site.
*/
public function testAllTokens() {
$this->drupalGet($this->getTokenTreeUrl(['token_types' => 'all']));
$this->assertTokenGroup('Current date');
$this->assertTokenGroup('Site information');
$this->assertTokenInTree('[current-date:html_date]', 'current-date');
$this->assertTokenInTree('[current-date:html_week]', 'current-date');
$this->assertTokenInTree('[date:html_date]', 'date');
$this->assertTokenInTree('[date:html_week]', 'date');
$this->assertTokenInTree('[current-user:account-name]', 'current-user');
$this->assertTokenInTree('[user:account-name]', 'user');
$this->assertTokenInTree('[current-page:url:unaliased]', 'current-page--url');
$this->assertTokenInTree('[current-page:url:unaliased:args]', 'current-page--url--unaliased');
$this->assertTokenInTree('[user:original:account-name]', 'user--original');
}
/**
* Test various tokens that are possible on the site.
*/
public function testGlobalTokens() {
$this->drupalGet($this->getTokenTreeUrl());
$this->assertTokenGroup('Current date');
$this->assertTokenGroup('Site information');
// Assert that non-global tokens are not listed.
$this->assertTokenNotInTree('[user:account-name]', 'user');
$this->assertTokenNotInTree('[user:original:account-name]', 'user--original');
// Assert some of the global tokens, just to be sure.
$this->assertTokenInTree('[current-date:html_date]', 'current-date');
$this->assertTokenInTree('[current-date:html_week]', 'current-date');
$this->assertTokenInTree('[current-user:account-name]', 'current-user');
$this->assertTokenInTree('[current-page:url:unaliased]', 'current-page--url');
$this->assertTokenInTree('[current-page:url:unaliased:args]', 'current-page--url--unaliased');
}
/**
* Tests if the token browser displays the user tokens.
*/
public function testUserTokens() {
$this->drupalGet($this->getTokenTreeUrl(['token_types' => ['user']]));
$this->assertTokenGroup('Users');
$this->assertTokenInTree('[user:account-name]', 'user');
$this->assertTokenInTree('[user:original:account-name]', 'user--original');
// Assert some of the restricted tokens to ensure they are not shown.
$this->assertTokenNotInTree('[user:one-time-login-url]', 'user');
$this->assertTokenNotInTree('[user:original:cancel-url]', 'user--original');
// Request with show_restricted set to TRUE to show restricted tokens and
// check for them.
$this->drupalGet($this->getTokenTreeUrl(['token_types' => ['user'], 'show_restricted' => TRUE]));
$this->assertEqual('MISS', $this->drupalGetHeader('x-drupal-dynamic-cache'), 'Cache was not hit');
$this->assertTokenInTree('[user:one-time-login-url]', 'user');
$this->assertTokenInTree('[user:original:cancel-url]', 'user--original');
}
/**
* Tests if the token browser displays the node tokens.
*/
public function testNodeTokens() {
$this->drupalGet($this->getTokenTreeUrl(['token_types' => ['node']]));
$this->assertTokenGroup('Nodes');
$this->assertTokenInTree('[node:body]', 'node');
$this->assertTokenInTree('[node:author:original:account-name]', 'node--author--original');
}
/**
* Get the URL for the token tree based on the specified options.
*
* The token tree route's URL requires CSRF and cannot be generated in the
* test code. The CSRF token generated using the test runner's session is
* different from the session inside the test environment. This is why the
* link has to be generated inside the environment.
*
* This function calls a page in token_module_test module which generates the
* link and the token. This then replaces the options query parameter with the
* specified options.
*
* The page also uses a title callback to set title to a render array, which
* allows us to test if [current-page:title] works properly.
*
* @param array $options
* The options for the token tree browser.
*
* @return string
* The complete URL of the token tree browser with the CSRF token.
*/
protected function getTokenTreeUrl($options = []) {
$this->drupalGet('token_module_test/browse');
$this->assertTitle('Available Tokens | Drupal');
$links = $this->xpath('//a[contains(@href, :href)]/@href', array(':href' => 'token/tree'));
$link = $this->getAbsoluteUrl((string) current($links));
if (!empty($options)) {
$options = Json::encode($options);
$link = str_replace('options=%5B%5D', 'options=' . urlencode($options), $link);
}
return $link;
}
}

View file

@ -0,0 +1,213 @@
<?php
namespace Drupal\token;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Utility\Token as TokenBase;
/**
* Service to retrieve token information.
*
* This service replaces the core's token service and provides the same
* functionality by extending it. It also provides additional functionality
* commonly required by the additional support provided by token module and
* other modules.
*/
class Token extends TokenBase implements TokenInterface {
/**
* Token definitions.
*
* @var array[]|null
* An array of token definitions, or NULL when the definitions are not set.
*
* @see self::resetInfo()
*/
protected $globalTokenTypes;
/**
* {@inheritdoc}
*/
public function getInfo() {
if (empty($this->tokenInfo)) {
$cache_id = 'token_info_sorted:' . $this->languageManager->getCurrentLanguage(LanguageInterface::TYPE_CONTENT)->getId();
$cache = $this->cache->get($cache_id);
if ($cache) {
$this->tokenInfo = $cache->data;
}
else {
$token_info = $this->moduleHandler->invokeAll('token_info');
$this->moduleHandler->alter('token_info', $token_info);
foreach (array_keys($token_info['types']) as $type_key) {
if (isset($token_info['types'][$type_key]['type'])) {
$base_type = $token_info['types'][$type_key]['type'];
// If this token type extends another token type, then merge in
// the base token type's tokens.
if (isset($token_info['tokens'][$base_type])) {
$token_info['tokens'] += [$type_key => []];
$token_info['tokens'][$type_key] += $token_info['tokens'][$base_type];
}
}
else {
// Add a 'type' value to each token type information.
$token_info['types'][$type_key]['type'] = $type_key;
}
}
// Pre-sort tokens.
$by_name = $this->prepareMultisort($token_info['types']);
array_multisort($by_name, SORT_ASC, SORT_NATURAL | SORT_FLAG_CASE, $token_info['types']);
foreach (array_keys($token_info['tokens']) as $type) {
$by_name = $this->prepareMultisort($token_info['tokens'][$type]);
array_multisort($by_name, SORT_ASC, SORT_NATURAL | SORT_FLAG_CASE, $token_info['tokens'][$type]);
}
$this->tokenInfo = $token_info;
$this->cache->set($cache_id, $this->tokenInfo, CacheBackendInterface::CACHE_PERMANENT, array(
static::TOKEN_INFO_CACHE_TAG,
));
}
}
return $this->tokenInfo;
}
/**
* Extracts data from the token data for use in array_multisort().
*
* @param array $token_info
* List of tokens or token types, each element must have a name key.
*
* @return string[]
* List of the names keyed by the token key.
*/
protected function prepareMultisort($token_info) {
$by_name = [];
foreach ($token_info as $key => $token_info_element) {
$by_name[$key] = $token_info_element['name'];
}
return $by_name;
}
/**
* {@inheritdoc}
*/
public function getTokenInfo($token_type, $token) {
if (empty($this->tokenInfo)) {
$this->getInfo();
}
return isset($this->tokenInfo['tokens'][$token_type][$token]) ? $this->tokenInfo['tokens'][$token_type][$token] : NULL;
}
/**
* {@inheritdoc}
*/
public function getTypeInfo($token_type) {
if (empty($this->tokenInfo)) {
$this->getInfo();
}
return isset($this->tokenInfo['types'][$token_type]) ? $this->tokenInfo['types'][$token_type] : NULL;
}
/**
* {@inheritdoc}
*/
public function getGlobalTokenTypes() {
if (empty($this->globalTokenTypes)) {
$token_info = $this->getInfo();
foreach ($token_info['types'] as $type => $type_info) {
// If the token types has not specified that 'needs-data' => TRUE, then
// it is a global token type that will always be replaced in any context.
if (empty($type_info['needs-data'])) {
$this->globalTokenTypes[] = $type;
}
}
}
return $this->globalTokenTypes;
}
/**
* {@inheritdoc}
*/
function getInvalidTokens($type, $tokens) {
$token_info = $this->getInfo();
$invalid_tokens = array();
foreach ($tokens as $token => $full_token) {
if (isset($token_info['tokens'][$type][$token])) {
continue;
}
// Split token up if it has chains.
$parts = explode(':', $token, 2);
if (!isset($token_info['tokens'][$type][$parts[0]])) {
// This is an invalid token (not defined).
$invalid_tokens[] = $full_token;
}
elseif (count($parts) == 2) {
$sub_token_info = $token_info['tokens'][$type][$parts[0]];
if (!empty($sub_token_info['dynamic'])) {
// If this token has been flagged as a dynamic token, skip it.
continue;
}
elseif (empty($sub_token_info['type'])) {
// If the token has chains, but does not support it, it is invalid.
$invalid_tokens[] = $full_token;
}
else {
// Recursively check the chained tokens.
$sub_tokens = $this->findWithPrefix(array($token => $full_token), $parts[0]);
$invalid_tokens = array_merge($invalid_tokens, $this->getInvalidTokens($sub_token_info['type'], $sub_tokens));
}
}
}
return $invalid_tokens;
}
/**
* {@inheritdoc}
*/
public function getInvalidTokensByContext($value, array $valid_types = []) {
if (in_array('all', $valid_types)) {
$info = $this->getInfo();
$valid_types = array_keys($info['types']);
}
else {
// Add the token types that are always valid in global context.
$valid_types = array_merge($valid_types, $this->getGlobalTokenTypes());
}
$invalid_tokens = array();
$value_tokens = is_string($value) ? $this->scan($value) : $value;
foreach ($value_tokens as $type => $tokens) {
if (!in_array($type, $valid_types)) {
// If the token type is not a valid context, its tokens are invalid.
$invalid_tokens = array_merge($invalid_tokens, array_values($tokens));
}
else {
// Check each individual token for validity.
$invalid_tokens = array_merge($invalid_tokens, $this->getInvalidTokens($type, $tokens));
}
}
array_unique($invalid_tokens);
return $invalid_tokens;
}
/**
* {@inheritdoc}
*/
public function resetInfo() {
parent::resetInfo();
$this->globalTokenTypes = NULL;
}
}

View file

@ -0,0 +1,82 @@
<?php
namespace Drupal\token;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
/**
* Service to provide mappings between entity and token types.
*
* Why do we need this? Because when the token API was moved to core we did not
* reuse the entity type as the base name for taxonomy terms and vocabulary
* tokens.
*/
class TokenEntityMapper implements TokenEntityMapperInterface {
/**
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* @var array
*/
protected $entityMappings;
public function __construct(EntityTypeManagerInterface $entity_type_manager, ModuleHandlerInterface $module_handler) {
$this->entityTypeManager = $entity_type_manager;
$this->moduleHandler = $module_handler;
}
/**
* {@inheritdoc}
*/
public function getEntityTypeMappings() {
if (empty($this->entityMappings)) {
foreach ($this->entityTypeManager->getDefinitions() as $entity_type => $info) {
$this->entityMappings[$entity_type] = $info->get('token_type') ?: $entity_type;
}
// Allow modules to alter the mapping array.
$this->moduleHandler->alter('token_entity_mapping', $this->entityMappings);
}
return $this->entityMappings;
}
/**
* {@inheritdoc}
*/
function getEntityTypeForTokenType($token_type, $fallback = FALSE) {
if (empty($this->entityMappings)) {
$this->getEntityTypeMappings();
}
$return = array_search($token_type, $this->entityMappings);
return $return !== FALSE ? $return : ($fallback ? $token_type : FALSE);
}
/**
* {@inheritdoc}
*/
function getTokenTypeForEntityType($entity_type, $fallback = FALSE) {
if (empty($this->entityMappings)) {
$this->getEntityTypeMappings();
}
return isset($this->entityMappings[$entity_type]) ? $this->entityMappings[$entity_type] : ($fallback ? $entity_type : FALSE);
}
/**
* {@inheritdoc}
*/
public function resetInfo() {
$this->entityMappings = NULL;
}
}

View file

@ -0,0 +1,53 @@
<?php
namespace Drupal\token;
interface TokenEntityMapperInterface {
/**
* Return an array of entity type to token type mappings.
*
* @return array
* An array of mappings with entity type mapping to token type.
*/
public function getEntityTypeMappings();
/**
* Return the entity type of a particular token type.
*
* @param string $token_type
* The token type for which the mapping is returned.
* @param bool $fallback
* (optional) Defaults to FALSE. If true, the same $value is returned in
* case the mapping was not found.
*
* @return string
* The entity type of the token type specified.
*
* @see token_entity_info_alter()
* @see http://drupal.org/node/737726
*/
function getEntityTypeForTokenType($token_type, $fallback = FALSE);
/**
* Return the token type of a particular entity type.
*
* @param string $entity_type
* The entity type for which the mapping is returned.
* @param bool $fallback
* (optional) Defaults to FALSE. If true, the same $value is returned in
* case the mapping was not found.
*
* @return string
* The token type of the entity type specified.
*
* @see token_entity_info_alter()
* @see http://drupal.org/node/737726
*/
function getTokenTypeForEntityType($entity_type, $fallback = FALSE);
/**
* Resets metadata describing token and entity mappings.
*/
public function resetInfo();
}

View file

@ -0,0 +1,75 @@
<?php
namespace Drupal\token;
interface TokenInterface {
/**
* Returns metadata describing supported token types.
*
* @param $token_type
* The token type for which the metadata is required.
*
* @return array[]
* An array of token type information from hook_token_info() for the
* specified token type.
*
* @see hook_token_info()
* @see hook_token_info_alter()
*/
public function getTypeInfo($token_type);
/**
* Returns metadata describing supported a token.
*
* @param $token_type
* The token type for which the metadata is required.
* @param $token
* The token name for which the metadata is required.
*
* @return array[]
* An array of information from hook_token_info() for the specified token.
*
* @see hook_token_info()
* @see hook_token_info_alter()
*
* @deprecated
*/
public function getTokenInfo($token_type, $token);
/**
* Get a list of token types that can be used without any context (global).
*
* @return array[]
* An array of global token types.
*/
public function getGlobalTokenTypes();
/**
* Validate an array of tokens based on their token type.
*
* @param string $type
* The type of tokens to validate (e.g. 'node', etc.)
* @param string[] $tokens
* A keyed array of tokens, and their original raw form in the source text.
*
* @return string[]
* An array with the invalid tokens in their original raw forms.
*/
function getInvalidTokens($type, $tokens);
/**
* Validate tokens in raw text based on possible contexts.
*
* @param string|string[] $value
* A string with the raw text containing the raw tokens, or an array of
* tokens from token_scan().
* @param string[] $valid_types
* An array of token types that will be used when token replacement is
* performed.
*
* @return string[]
* An array with the invalid tokens in their original raw forms.
*/
public function getInvalidTokensByContext($value, array $valid_types = []);
}

View file

@ -0,0 +1,20 @@
<?php
namespace Drupal\token;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DependencyInjection\ServiceProviderBase;
/**
* Replace core's token service with our own.
*/
class TokenServiceProvider extends ServiceProviderBase {
/**
* {@inheritdoc}
*/
public function alter(ContainerBuilder $container) {
$definition = $container->getDefinition('token');
$definition->setClass('\Drupal\token\Token');
}
}

View file

@ -0,0 +1,267 @@
<?php
namespace Drupal\token;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Render\BubbleableMetadata;
class TreeBuilder implements TreeBuilderInterface {
/**
* @var \Drupal\token\Token
*/
protected $tokenService;
/**
* @var \Drupal\token\TokenEntityMapperInterface
*/
protected $entityMapper;
/**
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected $languageManager;
/**
* @var \Drupal\Core\Cache\CacheBackendInterface
*/
protected $cacheBackend;
/**
* Cache already built trees.
*
* @var array
*/
protected $builtTrees;
public function __construct(TokenInterface $token_service, TokenEntityMapperInterface $entity_mapper, CacheBackendInterface $cache_backend, LanguageManagerInterface $language_manager) {
$this->tokenService = $token_service;
$this->entityMapper = $entity_mapper;
$this->cacheBackend = $cache_backend;
$this->languageManager = $language_manager;
}
/**
* {@inheritdoc}
*/
public function buildRenderable(array $token_types, array $options = []) {
// Set default options.
$options += [
'global_types' => TRUE,
'click_insert' => TRUE,
'show_restricted' => FALSE,
'show_nested' => FALSE,
'recursion_limit' => 3,
];
$info = $this->tokenService->getInfo();
if ($options['global_types']) {
$token_types = array_merge($token_types, $this->tokenService->getGlobalTokenTypes());
}
$element = array(
/*'#cache' => array(
'cid' => 'tree-rendered:' . hash('sha256', serialize(array('token_types' => $token_types, 'global_types' => NULL) + $variables)),
'tags' => array(Token::TOKEN_INFO_CACHE_TAG),
),*/
);
// @todo Find a way to use the render cache for this.
/*if ($cached_output = token_render_cache_get($element)) {
return $cached_output;
}*/
$tree_options = [
'flat' => TRUE,
'restricted' => $options['show_restricted'],
'nested' => $options['show_nested'],
'depth' => $options['recursion_limit'],
];
$token_tree = [];
foreach ($info['types'] as $type => $type_info) {
if (!in_array($type, $token_types)) {
continue;
}
$token_tree[$type] = $type_info;
$token_tree[$type]['tokens'] = $this->buildTree($type, $tree_options);
}
$element += [
'#type' => 'token_tree_table',
'#token_tree' => $token_tree,
'#show_restricted' => $options['show_restricted'],
'#show_nested' => $options['show_nested'],
'#click_insert' => $options['click_insert'],
'#columns' => ['name', 'token', 'description'],
'#empty' => t('No tokens available'),
];
return $element;
}
/**
* {@inheritdoc}
*/
public function buildAllRenderable(array $options = []) {
$info = $this->tokenService->getInfo();
$token_types = array_keys($info['types']);
// Disable merging in global types as we will be adding in all token types
// explicitly. There is no difference in leaving this set to TRUE except for
// an additional method call which is unnecessary.
$options['global_types'] = FALSE;
return $this->buildRenderable($token_types, $options);
}
/**
* {@inheritdoc}
*/
public function buildTree($token_type, array $options = []) {
$options += [
'restricted' => FALSE,
'depth' => 4,
'data' => [],
'values' => FALSE,
'flat' => FALSE,
];
// Do not allow past the maximum token information depth.
$options['depth'] = min($options['depth'], static::MAX_DEPTH);
// If $token_type is an entity, make sure we are using the actual token type.
if ($entity_token_type = $this->entityMapper->getTokenTypeForEntityType($token_type)) {
$token_type = $entity_token_type;
}
$langcode = $this->languageManager->getCurrentLanguage()->getId();
$tree_cid = "token_tree:{$token_type}:{$langcode}:{$options['depth']}";
// If we do not have this base tree in the static cache, check the cache
// otherwise generate and store it in the cache.
if (!isset($this->builtTrees[$tree_cid])) {
if ($cache = $this->cacheBackend->get($tree_cid)) {
$this->builtTrees[$tree_cid] = $cache->data;
}
else {
$options['parents'] = [];
$this->builtTrees[$tree_cid] = $this->getTokenData($token_type, $options);
$this->cacheBackend->set($tree_cid, $this->builtTrees[$tree_cid], Cache::PERMANENT, [Token::TOKEN_INFO_CACHE_TAG]);
}
}
$tree = $this->builtTrees[$tree_cid];
// If the user has requested a flat tree, convert it.
if (!empty($options['flat'])) {
$tree = $this->flattenTree($tree);
}
// Fill in token values.
if (!empty($options['values'])) {
$token_values = [];
foreach ($tree as $token => $token_info) {
if (!empty($token_info['dynamic']) || !empty($token_info['restricted'])) {
continue;
}
elseif (!isset($token_info['value'])) {
$token_values[$token_info['token']] = $token;
}
}
if (!empty($token_values)) {
$token_values = $this->tokenService->generate($token_type, $token_values, $options['data'], [], new BubbleableMetadata());
foreach ($token_values as $token => $replacement) {
$tree[$token]['value'] = $replacement;
}
}
}
return $tree;
}
/**
* {@inheritdoc}
*/
public function flattenTree(array $tree) {
$result = [];
foreach ($tree as $token => $token_info) {
$result[$token] = $token_info;
if (isset($token_info['children']) && is_array($token_info['children'])) {
$result += $this->flattenTree($token_info['children']);
}
}
return $result;
}
/**
* Generate a token tree.
*
* @param string $token_type
* The token type.
* @param array $options
* An associative array of additional options. See documentation for
* TreeBuilderInterface::buildTree() for more information.
*
* @return array
* The token data for the specified $token_type.
*
* @internal
*/
protected function getTokenData($token_type, array $options) {
$options += [
'parents' => [],
];
$info = $this->tokenService->getInfo();
if ($options['depth'] <= 0 || !isset($info['types'][$token_type]) || !isset($info['tokens'][$token_type])) {
return [];
}
$tree = [];
foreach ($info['tokens'][$token_type] as $token => $token_info) {
// Build the raw token string.
$token_parents = $options['parents'];
if (empty($token_parents)) {
// If the parents array is currently empty, assume the token type is its
// parent.
$token_parents[] = $token_type;
}
elseif (in_array($token, array_slice($token_parents, 1), TRUE)) {
// Prevent duplicate recursive tokens. For example, this will prevent
// the tree from generating the following tokens or deeper:
// [comment:parent:parent]
// [comment:parent:root:parent]
continue;
}
$token_parents[] = $token;
if (!empty($token_info['dynamic'])) {
$token_parents[] = '?';
}
$raw_token = '[' . implode(':', $token_parents) . ']';
$tree[$raw_token] = $token_info;
$tree[$raw_token]['raw token'] = $raw_token;
// Add the token's real name (leave out the base token type).
$tree[$raw_token]['token'] = implode(':', array_slice($token_parents, 1));
// Add the token's parent as its raw token value.
if (!empty($options['parents'])) {
$tree[$raw_token]['parent'] = '[' . implode(':', $options['parents']) . ']';
}
// Fetch the child tokens.
if (!empty($token_info['type'])) {
$child_options = $options;
$child_options['depth']--;
$child_options['parents'] = $token_parents;
$tree[$raw_token]['children'] = $this->getTokenData($token_info['type'], $child_options);
}
}
return $tree;
}
}

View file

@ -0,0 +1,79 @@
<?php
namespace Drupal\token;
interface TreeBuilderInterface {
/**
* The maximum depth for token tree recursion.
*/
const MAX_DEPTH = 9;
/**
* Build a tree array of tokens used for themeing or information.
*
* @param string $token_type
* The token type.
* @param array $options
* (optional) An associative array of additional options, with the following
* elements:
* - 'flat' (defaults to FALSE): Set to true to generate a flat list of
* token information. Otherwise, child tokens will be inside the
* 'children' parameter of a token.
* - 'restricted' (defaults to FALSE): Set to true to how restricted tokens.
* - 'depth' (defaults to 4): Maximum number of token levels to recurse.
*
* @return array
* The token information constructed in a tree or flat list form depending
* on $options['flat'].
*/
public function buildTree($token_type, array $options = []);
/**
* Flatten a token tree.
*
* @param array $tree
* The tree array as returned by TreeBuilderInterface::buildTree().
*
* @return array
* The flattened version of the tree.
*/
public function flattenTree(array $tree);
/**
* Build a render array with token tree built as per specified options.
*
* @param array $token_types
* An array containing token types that should be shown in the tree.
* @param array $options
* (optional) An associative array to control which tokens are shown and
* how. The properties available are:
* - 'global_types' (defaults to TRUE): Show all global token types along
* with the specified types.
* - 'click_insert' (defaults to TRUE): Include classes and caption to show
* allow inserting tokens in fields by clicking on them.
* - 'show_restricted' (defaults to FALSE): Show restricted tokens in the
* tree.
* - 'show_nested' (defaults to FALSE): If this token is nested and should
* therefor not show on the token browser as a top level token.
* - 'recursion_limit' (defaults to 3): Only show tokens up to the specified
* depth.
*
* @return array
* Render array for the token tree.
*/
public function buildRenderable(array $token_types, array $options = []);
/**
* Build a render array with token tree containing all possible tokens.
*
* @param array $options
* (optional) An associative array to control which tokens are shown and
* how. The properties available are: See
* \Drupal\token\TreeBuilderInterface::buildRenderable() for details.
*
* @return array
* Render array for the token tree.
*/
public function buildAllRenderable(array $options = []);
}

View file

@ -0,0 +1,19 @@
{#
/**
* @file
* Default theme implementation for the token tree link.
*
* Available variables:
* - url: The URL to the token tree page.
* - text: The text to be displayed in the link.
* - attributes: Attributes for the anchor tag.
* - link: The complete link.
*
* @see template_preprocess_token_tree_link()
*
* @ingroup themeable
*/
#}
{% if link -%}
{{ link }}
{%- endif %}

View file

@ -0,0 +1,6 @@
id: token_module_test
label: 'Token test'
status: true
langcode: en
locked: true
pattern: Y

View file

@ -0,0 +1,37 @@
<?php
namespace Drupal\token_module_test\Controller;
use Drupal\Core\Controller\ControllerBase;
use Symfony\Component\HttpFoundation\Request;
class TokenTreeBrowseController extends ControllerBase {
/**
* Page callback to output a link.
*/
function outputLink(Request $request) {
$build['tree']['#theme'] = 'token_tree_link';
$build['tokenarea'] = [
'#markup' => \Drupal::token()->replace('[current-page:title]'),
'#type' => 'markup',
];
return $build;
}
/**
* Title callback for the page outputting a link.
*
* We are using a title callback instead of directly defining the title in the
* routing YML file. This is so that we could return an array instead of a
* simple string. This allows us to test if [current-page:title] works with
* render arrays and other objects as titles.
*/
public function getTitle() {
return [
'#type' => 'markup',
'#markup' => 'Available Tokens',
];
}
}

View file

@ -0,0 +1,12 @@
type: module
name: Token Module Test
description: Testing module for token functionality.
package: Testing
# core: 8.x
hidden: TRUE
# Information added by Drupal.org packaging script on 2017-04-29
version: '8.x-1.0'
core: '8.x'
project: 'token'
datestamp: 1493466847

View file

@ -0,0 +1,32 @@
<?php
/**
* @file
* Helper module for token tests.
*/
use Drupal\node\NodeInterface;
/**
* Implements hook_page_attachments().
*/
function token_module_test_page_attachments() {
if ($debug = \Drupal::state()->get('token_page_tokens', array())) {
$debug += array('tokens' => array(), 'data' => array(), 'options' => array());
foreach (array_keys($debug['tokens']) as $token) {
$debug['values'][$token] = \Drupal::token()->replace($token, $debug['data'], $debug['options']);
}
\Drupal::state()->set('token_page_tokens', $debug);
}
}
/**
* Implements hook_ENTITY_TYPE_presave for Node entities.
*/
function token_module_test_node_presave(NodeInterface $node) {
// Transform tokens in the body.
// @see \Drupal\token\Tests\TokenMenuTest::testMenuTokens()
if ($node->hasField('body')) {
$node->body->value = \Drupal::token()
->replace($node->body->value, ['node' => $node]);
}
}

View file

@ -0,0 +1,7 @@
token_module_test.browse:
path: '/token_module_test/browse'
defaults:
_controller: '\Drupal\token_module_test\Controller\TokenTreeBrowseController::outputLink'
_title_callback: '\Drupal\token_module_test\Controller\TokenTreeBrowseController::getTitle'
requirements:
_access: 'TRUE'

View file

@ -0,0 +1,13 @@
<?php
/**
* Implements hook_token_info()
*/
function token_module_test_token_info() {
$info['tokens']['node']['colons:in:name'] = array(
'name' => t('A test token with colons in the name'),
'description' => NULL,
);
return $info;
}

View file

@ -0,0 +1,23 @@
langcode: de
status: true
dependencies:
module:
- image
- user
id: user.user.default
targetEntityType: user
bundle: user
mode: default
content:
account:
weight: -10
user_picture:
type: image_image
settings:
progress_indicator: throbber
preview_image_style: thumbnail
third_party_settings: { }
weight: -1
timezone:
weight: 6
hidden: { }

View file

@ -0,0 +1,31 @@
id: user.user.user_picture
status: true
langcode: en
entity_type: user
bundle: user
field_name: user_picture
label: Picture
description: 'Your virtual face or picture.'
required: false
default_value: { }
default_value_function: ''
settings:
file_extensions: 'png gif jpg jpeg'
file_directory: pictures
max_filesize: '30 KB'
alt_field: false
title_field: false
max_resolution: 85x85
min_resolution: ''
default_image:
uuid: null
alt: ''
title: ''
width: null
height: null
alt_field_required: false
title_field_required: false
field_type: image
dependencies:
config:
- field.storage.user.user_picture

View file

@ -0,0 +1,25 @@
id: user.user_picture
status: true
langcode: en
field_name: user_picture
entity_type: user
type: image
settings:
uri_scheme: public
default_image:
uuid: null
alt: ''
title: ''
width: null
height: null
module: image
locked: false
cardinality: 1
translatable: false
indexes:
target_id:
- target_id
dependencies:
module:
- image
- user

View file

@ -0,0 +1,14 @@
type: module
name: Token User picture
description: Testing module that provides user pictures field.
package: Testing
# core: 8.x
hidden: TRUE
dependencies:
- image
# Information added by Drupal.org packaging script on 2017-04-29
version: '8.x-1.0'
core: '8.x'
project: 'token'
datestamp: 1493466847

View file

@ -0,0 +1,60 @@
<?php
namespace Drupal\Tests\token\Kernel;
/**
* Tests array tokens.
*
* @group token
*/
class ArrayTest extends KernelTestBase {
function testArrayTokens() {
// Test a simple array.
$array = array(0 => 'a', 1 => 'b', 2 => 'c', 4 => 'd');
$tokens = array(
'first' => 'a',
'last' => 'd',
'value:0' => 'a',
'value:2' => 'c',
'count' => 4,
'keys' => '0, 1, 2, 4',
'keys:value:3' => '4',
'keys:join' => '0124',
'reversed' => 'd, c, b, a',
'reversed:keys' => '4, 2, 1, 0',
'join:/' => 'a/b/c/d',
'join' => 'abcd',
'join:, ' => 'a, b, c, d',
'join: ' => 'a b c d',
);
$this->assertTokens('array', array('array' => $array), $tokens);
// Test a mixed simple and render array.
// 2 => c, 0 => a, 4 => d, 1 => b
$array = array(
'#property' => 'value',
0 => 'a',
1 => array('#markup' => 'b', '#weight' => 0.01),
2 => array('#markup' => 'c', '#weight' => -10),
4 => array('#markup' => 'd', '#weight' => 0),
);
$tokens = array(
'first' => 'c',
'last' => 'b',
'value:0' => 'a',
'value:2' => 'c',
'count' => 4,
'keys' => '2, 0, 4, 1',
'keys:value:3' => '1',
'keys:join' => '2041',
'reversed' => 'b, d, a, c',
'reversed:keys' => '1, 4, 0, 2',
'join:/' => 'c/a/d/b',
'join' => 'cadb',
'join:, ' => 'c, a, d, b',
'join: ' => 'c a d b',
);
$this->assertTokens('array', array('array' => $array), $tokens);
}
}

View file

@ -0,0 +1,107 @@
<?php
namespace Drupal\Tests\token\Kernel;
use Drupal\node\Entity\Node;
use Drupal\Core\Url;
/**
* Test the book tokens.
*
* @group token
*/
class BookTest extends KernelTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['user', 'field', 'filter', 'text', 'node', 'book'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installEntitySchema('user');
$this->installEntitySchema('node');
$this->installSchema('book', array('book'));
$this->installSchema('node', array('node_access'));
$this->installConfig(array('node', 'book', 'field'));
}
function testBookTokens() {
$book = Node::create([
'type' => 'book',
'title' => 'Book Main Page',
'book' => ['bid' => 'new'],
]);
$book->save();
$page1 = Node::create([
'type' => 'book',
'title' => '1st Page',
'book' => ['bid' => $book->id(), 'pid' => $book->id()],
]);
$page1->save();
$page2 = Node::create([
'type' => 'book',
'title' => '2nd Page',
'book' => ['bid' => $book->id(), 'pid' => $page1->id()],
]);
$page2->save();
$book_title = $book->getTitle();
$tokens = [
'nid' => $book->id(),
'title' => $book_title,
'book:title' => $book_title,
'book:root' => $book_title,
'book:root:nid' => $book->id(),
'book:root:title' => $book_title,
'book:root:url' => Url::fromRoute('entity.node.canonical', ['node' => $book->id()], array('absolute' => TRUE))->toString(),
'book:root:content-type' => 'Book page',
'book:parent' => null,
'book:parents' => null,
];
$this->assertTokens('node', array('node' => $book), $tokens);
$tokens = [
'nid' => $page1->id(),
'title' => $page1->getTitle(),
'book:title' => $book_title,
'book:root' => $book_title,
'book:root:nid' => $book->id(),
'book:root:title' => $book_title,
'book:root:url' => Url::fromRoute('entity.node.canonical', ['node' => $book->id()], array('absolute' => TRUE))->toString(),
'book:root:content-type' => 'Book page',
'book:parent:nid' => $book->id(),
'book:parent:title' => $book_title,
'book:parent:url' => Url::fromRoute('entity.node.canonical', ['node' => $book->id()], array('absolute' => TRUE))->toString(),
'book:parents:count' => 1,
'book:parents:join:/' => $book_title,
];
$this->assertTokens('node', array('node' => $page1), $tokens);
$tokens = [
'nid' => $page2->id(),
'title' => $page2->getTitle(),
'book:title' => $book_title,
'book:root' => $book_title,
'book:root:nid' => $book->id(),
'book:root:title' => $book_title,
'book:root:url' => Url::fromRoute('entity.node.canonical', ['node' => $book->id()], array('absolute' => TRUE))->toString(),
'book:root:content-type' => 'Book page',
'book:parent:nid' => $page1->id(),
'book:parent:title' => $page1->getTitle(),
'book:parent:url' => Url::fromRoute('entity.node.canonical', ['node' => $page1->id()], array('absolute' => TRUE))->toString(),
'book:parents:count' => 2,
'book:parents:join:/' => $book_title . '/' . $page1->getTitle(),
];
$this->assertTokens('node', array('node' => $page2), $tokens);
}
}

View file

@ -0,0 +1,102 @@
<?php
namespace Drupal\Tests\token\Kernel;
use Drupal\comment\Entity\Comment;
use Drupal\comment\Tests\CommentTestTrait;
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
use Drupal\Core\Url;
/**
* Tests comment tokens.
*
* @group token
*/
class CommentTest extends KernelTestBase {
use CommentTestTrait;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['node', 'comment', 'field', 'text', 'entity_reference'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installEntitySchema('node');
$this->installEntitySchema('user');
$this->installEntitySchema('comment');
$this->installSchema('comment', ['comment_entity_statistics']);
$node_type = NodeType::create(['type' => 'page', 'name' => t('Page')]);
$node_type->save();
$this->installConfig(['comment']);
$this->addDefaultCommentField('node', 'page');
}
function testCommentTokens() {
$node = Node::create([
'type' => 'page',
'title' => $this->randomMachineName()
]);
$node->save();
$parent_comment = Comment::create([
'entity_id' => $node->id(),
'entity_type' => 'node',
'field_name' => 'comment',
'name' => 'anonymous user',
'mail' => 'anonymous@example.com',
'subject' => $this->randomMachineName(),
'body' => $this->randomMachineName(),
]);
$parent_comment->save();
// Fix http://example.com/index.php/comment/1 fails 'url:path' test.
$parent_comment_path = $parent_comment->url();
$tokens = array(
'url' => $parent_comment->urlInfo('canonical', ['fragment' => "comment-{$parent_comment->id()}"])->setAbsolute()->toString(),
'url:absolute' => $parent_comment->urlInfo('canonical', ['fragment' => "comment-{$parent_comment->id()}"])->setAbsolute()->toString(),
'url:relative' => $parent_comment->urlInfo('canonical', ['fragment' => "comment-{$parent_comment->id()}"])->toString(),
'url:path' => $parent_comment_path,
'parent:url:absolute' => NULL,
);
$this->assertTokens('comment', array('comment' => $parent_comment), $tokens);
$comment = Comment::create([
'entity_id' => $node->id(),
'pid' => $parent_comment->id(),
'entity_type' => 'node',
'field_name' => 'comment',
'uid' => 1,
'name' => 'anonymous user',
'mail' => 'anonymous@example.com',
'subject' => $this->randomMachineName(),
'body' => $this->randomMachineName(),
]);
$comment->save();
// Fix http://example.com/index.php/comment/1 fails 'url:path' test.
$comment_path = Url::fromRoute('entity.comment.canonical', array('comment' => $comment->id()))->toString();
$tokens = array(
'url' => $comment->urlInfo('canonical', ['fragment' => "comment-{$comment->id()}"])->setAbsolute()->toString(),
'url:absolute' => $comment->urlInfo('canonical', ['fragment' => "comment-{$comment->id()}"])->setAbsolute()->toString(),
'url:relative' => $comment->urlInfo('canonical', ['fragment' => "comment-{$comment->id()}"])->toString(),
'url:path' => $comment_path,
'parent:url:absolute' => $parent_comment->urlInfo('canonical', ['fragment' => "comment-{$parent_comment->id()}"])->setAbsolute()->toString(),
);
$this->assertTokens('comment', array('comment' => $comment), $tokens);
}
}

View file

@ -0,0 +1,35 @@
<?php
namespace Drupal\Tests\token\Kernel;
/**
* Tests date tokens.
*
* @group token
*/
class DateTest extends KernelTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = [];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installConfig(['system', 'token_module_test']);
}
function testDateTokens() {
$tokens = array(
'token_module_test' => '1984',
'invalid_format' => NULL,
);
$this->assertTokens('date', array('date' => 453859200), $tokens);
}
}

View file

@ -0,0 +1,104 @@
<?php
namespace Drupal\Tests\token\Kernel;
use Drupal\Component\Utility\Unicode;
use Drupal\node\Entity\Node;
use Drupal\taxonomy\Entity\Vocabulary;
use Drupal\taxonomy\VocabularyInterface;
/**
* Tests entity tokens.
*
* @group token
*/
class EntityTest extends KernelTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['node', 'taxonomy', 'text'];
/**
* {@inheritdoc}
*/
public function setUp() {
parent::setUp();
// Create the default tags vocabulary.
$vocabulary = Vocabulary::create([
'name' => 'Tags',
'vid' => 'tags',
]);
$vocabulary->save();
$this->installEntitySchema('taxonomy_term');
$this->installEntitySchema('user');
$this->installEntitySchema('node');
$this->vocab = $vocabulary;
}
function testEntityMapping() {
/** @var \Drupal\token\TokenEntityMapperInterface $mapper */
$mapper = \Drupal::service('token.entity_mapper');
$this->assertIdentical($mapper->getEntityTypeForTokenType('node'), 'node');
$this->assertIdentical($mapper->getEntityTypeForTokenType('term'), 'taxonomy_term');
$this->assertIdentical($mapper->getEntityTypeForTokenType('vocabulary'), 'taxonomy_vocabulary');
$this->assertIdentical($mapper->getEntityTypeForTokenType('invalid'), FALSE);
$this->assertIdentical($mapper->getEntityTypeForTokenType('invalid', TRUE), 'invalid');
$this->assertIdentical($mapper->getTokenTypeForEntityType('node'), 'node');
$this->assertIdentical($mapper->getTokenTypeForEntityType('taxonomy_term'), 'term');
$this->assertIdentical($mapper->getTokenTypeForEntityType('taxonomy_vocabulary'), 'vocabulary');
$this->assertIdentical($mapper->getTokenTypeForEntityType('invalid'), FALSE);
$this->assertIdentical($mapper->getTokenTypeForEntityType('invalid', TRUE), 'invalid');
// Test that when we send the mis-matched entity type into token_replace()
// that we still get the tokens replaced.
$vocabulary = entity_load('taxonomy_vocabulary', 'tags');
$term = $this->addTerm($vocabulary);
$this->assertIdentical(\Drupal::token()->replace('[vocabulary:name]', array('taxonomy_vocabulary' => $vocabulary)), $vocabulary->label());
$this->assertIdentical(\Drupal::token()->replace('[term:name][term:vocabulary:name]', array('taxonomy_term' => $term)), $term->label() . $vocabulary->label());
}
function addTerm(VocabularyInterface $vocabulary, array $term = array()) {
$term += array(
'name' => Unicode::strtolower($this->randomMachineName(5)),
'vid' => $vocabulary->id(),
);
$term = entity_create('taxonomy_term', $term);
$term->save();
return $term;
}
/**
* Test the [entity:original:*] tokens.
*/
function testEntityOriginal() {
$node = Node::create(['type' => 'page', 'title' => 'Original title']);
$node->save();
$tokens = array(
'nid' => $node->id(),
'title' => 'Original title',
'original' => NULL,
'original:nid' => NULL,
);
$this->assertTokens('node', array('node' => $node), $tokens);
// Emulate the original entity property that would be available from
// node_save() and change the title for the node.
$node->original = entity_load_unchanged('node', $node->id());
$node->title = 'New title';
$tokens = array(
'nid' => $node->id(),
'title' => 'New title',
'original' => 'Original title',
'original:nid' => $node->id(),
);
$this->assertTokens('node', array('node' => $node), $tokens);
}
}

View file

@ -0,0 +1,708 @@
<?php
namespace Drupal\Tests\token\Kernel;
use Drupal\Component\Utility\Unicode;
use Drupal\contact\Entity\ContactForm;
use Drupal\Core\Entity\Entity\EntityViewMode;
use Drupal\Core\Render\Markup;
use Drupal\datetime\Plugin\Field\FieldType\DateTimeItem;
use Drupal\datetime_range\Plugin\Field\FieldType\DateRangeItem;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\filter\Entity\FilterFormat;
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
use Drupal\contact\Entity\Message;
use Drupal\Component\Utility\Html;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\taxonomy\Tests\TaxonomyTestTrait;
use Drupal\language\Entity\ConfigurableLanguage;
/**
* Tests field tokens.
*
* @group token
*/
class FieldTest extends KernelTestBase {
use TaxonomyTestTrait;
/**
* @var \Drupal\filter\FilterFormatInterface
*/
protected $testFormat;
/**
* Vocabulary for testing chained token support.
*
* @var \Drupal\taxonomy\VocabularyInterface
*/
protected $vocabulary;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['node', 'text', 'field', 'filter', 'contact', 'options', 'taxonomy', 'language', 'datetime', 'datetime_range'];
/**
* {@inheritdoc}
*/
public function setUp() {
parent::setUp();
$this->installEntitySchema('user');
$this->installEntitySchema('node');
$this->installEntitySchema('taxonomy_term');
// Create the article content type with a text field.
$node_type = NodeType::create([
'type' => 'article',
]);
$node_type->save();
$field_storage = FieldStorageConfig::create([
'field_name' => 'test_field',
'entity_type' => 'node',
'type' => 'text',
]);
$field_storage->save();
$field = FieldConfig::create([
'field_name' => 'test_field',
'entity_type' => 'node',
'bundle' => 'article',
'label' => 'Test field',
]);
$field->save();
// Create a reference field with the same name on user.
$field_storage = FieldStorageConfig::create([
'field_name' => 'test_field',
'entity_type' => 'user',
'type' => 'entity_reference',
]);
$field_storage->save();
$field = FieldConfig::create([
'field_name' => 'test_field',
'entity_type' => 'user',
'bundle' => 'user',
'label' => 'Test field',
]);
$field->save();
$this->testFormat = FilterFormat::create([
'format' => 'test',
'weight' => 1,
'filters' => [
'filter_html_escape' => ['status' => TRUE],
],
]);
$this->testFormat->save();
// Create a multi-value list_string field.
$field_storage = FieldStorageConfig::create([
'field_name' => 'test_list',
'entity_type' => 'node',
'type' => 'list_string',
'cardinality' => 2,
'settings' => [
'allowed_values' => [
'key1' => 'value1',
'key2' => 'value2',
]
],
]);
$field_storage->save();
$this->field = FieldConfig::create([
'field_name' => 'test_list',
'entity_type' => 'node',
'bundle' => 'article',
])->save();
// Add an untranslatable node reference field.
FieldStorageConfig::create([
'field_name' => 'test_reference',
'type' => 'entity_reference',
'entity_type' => 'node',
'settings' => [
'target_type' => 'node',
],
'translatable' => FALSE,
])->save();
FieldConfig::create([
'field_name' => 'test_reference',
'entity_type' => 'node',
'bundle' => 'article',
'label' => 'Test reference',
])->save();
// Add an untranslatable taxonomy term reference field.
$this->vocabulary = $this->createVocabulary();
FieldStorageConfig::create([
'field_name' => 'test_term_reference',
'type' => 'entity_reference',
'entity_type' => 'node',
'settings' => [
'target_type' => 'taxonomy_term',
],
'translatable' => FALSE,
])->save();
FieldConfig::create([
'field_name' => 'test_term_reference',
'entity_type' => 'node',
'bundle' => 'article',
'label' => 'Test term reference',
'settings' => [
'handler' => 'default:taxonomy_term',
'handler_settings' => [
'target_bundles' => [
$this->vocabulary->id() => $this->vocabulary->id(),
],
],
],
])->save();
// Add a field to terms of the created vocabulary.
$storage = FieldStorageConfig::create([
'field_name' => 'term_field',
'entity_type' => 'taxonomy_term',
'type' => 'text',
]);
$storage->save();
$field = FieldConfig::create([
'field_name' => 'term_field',
'entity_type' => 'taxonomy_term',
'bundle' => $this->vocabulary->id(),
]);
$field->save();
// Add a second language.
$language = ConfigurableLanguage::create([
'id' => 'de',
'label' => 'German',
]);
$language->save();
// Add a datetime field.
$field_datetime_storage = FieldStorageConfig::create(array(
'field_name' => 'field_datetime',
'type' => 'datetime',
'entity_type' => 'node',
'settings' => array('datetime_type' => DateTimeItem::DATETIME_TYPE_DATETIME),
));
$field_datetime_storage->save();
$field_datetime = FieldConfig::create([
'field_storage' => $field_datetime_storage,
'bundle' => 'article',
]);
$field_datetime->save();
// Add a daterange field.
$field_daterange_storage = FieldStorageConfig::create(array(
'field_name' => 'field_daterange',
'type' => 'daterange',
'entity_type' => 'node',
'settings' => array('datetime_type' => DateRangeItem::DATETIME_TYPE_DATETIME),
));
$field_daterange_storage->save();
$field_daterange = FieldConfig::create([
'field_storage' => $field_daterange_storage,
'bundle' => 'article',
]);
$field_daterange->save();
}
/**
* Tests [entity:field_name] tokens.
*/
public function testEntityFieldTokens() {
// Create a node with a value in its fields and test its tokens.
$entity = Node::create([
'title' => 'Test node title',
'type' => 'article',
'test_field' => [
'value' => 'foo',
'format' => $this->testFormat->id(),
],
'test_list' => [
'value1',
'value2',
],
]);
$entity->save();
$this->assertTokens('node', ['node' => $entity], [
'test_field' => Markup::create('foo'),
'test_field:0' => Markup::create('foo'),
'test_field:0:value' => 'foo',
'test_field:value' => 'foo',
'test_field:0:format' => $this->testFormat->id(),
'test_field:format' => $this->testFormat->id(),
'test_list:0' => Markup::create('value1'),
'test_list:1' => Markup::create('value2'),
'test_list:0:value' => Markup::create('value1'),
'test_list:value' => Markup::create('value1'),
'test_list:1:value' => Markup::create('value2'),
]);
// Verify that no third token was generated for the list_string field.
$this->assertNoTokens('node', ['node' => $entity], [
'test_list:2',
'test_list:2:value',
]);
// Test the test_list token metadata.
$tokenService = \Drupal::service('token');
$token_info = $tokenService->getTokenInfo('node', 'test_list');
$this->assertEqual($token_info['name'], 'test_list');
$this->assertEqual($token_info['module'], 'token');
$this->assertEqual($token_info['type'], 'list<node-test_list>');
$typeInfo = $tokenService->getTypeInfo('list<node-test_list>');
$this->assertEqual($typeInfo['name'], 'List of test_list values');
$this->assertEqual($typeInfo['type'], 'list<node-test_list>');
// Create a node type that does not have test_field field.
$node_type = NodeType::create([
'type' => 'page',
]);
$node_type->save();
$node_without_test_field = Node::create([
'title' => 'Node without test_field',
'type' => 'page',
]);
$node_without_test_field->save();
// Ensure that trying to generate tokens for a non-existing field does not
// throw an exception.
$this->assertNoTokens('node', ['node' => $node_without_test_field], ['test_field']);
// Create a node without a value in the text field and test its token.
$entity = Node::create([
'title' => 'Test node title',
'type' => 'article',
]);
$entity->save();
$this->assertNoTokens('node', ['node' => $entity], [
'test_field',
]);
}
/**
* Tests the token metadata for a field token.
*/
public function testFieldTokenInfo() {
/** @var \Drupal\token\Token $tokenService */
$tokenService = \Drupal::service('token');
// Test the token info of the text field of the artcle content type.
$token_info = $tokenService->getTokenInfo('node', 'test_field');
$this->assertEqual($token_info['name'], 'Test field', 'The token info name is correct.');
$this->assertEqual($token_info['description'], 'Text (formatted) field.', 'The token info description is correct.');
$this->assertEqual($token_info['module'], 'token', 'The token info module is correct.');
// Now create two more content types that share the field but the last
// of them sets a different label. This should show an alternative label
// at the token info.
$node_type = NodeType::create([
'type' => 'article2',
]);
$node_type->save();
$field = FieldConfig::create([
'field_name' => 'test_field',
'entity_type' => 'node',
'bundle' => 'article2',
'label' => 'Test field',
]);
$field->save();
$node_type = NodeType::create([
'type' => 'article3',
]);
$node_type->save();
$field = FieldConfig::create([
'field_name' => 'test_field',
'entity_type' => 'node',
'bundle' => 'article3',
'label' => 'Different test field',
]);
$field->save();
$token_info = $tokenService->getTokenInfo('node', 'test_field');
$this->assertEqual($token_info['name'], 'Test field', 'The token info name is correct.');
$this->assertEqual((string) $token_info['description'], 'Text (formatted) field. Also known as <em class="placeholder">Different test field</em>.', 'When a field is used in several bundles with different labels, this is noted at the token info description.');
$this->assertEqual($token_info['module'], 'token', 'The token info module is correct.');
$this->assertEqual($token_info['type'], 'node-test_field', 'The field property token info type is correct.');
// Test field property token info.
$token_info = $tokenService->getTokenInfo('node-test_field', 'value');
$this->assertEqual($token_info['name'], 'Text', 'The field property token info name is correct.');
// This particular field property description happens to be empty.
$this->assertEqual((string) $token_info['description'], '', 'The field property token info description is correct.');
$this->assertEqual($token_info['module'], 'token', 'The field property token info module is correct.');
}
/**
* Test tokens on node with the token view mode overriding default formatters.
*/
public function testTokenViewMode() {
$value = 'A really long string that should be trimmed by the special formatter on token view we are going to have.';
// The formatter we are going to use will eventually call Unicode::strlen.
// This expects that the Unicode has already been explicitly checked, which
// happens in DrupalKernel. But since that doesn't run in kernel tests, we
// explicitly call this here.
Unicode::check();
// Create a node with a value in the text field and test its token.
$entity = Node::create([
'title' => 'Test node title',
'type' => 'article',
'test_field' => [
'value' => $value,
'format' => $this->testFormat->id(),
],
]);
$entity->save();
$this->assertTokens('node', ['node' => $entity], [
'test_field' => Markup::create($value),
]);
// Now, create a token view mode which sets a different format for
// test_field. When replacing tokens, this formatter should be picked over
// the default formatter for the field type.
// @see field_tokens().
$view_mode = EntityViewMode::create([
'id' => 'node.token',
'targetEntityType' => 'node',
]);
$view_mode->save();
$entity_display = entity_get_display('node', 'article', 'token');
$entity_display->setComponent('test_field', [
'type' => 'text_trimmed',
'settings' => [
'trim_length' => 50,
]
]);
$entity_display->save();
$this->assertTokens('node', ['node' => $entity], [
'test_field' => Markup::create(substr($value, 0, 50)),
]);
}
/**
* Test that tokens are properly created for an entity's base fields.
*/
public function testBaseFieldTokens() {
// Create a new contact_message entity and verify that tokens are generated
// for its base fields. The contact_message entity type is used because it
// provides no tokens by default.
$contact_form = ContactForm::create([
'id' => 'form_id',
]);
$contact_form->save();
$entity = Message::create([
'contact_form' => 'form_id',
'uuid' => '123',
'langcode' => 'en',
'name' => 'Test name',
'mail' => 'Test mail',
'subject' => 'Test subject',
'message' => 'Test message',
'copy' => FALSE,
]);
$entity->save();
$this->assertTokens('contact_message', ['contact_message' => $entity], [
'uuid' => Markup::create('123'),
'langcode' => Markup::create('English'),
'name' => Markup::create('Test name'),
'mail' => Markup::create('Test mail'),
'subject' => Markup::create('Test subject'),
'message' => Markup::create('Test message'),
'copy' => 'Off',
]);
// Test the metadata of one of the tokens.
$tokenService = \Drupal::service('token');
$token_info = $tokenService->getTokenInfo('contact_message', 'subject');
$this->assertEquals($token_info['name'], 'Subject');
$this->assertEquals($token_info['description'], 'Text (plain) field.');
$this->assertEquals($token_info['module'], 'token');
// Verify that node entity type doesn't have a uid token.
$this->assertNull($tokenService->getTokenInfo('node', 'uid'));
}
/*
* Tests chaining entity reference tokens.
*/
public function testEntityReferenceTokens() {
$reference = Node::create([
'title' => 'Test node to reference',
'type' => 'article',
'test_field' => [
'value' => 'foo',
'format' => $this->testFormat->id(),
]
]);
$reference->save();
$term_reference_field_value = $this->randomString();
$term_reference = $this->createTerm($this->vocabulary, [
'name' => 'Term to reference',
'term_field' => [
'value' => $term_reference_field_value,
'format' => $this->testFormat->id(),
],
]);
$entity = Node::create([
'title' => 'Test entity reference',
'type' => 'article',
'test_reference' => ['target_id' => $reference->id()],
'test_term_reference' => ['target_id' => $term_reference->id()],
]);
$entity->save();
$this->assertTokens('node', ['node' => $entity], [
'test_reference:entity:title' => Markup::create('Test node to reference'),
'test_reference:entity:test_field' => Markup::create('foo'),
'test_term_reference:entity:term_field' => Html::escape($term_reference_field_value),
'test_reference:target_id' => $reference->id(),
'test_term_reference:target_id' => $term_reference->id(),
'test_term_reference:entity:url:path' => '/' . $term_reference->toUrl('canonical')->getInternalPath(),
// Expects the entity's label to be returned for :entity tokens.
'test_reference:entity' => $reference->label(),
'test_term_reference:entity' => $term_reference->label(),
]);
// Test some non existent tokens.
$this->assertNoTokens('node', ['node' => $entity], [
'test_reference:1:title',
'test_reference:entity:does_not_exist',
'test_reference:does_not:exist',
'test_term_reference:does_not_exist',
'test_term_reference:does:not:exist',
'test_term_reference:does_not_exist:0',
'non_existing_field:entity:title',
]);
/** @var \Drupal\token\Token $token_service */
$token_service = \Drupal::service('token');
$token_info = $token_service->getTokenInfo('node', 'test_reference');
$this->assertEquals('Test reference', $token_info['name']);
$this->assertEquals('Entity reference field.', (string) $token_info['description']);
$this->assertEquals('token', $token_info['module']);
$this->assertEquals('node-test_reference', $token_info['type']);
// Test target_id field property token info.
$token_info = $token_service->getTokenInfo('node-test_reference', 'target_id');
$this->assertEquals('Content ID', $token_info['name']);
$this->assertEquals('token', $token_info['module']);
$this->assertEquals('token', $token_info['module']);
// Test entity field property token info.
$token_info = $token_service->getTokenInfo('node-test_reference', 'entity');
$this->assertEquals('Content', $token_info['name']);
$this->assertEquals('The referenced entity', $token_info['description']);
$this->assertEquals('token', $token_info['module']);
$this->assertEquals('node', $token_info['type']);
// Test entity field property token info of the term reference.
$token_info = $token_service->getTokenInfo('node-test_term_reference', 'entity');
$this->assertEquals('Taxonomy term', $token_info['name']);
$this->assertEquals('The referenced entity', $token_info['description']);
$this->assertEquals('token', $token_info['module']);
$this->assertEquals('term', $token_info['type']);
}
/**
* Tests support for cardinality > 1 for entity reference tokens.
*/
public function testEntityReferenceTokensCardinality() {
/** @var \Drupal\field\FieldStorageConfigInterface $storage */
$storage = FieldStorageConfig::load('node.test_term_reference');
$storage->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED);
$storage->save();
// Add a few terms.
$terms = [];
$terms_value = [];
foreach (range(1, 3) as $i) {
$terms_value[$i] = $this->randomString();
$terms[$i] = $this->createTerm($this->vocabulary, [
'name' => $this->randomString(),
'term_field' => [
'value' => $terms_value[$i],
'format' => $this->testFormat->id(),
],
]);
}
$entity = Node::create([
'title' => 'Test multivalue chained tokens',
'type' => 'article',
'test_term_reference' => [
['target_id' => $terms[1]->id()],
['target_id' => $terms[2]->id()],
['target_id' => $terms[3]->id()],
],
]);
$entity->save();
$this->assertTokens('node', ['node' => $entity], [
'test_term_reference:0:entity:term_field' => Html::escape($terms[1]->term_field->value),
'test_term_reference:1:entity:term_field' => Html::escape($terms[2]->term_field->value),
'test_term_reference:2:entity:term_field' => Html::escape($terms[3]->term_field->value),
'test_term_reference:0:target_id' => $terms[1]->id(),
'test_term_reference:1:target_id' => $terms[2]->id(),
'test_term_reference:2:target_id' => $terms[3]->id(),
// Expects the entity's label to be returned for :entity tokens.
'test_term_reference:0:entity' => $terms[1]->label(),
'test_term_reference:1:entity' => $terms[2]->label(),
'test_term_reference:2:entity' => $terms[3]->label(),
// To make sure tokens without an explicit delta can also be replaced in
// the same token replacement call.
'test_term_reference:entity:term_field' => Html::escape($terms[1]->term_field->value),
'test_term_reference:target_id' => $terms[1]->id(),
]);
// Test some non existent tokens.
$this->assertNoTokens('node', ['node' => $entity], [
'test_term_reference:3:term_field',
'test_term_reference:0:does_not_exist',
'test_term_reference:1:does:not:exist',
'test_term_reference:1:2:does_not_exist',
]);
}
/**
* Test tokens for multilingual fields and entities.
*/
public function testMultilingualFields() {
// Create an english term and add a german translation for it.
$term = $this->createTerm($this->vocabulary, [
'name' => 'english-test-term',
'langcode' => 'en',
'term_field' => [
'value' => 'english-term-field-value',
'format' => $this->testFormat->id(),
],
]);
$term->addTranslation('de', [
'name' => 'german-test-term',
'term_field' => [
'value' => 'german-term-field-value',
'format' => $this->testFormat->id(),
],
])->save();
$german_term = $term->getTranslation('de');
// Create an english node, add a german translation for it and add the
// english term to the english node's entity reference field and the
// german term to the german's entity reference field.
$node = Node::create([
'title' => 'english-node-title',
'type' => 'article',
'test_term_reference' => [
'target_id' => $term->id(),
],
'test_field' => [
'value' => 'test-english-field',
'format' => $this->testFormat->id(),
],
]);
$node->addTranslation('de', [
'title' => 'german-node-title',
'test_term_reference' => [
'target_id' => $german_term->id(),
],
'test_field' => [
'value' => 'test-german-field',
'format' => $this->testFormat->id(),
],
])->save();
// Verify the :title token of the english node and the :name token of the
// english term it refers to. Also verify the value of the term's field.
$this->assertTokens('node', ['node' => $node], [
'title' => 'english-node-title',
'test_term_reference:entity:name' => 'english-test-term',
'test_term_reference:entity:term_field:value' => 'english-term-field-value',
'test_term_reference:entity:term_field' => 'english-term-field-value',
'test_field' => 'test-english-field',
'test_field:value' => 'test-english-field',
]);
// Same test for the german node and its german term.
$german_node = $node->getTranslation('de');
$this->assertTokens('node', ['node' => $german_node], [
'title' => 'german-node-title',
'test_term_reference:entity:name' => 'german-test-term',
'test_term_reference:entity:term_field:value' => 'german-term-field-value',
'test_term_reference:entity:term_field' => 'german-term-field-value',
'test_field' => 'test-german-field',
'test_field:value' => 'test-german-field',
]);
// If the langcode is specified, it should have priority over the node's
// active language.
$tokens = [
'test_field' => 'test-german-field',
'test_field:value' => 'test-german-field',
'test_term_reference:entity:term_field' => 'german-term-field-value',
'test_term_reference:entity:term_field:value' => 'german-term-field-value',
];
$this->assertTokens('node', ['node' => $node], $tokens, ['langcode' => 'de']);
}
/**
* Tests support for a datetime fields.
*/
public function testDatetimeFieldTokens() {
$node = Node::create([
'title' => 'Node for datetime field',
'type' => 'article',
]);
$node->set('field_datetime', '1925-09-28T00:00:00')->save();
$this->assertTokens('node', ['node' => $node], [
'field_datetime:date:custom:Y' => '1925',
'field_datetime:date:html_month' => '1925-09',
'field_datetime:date' => $node->field_datetime->date->getTimestamp(),
]);
}
/**
* Tests support for a daterange fields.
*/
public function testDatetimeRangeFieldTokens() {
$node = Node::create([
'title' => 'Node for daterange field',
'type' => 'article',
]);
$node->field_daterange->value = '2013-12-22T00:00:00';
$node->field_daterange->end_value = '2016-08-26T00:00:00';
$node->save();
$this->assertTokens('node', ['node' => $node], [
'field_daterange:start_date:html_month' => '2013-12',
'field_daterange:start_date:custom:Y' => '2013',
'field_daterange:end_date:custom:Y' => '2016',
'field_daterange:start_date' => $node->field_daterange->start_date->getTimestamp(),
]);
}
}

View file

@ -0,0 +1,55 @@
<?php
namespace Drupal\Tests\token\Kernel;
/**
* Tests file tokens.
*
* @group token
*/
class FileTest extends KernelTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('file');
/**
* {@inheritdoc}
*/
public function setUp() {
parent::setUp();
$this->installEntitySchema('file');
}
function testFileTokens() {
// Create a test file object.
$file = entity_create('file', array(
'fid' => 1,
'filename' => 'test.png',
'filesize' => 100,
'uri' => 'public://images/test.png',
'filemime' => 'image/png',
));
$tokens = array(
'basename' => 'test.png',
'extension' => 'png',
'size-raw' => 100,
);
$this->assertTokens('file', array('file' => $file), $tokens);
// Test a file with no extension and a fake name.
$file->filename = 'Test PNG image';
$file->uri = 'public://images/test';
$tokens = array(
'basename' => 'test',
'extension' => '',
'size-raw' => 100,
);
$this->assertTokens('file', array('file' => $file), $tokens);
}
}

View file

@ -0,0 +1,33 @@
<?php
namespace Drupal\Tests\token\Kernel;
use Drupal\KernelTests\KernelTestBase as BaseKernelTestBase;
use Drupal\token\Tests\TokenTestTrait;
/**
* Helper test class with some added functions for testing.
*/
abstract class KernelTestBase extends BaseKernelTestBase {
use TokenTestTrait;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['path', 'token', 'token_module_test', 'system', 'user'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installSchema('system', ['router', 'url_alias']);
\Drupal::service('router.builder')->rebuild();
$this->installConfig(['system']);
}
}

View file

@ -0,0 +1,99 @@
<?php
namespace Drupal\Tests\token\Kernel;
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
use Drupal\Core\Url;
/**
* Test the node and content type tokens.
*
* @group token
*/
class NodeTest extends KernelTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['node', 'field', 'text'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installEntitySchema('user');
$this->installEntitySchema('node');
$node_type = NodeType::create([
'type' => 'page',
'name' => 'Basic page',
'description' => "Use <em>basic pages</em> for your static content, such as an 'About us' page.",
]);
$node_type->save();
$node_type = NodeType::create([
'type' => 'article',
'name' => 'Article',
'description' => "Use <em>articles</em> for time-sensitive content like news, press releases or blog posts.",
]);
$node_type->save();
}
function testNodeTokens() {
$page = Node::create([
'type' => 'page',
'title' => 'Source Title',
'revision_log' => $this->randomMachineName(),
'path' => array('alias' => '/content/source-node')
]);
$page->save();
$tokens = array(
'log' => $page->revision_log->value,
'url:path' => '/content/source-node',
'url:absolute' => Url::fromRoute('entity.node.canonical', ['node' => $page->id()], array('absolute' => TRUE))->toString(),
'url:relative' => Url::fromRoute('entity.node.canonical', ['node' => $page->id()], array('absolute' => FALSE))->toString(),
'url:unaliased:path' => "/node/{$page->id()}",
'content-type' => 'Basic page',
'content-type:name' => 'Basic page',
'content-type:machine-name' => 'page',
'content-type:description' => "Use <em>basic pages</em> for your static content, such as an 'About us' page.",
'content-type:node-count' => 1,
'content-type:edit-url' => Url::fromRoute('entity.node_type.edit_form', ['node_type' => 'page'], array('absolute' => TRUE))->toString(),
'source:title' => 'Source Title',
// Deprecated tokens.
'type' => 'page',
'type-name' => 'Basic page',
'url:alias' => '/content/source-node',
);
$this->assertTokens('node', array('node' => $page), $tokens);
$article = Node::create([
'type' => 'article',
'title' => 'Source Title',
]);
$article->save();
$tokens = array(
'log' => '',
'url:path' => "/node/{$article->id()}",
'url:absolute' => Url::fromRoute('entity.node.canonical', ['node' => $article->id()], array('absolute' => TRUE))->toString(),
'url:relative' => Url::fromRoute('entity.node.canonical', ['node' => $article->id()], array('absolute' => FALSE))->toString(),
'url:unaliased:path' => "/node/{$article->id()}",
'content-type' => 'Article',
'content-type:name' => 'Article',
'content-type:machine-name' => 'article',
'content-type:description' => "Use <em>articles</em> for time-sensitive content like news, press releases or blog posts.",
'content-type:node-count' => 1,
'content-type:edit-url' => Url::fromRoute('entity.node_type.edit_form', ['node_type' => 'article'], array('absolute' => TRUE))->toString(),
'source:title' => 'Source Title',
// Deprecated tokens.
'type' => 'article',
'type-name' => 'Article',
'url:alias' => "/node/{$article->id()}",
);
$this->assertTokens('node', array('node' => $article), $tokens);
}
}

View file

@ -0,0 +1,27 @@
<?php
namespace Drupal\Tests\token\Kernel;
/**
* Tests random tokens.
*
* @group token
*/
class RandomTest extends KernelTestBase {
function testRandomTokens() {
$tokens = array(
'number' => '[0-9]{1,}',
'hash:md5' => '[0-9a-f]{32}',
'hash:sha1' => '[0-9a-f]{40}',
'hash:sha256' => '[0-9a-f]{64}',
'hash:invalid-algo' => NULL,
);
$first_set = $this->assertTokens('random', array(), $tokens, array('regex' => TRUE));
$second_set = $this->assertTokens('random', array(), $tokens, array('regex' => TRUE));
foreach ($first_set as $token => $value) {
$this->assertNotIdentical($first_set[$token], $second_set[$token]);
}
}
}

View file

@ -0,0 +1,151 @@
<?php
namespace Drupal\Tests\token\Kernel;
use Drupal\Component\Utility\Unicode;
use Drupal\taxonomy\Entity\Vocabulary;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\Core\Url;
/**
* Tests taxonomy tokens.
*
* @group token
*/
class TaxonomyTest extends KernelTestBase {
protected $vocab;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('taxonomy', 'text', 'language');
/**
* {@inheritdoc}
*/
public function setUp() {
parent::setUp();
$this->installEntitySchema('taxonomy_term');
// Create the default tags vocabulary.
$vocabulary = Vocabulary::create([
'name' => 'Tags',
'vid' => 'tags',
]);
$vocabulary->save();
$this->vocab = $vocabulary;
}
/**
* Test the additional taxonomy term tokens.
*/
function testTaxonomyTokens() {
$root_term = $this->addTerm($this->vocab, array('name' => 'Root term', 'path' => array('alias' => '/root-term')));
$tokens = array(
'url' => Url::fromRoute('entity.taxonomy_term.canonical', ['taxonomy_term' => $root_term->id()], array('absolute' => TRUE))->toString(),
'url:absolute' => Url::fromRoute('entity.taxonomy_term.canonical', ['taxonomy_term' => $root_term->id()], array('absolute' => TRUE))->toString(),
'url:relative' => Url::fromRoute('entity.taxonomy_term.canonical', ['taxonomy_term' => $root_term->id()], array('absolute' => FALSE))->toString(),
'url:path' => '/root-term',
'url:unaliased:path' => "/taxonomy/term/{$root_term->id()}",
'edit-url' => Url::fromRoute('entity.taxonomy_term.edit_form', ['taxonomy_term' => $root_term->id()], array('absolute' => TRUE))->toString(),
'parents' => NULL,
'parents:count' => NULL,
'parents:keys' => NULL,
'root' => NULL,
// Deprecated tokens
'url:alias' => '/root-term',
);
$this->assertTokens('term', array('term' => $root_term), $tokens);
$parent_term = $this->addTerm($this->vocab, array('name' => 'Parent term', 'parent' => $root_term->id()));
$tokens = array(
'url' => Url::fromRoute('entity.taxonomy_term.canonical', ['taxonomy_term' => $parent_term->id()], array('absolute' => TRUE))->toString(),
'url:absolute' => Url::fromRoute('entity.taxonomy_term.canonical', ['taxonomy_term' => $parent_term->id()], array('absolute' => TRUE))->toString(),
'url:relative' => Url::fromRoute('entity.taxonomy_term.canonical', ['taxonomy_term' => $parent_term->id()], array('absolute' => FALSE))->toString(),
'url:path' => "/taxonomy/term/{$parent_term->id()}",
'url:unaliased:path' => "/taxonomy/term/{$parent_term->id()}",
'edit-url' => Url::fromRoute('entity.taxonomy_term.edit_form', ['taxonomy_term' => $parent_term->id()], array('absolute' => TRUE))->toString(),
'parents' => 'Root term',
'parents:count' => 1,
'parents:keys' => $root_term->id(),
'root' => $root_term->label(),
'root:tid' => $root_term->id(),
// Deprecated tokens
'url:alias' => "/taxonomy/term/{$parent_term->id()}",
);
$this->assertTokens('term', array('term' => $parent_term), $tokens);
$term = $this->addTerm($this->vocab, array('name' => 'Test term', 'parent' => $parent_term->id()));
$tokens = array(
'parents' => 'Root term, Parent term',
'parents:count' => 2,
'parents:keys' => implode(', ', array($root_term->id(), $parent_term->id())),
);
$this->assertTokens('term', array('term' => $term), $tokens);
}
/**
* Test the additional vocabulary tokens.
*/
function testVocabularyTokens() {
$vocabulary = $this->vocab;
$tokens = array(
'machine-name' => 'tags',
'edit-url' => Url::fromRoute('entity.taxonomy_vocabulary.edit_form', ['taxonomy_vocabulary' => $vocabulary->id()], array('absolute' => TRUE))->toString(),
);
$this->assertTokens('vocabulary', array('vocabulary' => $vocabulary), $tokens);
}
function addVocabulary(array $vocabulary = array()) {
$vocabulary += array(
'name' => Unicode::strtolower($this->randomMachineName(5)),
'nodes' => array('article' => 'article'),
);
$vocabulary = entity_create('taxonomy_vocabulary', $vocabulary)->save();
return $vocabulary;
}
function addTerm($vocabulary, array $term = array()) {
$term += array(
'name' => Unicode::strtolower($this->randomMachineName(5)),
'vid' => $vocabulary->id(),
);
$term = entity_create('taxonomy_term', $term);
$term->save();
return $term;
}
/**
* Test the multilingual terms.
*/
function testMultilingualTerms() {
// Add a second language.
$language = ConfigurableLanguage::createFromLangcode('de');
$language->save();
// Create an english parent term and add a german translation for it.
$parent_term = $this->addTerm($this->vocab, [
'name' => 'english-parent-term',
'langcode' => 'en',
]);
$parent_term->addTranslation('de', [
'name' => 'german-parent-term',
])->save();
// Create a term related to the parent term.
$child_term = $this->addTerm($this->vocab, [
'name' => 'english-child-term',
'langcode' => 'en',
'parent' => $parent_term->id(),
]);
$child_term->addTranslation('de', [
'name' => 'german-child-term',
])->save();
// Expect the parent term to be in the specified language.
$this->assertTokens('term', array('term' => $child_term), ['parents' => 'german-parent-term'], ['langcode' => 'de']);
}
}

View file

@ -0,0 +1,119 @@
<?php
namespace Drupal\Tests\token\Kernel;
/**
* Test basic, low-level token functions.
*
* @group token
*/
class UnitTest extends KernelTestBase {
/**
* @var \Drupal\token\Token
*/
protected $tokenService;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['file', 'node'];
/**
* {@inheritdoc}
*/
public function setUp() {
parent::setUp();
$this->tokenService = \Drupal::token();
}
/**
* Test invalid tokens.
*/
public function testGetInvalidTokens() {
$tests = array();
$tests[] = array(
'valid tokens' => array(
'[node:title]',
'[node:created:short]',
'[node:created:custom:invalid]',
'[node:created:custom:mm-YYYY]',
'[node:colons:in:name]',
'[site:name]',
'[site:slogan]',
'[current-date:short]',
'[current-user:uid]',
'[current-user:ip-address]',
),
'invalid tokens' => array(
'[node:title:invalid]',
'[node:created:invalid]',
'[node:created:short:invalid]',
'[node:colons:in:name:invalid]',
'[invalid:title]',
'[site:invalid]',
'[user:ip-address]',
'[user:uid]',
'[comment:cid]',
// Deprecated tokens
'[node:tnid]',
'[node:type]',
'[node:type-name]',
'[date:short]',
),
'types' => array('node'),
);
$tests[] = array(
'valid tokens' => array(
'[node:title]',
'[node:created:short]',
'[node:created:custom:invalid]',
'[node:created:custom:mm-YYYY]',
'[node:colons:in:name]',
'[site:name]',
'[site:slogan]',
'[user:uid]',
'[current-date:short]',
'[current-user:uid]',
),
'invalid tokens' => array(
'[node:title:invalid]',
'[node:created:invalid]',
'[node:created:short:invalid]',
'[node:colons:in:name:invalid]',
'[invalid:title]',
'[site:invalid]',
'[user:ip-address]',
'[comment:cid]',
// Deprecated tokens
'[node:tnid]',
'[node:type]',
'[node:type-name]',
),
'types' => array('all'),
);
foreach ($tests as $test) {
$tokens = array_merge($test['valid tokens'], $test['invalid tokens']);
shuffle($tokens);
$invalid_tokens = $this->tokenService->getInvalidTokensByContext(implode(' ', $tokens), $test['types']);
sort($invalid_tokens);
sort($test['invalid tokens']);
$this->assertEqual($invalid_tokens, $test['invalid tokens'], 'Invalid tokens detected properly: ' . implode(', ', $invalid_tokens));
}
}
/**
* Test that tokens are generated only for content entities.
*/
public function testContentEntityOnlyTokens() {
// Verify that type and token info for a config entity is not generated.
$this->assertNull($this->tokenService->getTokenInfo('user_role', 'original'));
$this->assertNull($this->tokenService->getTokenInfo('user_role', 'url'));
$this->assertNull($this->tokenService->getTypeInfo('user_role'));
}
}

View file

@ -0,0 +1,20 @@
<?php
/**
* @file
* Drush integration for the Token module.
*/
/**
* Implements hook_drush_cache_clear().
*/
function token_drush_cache_clear(&$types) {
$types['token'] = 'drush_token_cache_clear_token_info';
}
/**
* Clear caches internal to Token module.
*/
function drush_token_cache_clear_token_info() {
token_clear_cache();
}

View file

@ -0,0 +1,10 @@
type: module
name: Token
description: Provides a user interface for the Token API and some missing core tokens.
# core: 8.x
# Information added by Drupal.org packaging script on 2017-04-29
version: '8.x-1.0'
core: '8.x'
project: 'token'
datestamp: 1493466847

View file

@ -0,0 +1,316 @@
<?php
/**
* @file
* Install, update and uninstall functions for the token module.
*/
/**
* Implements hook_requirements().
*/
function token_requirements($phase = 'runtime') {
$requirements = array();
if ($phase == 'runtime') {
// Check for various token definition problems.
$token_problems = token_get_token_problems();
// Format and display each token problem.
foreach ($token_problems as $problem_key => $problem) {
if (!empty($problem['problems'])) {
$problems = array_unique($problem['problems']);
$build = [
'#theme' => 'item_list',
'#items' => $problems,
];
$requirements['token-' . $problem_key] = array(
'title' => $problem['label'],
'value' => \Drupal::service('renderer')->renderPlain($build),
'severity' => $problem['severity'],
);
}
}
}
return $requirements;
}
/**
* Implements hook_install().
*/
function token_install() {
// Create a token view mode for each entity type.
$info = \Drupal::entityTypeManager()->getDefinitions();
foreach ($info as $entity_type => $entity_type_info) {
// We're only interested in entity types with a view builder.
if (!$entity_type_info->getViewBuilderClass()) {
continue;
}
// Try to find a token view mode for that entity type.
$storage = \Drupal::entityTypeManager()->getStorage('entity_view_mode');
// Add a token view mode if it does not already exist.
if (!$storage->load("$entity_type.token")) {
$storage->create(array(
'targetEntityType' => $entity_type,
'id' => "$entity_type.token",
'status' => TRUE,
'label' => t('Token'),
))->save();
}
}
}
/**
* Build a list of Drupal 6 tokens and their Drupal 7 token names.
*/
function _token_upgrade_token_list() {
$tokens = array(
// Global tokens
'user-name' => 'current-user:name',
'user-id' => 'current-user:id',
'user-mail' => 'current-user:mail',
'site-url' => 'site:url',
'site-name' => 'site:name',
'site-slogan' => 'site:slogan',
'site-mission' => 'site:mission',
'site-mail' => 'site:mail',
'site-date' => 'date:short',
//'site-date-' => '', // Date tokens expanded below
'current-page-path' => 'current-page:path',
'current-page-url' => 'current-page:url',
'page-number' => 'current-page:page-number',
// Comment tokens
'comment-cid' => 'comment:cid',
'comment-nid' => 'comment:node:nid',
'comment-title' => 'comment:title',
'comment-body' => 'comment:body',
'comment-author-name' => 'comment:author:name',
'comment-author-mail' => 'comment:author:mail',
//'comment-body-format' => '',
//'comment-' => '', // Date tokens expanded below
'comment-node-title' => 'comment:node',
// Node tokens
'nid' => 'node:nid',
'type' => 'node:type',
'type-name' => 'node:type-name',
'language' => 'node:language',
'title' => 'node:title',
'author-uid' => 'node:author:uid',
'author-name' => 'node:author:name',
'author-mail' => 'node:author:mail',
'node_comment_count' => 'node:comment-count',
'unread_comment_count' => 'node:comment-count-new',
'log' => 'node:log',
//'' => '', // Date tokens expanded below
//'mod-' => '', // Date tokens expanded below
'menupath' => 'node:menu-link:parent:path][node:menu-link',
'menu' => 'node:menu-link:menu-name',
'menu-link-title' => 'node:menu-link',
'menu-link-mlid' => 'node:menu-link:mlid',
'menu-link-plid' => 'node:menu-link:parent:mlid',
//'term' => 'node:term',
//'term-id' => 'node:term:tid',
//'vocab' => 'node:term:vocabulary',
//'vocab-id' => 'node:term:vocabulary:vid',
// Book tokens
//'book' => 'node:book',
//'book_id' => 'node:book:bid',
//'bookpath' => 'node:book:path',
// Taxonomy tokens
'tid' => 'term:tid',
'cat' => 'term:name',
'cat-description' => 'term:description',
'vid' => 'term:vocabulary:vid',
'vocab' => 'term:vocabulary',
'vocab-description' => 'term:vocabulary:description',
// User tokens
'user' => 'user:name',
'uid' => 'user:uid',
'mail' => 'user:mail',
'reg-date' => 'user:created',
'reg-since' => 'user:created:since',
//'user-created' => '', // Date tokens expanded below
'log-date' => 'user:last-login',
'log-since' => 'user:last-login:since',
//'user-last-login' => '', // Date tokens expanded below
//'date-in-tz' => '',
'account-url' => 'user:url',
'account-edit' => 'user:edit-url',
);
// Account for date tokens which need to be expanded.
$tokens += _token_upgrade_token_date_list('site-', 'site:date');
$tokens += _token_upgrade_token_date_list('', 'node:created');
$tokens += _token_upgrade_token_date_list('mod-', 'node:changed');
//$tokens += _token_upgrade_token_date_list('node-revision-', 'node:changed');
$tokens += _token_upgrade_token_date_list('comment-', 'comment:created');
$tokens += _token_upgrade_token_date_list('user-register-', 'user:created');
$tokens += _token_upgrade_token_date_list('user-last-login-', 'user:last-login');
return $tokens;
}
/**
* Build a list of Drupal 6 date tokens and their Drupal 7 token names.
*/
function _token_upgrade_token_date_list($old_token, $new_token) {
$tokens = array();
$formats = array(
'yyyy' => 'Y',
'yy' => 'y',
'month' => 'F',
'mon' => 'M',
'mm' => 'm',
'm' => 'n',
'ww' => 'W',
'date' => 'N',
'day' => 'l',
'ddd' => 'D',
'dd' => 'd',
'd' => 'j',
);
foreach ($formats as $token_format => $date_format) {
$tokens[$old_token . $token_format] = "$new_token:custom:$date_format";
}
$tokens[$old_token . 'raw'] = "$new_token:raw";
$tokens[$old_token . 'since'] = "$new_token:since";
return $tokens;
}
/**
* Update a string containing Drupal 6 style tokens to Drupal 7 style tokens.
*
* @param $text
* A string containing tokens.
* @param $updates
* An optional array of Drupal 7 tokens keyed by their Drupal 6 token name.
* The default tokens will be merged into this array. Note neither the old
* or new token names should include the surrounding bracket ([ and ])
* characters.
* @return
* A string with the tokens upgraded
*
* @see _token_upgrade_token_list()
*/
function token_update_token_text($text, $updates = array(), $leading = '[', $trailing = ']') {
$updates += _token_upgrade_token_list();
$regex = '/' . preg_quote($leading, '/') . '([^\s]*)' . preg_quote($trailing, '/') . '/';
preg_match_all($regex, $text, $matches);
foreach ($matches[1] as $index => $old_token) {
if (isset($updates[$old_token])) {
$new_token = $updates[$old_token];
$text = str_replace("{$leading}{$old_token}{$trailing}", "[$new_token]", $text);
// Also replace any tokens that have a -raw suffix.
$text = str_replace("{$leading}{$old_token}-raw{$trailing}", "[$new_token]", $text);
}
}
return $text;
}
/**
* Get token problems.
*/
function token_get_token_problems() {
// @todo Improve the duplicate checking to report which modules are the offenders.
//$token_info = array();
//foreach (module_implements('token_info') as $module) {
// $module_token_info = module_invoke($module, 'token_info');
// if (in_array($module, _token_core_supported_modules())) {
// $module .= '/token';
// }
// if (isset($module_token_info['types'])) {
// if (is_array($module_token_info['types'])) {
// foreach (array_keys($module_token_info['types']) as $type) {
// if (is_array($module_token_info['types'][$type])) {
// $module_token_info['types'][$type] += array('module' => $module);
// }
// }
// }
// }
// if (isset($module_token_info['tokens'])) {
// if (is_array($module_token_info['tokens'])) {
//
// }
// }
// if (is_array($module_token_info)) {
// $token_info = array_merge_recursive($token_info, $module_token_info);
// }
//}
$token_info = \Drupal::token()->getInfo();
$token_problems = array(
'not-array' => array(
'label' => t('Tokens or token types not defined as arrays'),
'severity' => REQUIREMENT_ERROR,
),
'missing-info' => array(
'label' => t('Tokens or token types missing name property'),
'severity' => REQUIREMENT_WARNING,
),
'type-no-tokens' => array(
'label' => t('Token types do not have any tokens defined'),
'severity' => REQUIREMENT_INFO,
),
'tokens-no-type' => array(
'label' => t('Token types are not defined but have tokens'),
'severity' => REQUIREMENT_INFO,
),
'duplicate' => array(
'label' => t('Token or token types are defined by multiple modules'),
'severity' => REQUIREMENT_ERROR,
),
);
// Check token types for problems.
foreach ($token_info['types'] as $type => $type_info) {
$real_type = !empty($type_info['type']) ? $type_info['type'] : $type;
if (!is_array($type_info)) {
$token_problems['not-array']['problems'][] = "\$info['types']['$type']";
continue;
}
elseif (!isset($type_info['name'])) {
$token_problems['missing-info']['problems'][] = "\$info['types']['$type']";
}
elseif (is_array($type_info['name'])) {
$token_problems['duplicate']['problems'][] = "\$info['types']['$type']";
}
elseif (empty($token_info['tokens'][$real_type])) {
$token_problems['type-no-tokens']['problems'][] = "\$info['types']['$real_type']";
}
}
// Check tokens for problems.
foreach ($token_info['tokens'] as $type => $tokens) {
if (!is_array($tokens)) {
$token_problems['not-array']['problems'][] = "\$info['tokens']['$type']";
continue;
}
else {
foreach (array_keys($tokens) as $token) {
if (!is_array($tokens[$token])) {
$token_problems['not-array']['problems'][] = "\$info['tokens']['$type']['$token']";
continue;
}
elseif (!isset($tokens[$token]['name'])) {
$token_problems['missing-info']['problems'][] = "\$info['tokens']['$type']['$token']";
}
elseif (is_array($tokens[$token]['name'])) {
$token_problems['duplicate']['problems'][] = "\$info['tokens']['$type']['$token']";
}
}
}
if (!isset($token_info['types'][$type])) {
$token_problems['tokens-no-type']['problems'][] = "\$info['types']['$type']";
}
}
return $token_problems;
}

View file

@ -0,0 +1,25 @@
jquery.treeTable:
remote: 'http://plugins.jquery.com/treetable/'
version: 3.2.0
license:
name: MIT
url: https://github.com/ludo/jquery-treetable/blob/3.2.0/MIT-LICENSE.txt
gpl-compatible: true
js:
js/jquery.treetable.js: {}
css:
component:
css/jquery.treetable.css: {}
css/token.treetable.theme.css: {}
dependencies:
- core/jquery
token:
version: VERSION
js:
js/token.js: {}
css:
component:
css/token.css: {}
dependencies:
- core/jquery
- core/drupal

View file

@ -0,0 +1,3 @@
token.devel_entities:
class: \Drupal\Core\Menu\LocalTaskDefault
deriver: \Drupal\token\Plugin\Derivative\DevelLocalTask

View file

@ -0,0 +1,788 @@
<?php
/**
* @file
* Enhances the token API in core: adds a browseable UI, missing tokens, etc.
*/
use Drupal\Component\Render\PlainTextOutput;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Block\BlockPluginInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Menu\MenuLinkInterface;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\Core\Render\Element;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\TypedData\TranslatableInterface;
use Drupal\menu_link_content\Entity\MenuLinkContent;
use Drupal\menu_link_content\MenuLinkContentInterface;
use Drupal\node\Entity\Node;
use Drupal\node\NodeInterface;
/**
* Implements hook_help().
*/
function token_help($route_name, RouteMatchInterface $route_match) {
if ($route_name == 'help.page.token') {
$token_tree = \Drupal::service('token.tree_builder')->buildAllRenderable([
'click_insert' => FALSE,
'show_restricted' => TRUE,
'show_nested' => FALSE,
]);
$output = '<h3>' . t('About') . '</h3>';
$output .= '<p>' . t('The <a href=":project">Token</a> module provides a user interface for the site token system. It also adds some additional tokens that are used extensively during site development. Tokens are specially formatted chunks of text that serve as placeholders for a dynamically generated value. For more information, covering both the token system and the additional tools provided by the Token module, see the <a href=":online">online documentation</a>.', [':online' => 'https://www.drupal.org/documentation/modules/token', ':project' => 'https://www.drupal.org/project/token']) . '</p>';
$output .= '<h3>' . t('Uses') . '</h3>';
$output .= '<p>' . t('Your website uses a shared token system for exposing and using placeholder tokens and their appropriate replacement values. This allows for any module to provide placeholder tokens for strings without having to reinvent the wheel. It also ensures consistency in the syntax used for tokens, making the system as a whole easier for end users to use.') . '</p>';
$output .= '<dl>';
$output .= '<dt>' . t('The list of the currently available tokens on this site are shown below.') . '</dt>';
$output .= '<dd>' . \Drupal::service('renderer')->render($token_tree) . '</dd>';
$output .= '</dl>';
return $output;
}
}
/**
* Return an array of the core modules supported by token.module.
*/
function _token_core_supported_modules() {
return array('book', 'field', 'menu_ui');
}
/**
* Implements hook_theme().
*/
function token_theme() {
$info['token_tree_link'] = [
'variables' => [
'token_types' => [],
'global_types' => TRUE,
'click_insert' => TRUE,
'show_restricted' => FALSE,
'show_nested' => FALSE,
'recursion_limit' => 3,
'text' => NULL,
'options' => [],
],
'file' => 'token.pages.inc',
];
return $info;
}
/**
* Implements hook_block_view_alter().
*/
function token_block_view_alter(&$build, BlockPluginInterface $block) {
$label = $build['#configuration']['label'];
if ($label != '<none>') {
// The label is automatically escaped, avoid escaping it twice.
// @todo https://www.drupal.org/node/2580723 will add a method or option
// to the token API to do this, use that when available.
$bubbleable_metadata = BubbleableMetadata::createFromRenderArray($build);
$build['#configuration']['label'] = PlainTextOutput::renderFromHtml(\Drupal::token()->replace($label, [], [], $bubbleable_metadata));
$bubbleable_metadata->applyTo($build);
}
}
/**
* Implements hook_form_FORM_ID_alter().
*/
function token_form_block_form_alter(&$form, FormStateInterface $form_state) {
$token_tree = [
'#theme' => 'token_tree_link',
'#token_types' => [],
];
$rendered_token_tree = \Drupal::service('renderer')->render($token_tree);
$form['settings']['label']['#description'] =
t('This field supports tokens. @browse_tokens_link', ['@browse_tokens_link' => $rendered_token_tree])
;
$form['settings']['label']['#element_validate'][] = 'token_element_validate';
$form['settings']['label'] += ['#token_types' => []];
}
/**
* Implements hook_field_info_alter().
*/
function token_field_info_alter(&$info) {
$defaults = array(
'taxonomy_term_reference' => 'taxonomy_term_reference_plain',
'number_integer' => 'number_unformatted',
'number_decimal' => 'number_unformatted',
'number_float' => 'number_unformatted',
'file' => 'file_url_plain',
'image' => 'file_url_plain',
'text' => 'text_default',
'text_long' => 'text_default',
'text_with_summary' => 'text_default',
'list_integer' => 'list_default',
'list_float' => 'list_default',
'list_string' => 'list_default',
'list_boolean' => 'list_default',
);
foreach ($defaults as $field_type => $default_token_formatter) {
if (isset($info[$field_type])) {
$info[$field_type] += array('default_token_formatter' => $default_token_formatter);
}
}
}
/**
* Implements hook_date_format_insert().
*/
function token_date_format_insert() {
token_clear_cache();
}
/**
* Implements hook_date_format_delete().
*/
function token_date_format_delete() {
token_clear_cache();
}
/**
* Implements hook_field_storage_config_presave().
*/
function token_field_config_presave($instance) {
token_clear_cache();
}
/**
* Implements hook_field_storage_config_delete().
*/
function token_field_config_delete($instance) {
token_clear_cache();
}
/**
* Clear token caches and static variables.
*/
function token_clear_cache() {
\Drupal::token()->resetInfo();
\Drupal::service('token.entity_mapper')->resetInfo();
drupal_static_reset('token_menu_link_load_all_parents');
drupal_static_reset('token_book_link_load');
}
/**
* Implements hook_entity_type_alter().
*
* Because some token types to do not match their entity type names, we have to
* map them to the proper type. This is purely for other modules' benefit.
*
* @see \Drupal\token\TokenEntityMapperInterface::getEntityTypeMappings()
* @see http://drupal.org/node/737726
*/
function token_entity_type_alter(array &$entity_types) {
$devel_exists = \Drupal::moduleHandler()->moduleExists('devel');
/* @var $entity_types EntityTypeInterface[] */
foreach ($entity_types as $entity_type_id => $entity_type) {
if (!$entity_type->get('token_type')) {
// Fill in default token types for entities.
switch ($entity_type_id) {
case 'taxonomy_term':
case 'taxonomy_vocabulary':
// Stupid taxonomy token types...
$entity_type->set('token_type', str_replace('taxonomy_', '', $entity_type_id));
break;
default:
// By default the token type is the same as the entity type.
$entity_type->set('token_type', $entity_type_id);
break;
}
}
if ($devel_exists
&& $entity_type->hasViewBuilderClass()
&& ($canonical = $entity_type->getLinkTemplate('canonical'))
&& !$entity_type->hasLinkTemplate('token-devel')) {
$entity_type->setLinkTemplate('token-devel', $canonical . '/devel/token');
}
}
}
/**
* Implements hook_entity_view_modes_info().
*/
/**
* Implements hook_module_implements_alter().
*
* Adds missing token support for core modules.
*/
function token_module_implements_alter(&$implementations, $hook) {
module_load_include('inc', 'token', 'token.tokens');
if ($hook == 'tokens' || $hook == 'token_info' || $hook == 'token_info_alter' || $hook == 'tokens_alter') {
foreach (_token_core_supported_modules() as $module) {
if (\Drupal::moduleHandler()->moduleExists($module) && function_exists($module . '_' . $hook)) {
$implementations[$module] = TRUE;
}
}
// Move token.module to get included first since it is responsible for
// other modules.
if (isset($implementations['token'])) {
unset($implementations['token']);
$implementations = array_merge(array('token' => 'tokens'), $implementations);
}
}
}
/**
* Return the module responsible for a token.
*
* @param string $type
* The token type.
* @param string $name
* The token name.
*
* @return mixed
* The value of $info['tokens'][$type][$name]['module'] from token info, or
* NULL if the value does not exist.
*/
function _token_module($type, $name) {
$token_info = \Drupal::token()->getTokenInfo($type, $name);
return isset($token_info['module']) ? $token_info['module'] : NULL;
}
/**
* Validate a form element that should have tokens in it.
*
* Form elements that want to add this validation should have the #token_types
* parameter defined.
*
* For example:
* @code
* $form['my_node_text_element'] = array(
* '#type' => 'textfield',
* '#title' => t('Some text to token-ize that has a node context.'),
* '#default_value' => 'The title of this node is [node:title].',
* '#element_validate' => array('token_element_validate'),
* '#token_types' => array('node'),
* '#min_tokens' => 1,
* '#max_tokens' => 10,
* );
* @endcode
*/
function token_element_validate($element, FormStateInterface $form_state) {
$value = isset($element['#value']) ? $element['#value'] : $element['#default_value'];
if (!Unicode::strlen($value)) {
// Empty value needs no further validation since the element should depend
// on using the '#required' FAPI property.
return $element;
}
$tokens = \Drupal::token()->scan($value);
$title = empty($element['#title']) ? $element['#parents'][0] : $element['#title'];
// Validate if an element must have a minimum number of tokens.
if (isset($element['#min_tokens']) && count($tokens) < $element['#min_tokens']) {
$error = \Drupal::translation()->formatPlural($element['#min_tokens'], '%name must contain at least one token.', '%name must contain at least @count tokens.', array('%name' => $title));
$form_state->setError($element, $error);
}
// Validate if an element must have a maximum number of tokens.
if (isset($element['#max_tokens']) && count($tokens) > $element['#max_tokens']) {
$error = \Drupal::translation()->formatPlural($element['#max_tokens'], '%name must contain at most one token.', '%name must contain at most @count tokens.', array('%name' => $title));
$form_state->setError($element, $error);
}
// Check if the field defines specific token types.
if (isset($element['#token_types'])) {
$invalid_tokens = \Drupal::token()->getInvalidTokensByContext($tokens, $element['#token_types']);
if ($invalid_tokens) {
$form_state->setError($element, t('%name is using the following invalid tokens: @invalid-tokens.', array('%name' => $title, '@invalid-tokens' => implode(', ', $invalid_tokens))));
}
}
return $element;
}
/**
* Implements hook_form_FORM_ID_alter().
*/
function token_form_field_config_edit_form_alter(&$form, FormStateInterface $form_state) {
$field_config = $form_state->getFormObject()->getEntity();
$field_storage = $field_config->getFieldStorageDefinition();
if ($field_storage->isLocked()) {
return;
}
$field_type = $field_storage->getType();
if (($field_type == 'file' || $field_type == 'image') && isset($form['settings']['file_directory'])) {
// GAH! We can only support global tokens in the upload file directory path.
$form['settings']['file_directory']['#element_validate'][] = 'token_element_validate';
// Date support needs to be implicitly added, as while technically it's not
// a global token, it is a not only used but is the default value.
// https://www.drupal.org/node/2642160
$form['settings']['file_directory'] += array('#token_types' => array('date'));
$form['settings']['file_directory']['#description'] .= ' ' . t('This field supports tokens.');
}
// Note that the description is tokenized via token_field_widget_form_alter().
$form['description']['#element_validate'][] = 'token_element_validate';
$form['description'] += array('#token_types' => array());
$form['token_tree'] = array(
'#theme' => 'token_tree_link',
'#token_types' => array(),
'#weight' => $form['description']['#weight'] + 0.5,
);
}
/**
* Implements hook_form_BASE_FORM_ID_alter().
*
* Alters the configure action form to add token context validation and
* adds the token tree for a better token UI and selection.
*/
function token_form_action_form_alter(&$form, $form_state) {
switch ($form['plugin']['#value']) {
case 'action_message_action':
case 'action_send_email_action':
case 'action_goto_action':
$form['token_tree'] = [
'#theme' => 'token_tree_link',
'#token_types' => 'all',
'#weight' => 100,
];
$form['actions']['#weight'] = 101;
// @todo Add token validation to the action fields that can use tokens.
break;
}
}
/**
* Implements hook_form_FORM_ID_alter().
*
* Alters the user e-mail fields to add token context validation and
* adds the token tree for a better token UI and selection.
*/
function token_form_user_admin_settings_alter(&$form, FormStateInterface $form_state) {
$email_token_help = t('Available variables are: [site:name], [site:url], [user:display-name], [user:account-name], [user:mail], [site:login-url], [site:url-brief], [user:edit-url], [user:one-time-login-url], [user:cancel-url].');
foreach (Element::children($form) as $key) {
$element = &$form[$key];
// Remove the crummy default token help text.
if (!empty($element['#description'])) {
$element['#description'] = trim(str_replace($email_token_help, t('The list of available tokens that can be used in e-mails is provided below.'), $element['#description']));
}
switch ($key) {
case 'email_admin_created':
case 'email_pending_approval':
case 'email_no_approval_required':
case 'email_password_reset':
case 'email_cancel_confirm':
// Do nothing, but allow execution to continue.
break;
case 'email_activated':
case 'email_blocked':
case 'email_canceled':
// These fieldsets have their e-mail elements inside a 'settings'
// sub-element, so switch to that element instead.
$element = &$form[$key]['settings'];
break;
default:
continue 2;
}
foreach (Element::children($element) as $sub_key) {
if (!isset($element[$sub_key]['#type'])) {
continue;
}
elseif ($element[$sub_key]['#type'] == 'textfield' && substr($sub_key, -8) === '_subject') {
// Add validation to subject textfields.
$element[$sub_key]['#element_validate'][] = 'token_element_validate';
$element[$sub_key] += array('#token_types' => array('user'));
}
elseif ($element[$sub_key]['#type'] == 'textarea' && substr($sub_key, -5) === '_body') {
// Add validation to body textareas.
$element[$sub_key]['#element_validate'][] = 'token_element_validate';
$element[$sub_key] += array('#token_types' => array('user'));
}
}
}
// Add the token tree UI.
$form['email']['token_tree'] = array(
'#theme' => 'token_tree_link',
'#token_types' => array('user'),
'#show_restricted' => TRUE,
'#show_nested' => FALSE,
'#weight' => 90,
);
}
/**
* Prepare a string for use as a valid token name.
*
* @param $name
* The token name to clean.
* @return
* The cleaned token name.
*/
function token_clean_token_name($name) {
static $names = array();
if (!isset($names[$name])) {
$cleaned_name = strtr($name, array(' ' => '-', '_' => '-', '/' => '-', '[' => '-', ']' => ''));
$cleaned_name = preg_replace('/[^\w\-]/i', '', $cleaned_name);
$cleaned_name = trim($cleaned_name, '-');
$names[$name] = $cleaned_name;
}
return $names[$name];
}
/**
* Do not use this function yet. Its API has not been finalized.
*/
function token_render_array(array $array, array $options = array()) {
$rendered = array();
/** @var \Drupal\Core\Render\RendererInterface $renderer */
$renderer = \Drupal::service('renderer');
foreach (token_element_children($array) as $key) {
$value = $array[$key];
$rendered[] = is_array($value) ? $renderer->renderPlain($value) : (string) $value;
}
$join = isset($options['join']) ? $options['join'] : ', ';
return implode($join, $rendered);
}
/**
* Do not use this function yet. Its API has not been finalized.
*/
function token_render_array_value($value, array $options = array()) {
/** @var \Drupal\Core\Render\RendererInterface $renderer */
$renderer = \Drupal::service('renderer');
$rendered = is_array($value) ? $renderer->renderPlain($value) : (string) $value;
return $rendered;
}
/**
* Copy of drupal_render_cache_get() that does not care about request method.
*/
function token_render_cache_get($elements) {
if (!$cid = drupal_render_cid_create($elements)) {
return FALSE;
}
$bin = isset($elements['#cache']['bin']) ? $elements['#cache']['bin'] : 'render';
if (!empty($cid) && $cache = \Drupal::cache($bin)->get($cid)) {
// Add additional libraries, JavaScript, CSS and other data attached
// to this element.
if (isset($cache->data['#attached'])) {
drupal_process_attached($cache->data);
}
// Return the rendered output.
return $cache->data['#markup'];
}
return FALSE;
}
/**
* Coyp of drupal_render_cache_set() that does not care about request method.
*/
function token_render_cache_set(&$markup, $elements) {
// This should only run of drupal_render_cache_set() did not.
if (in_array(\Drupal::request()->server->get('REQUEST_METHOD'), array('GET', 'HEAD'))) {
return FALSE;
}
$original_method = \Drupal::request()->server->get('REQUEST_METHOD');
\Drupal::request()->server->set('REQUEST_METHOD', 'GET');
drupal_render_cache_set($markup, $elements);
\Drupal::request()->server->set('REQUEST_METHOD', $original_method);
}
/**
* Loads menu link titles for all purents of a menu link plugin ID.
*
* @param string $plugin_id
* The menu link plugin ID.
* @param string $langcode
* The language code.
*
* @return string[]
* List of menu link parent titles.
*/
function token_menu_link_load_all_parents($plugin_id, $langcode) {
$cache = &drupal_static(__FUNCTION__, array());
if (!isset($cache[$plugin_id][$langcode])) {
$cache[$plugin_id][$langcode] = array();
/** @var \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager */
$menu_link_manager = \Drupal::service('plugin.manager.menu.link');
$parent_ids = $menu_link_manager->getParentIds($plugin_id);
// Remove the current plugin ID from the parents.
unset($parent_ids[$plugin_id]);
foreach ($parent_ids as $parent_id) {
$parent = $menu_link_manager->createInstance($parent_id);
$cache[$plugin_id][$langcode] = array($parent_id => token_menu_link_translated_title($parent, $langcode)) + $cache[$plugin_id][$langcode];
}
}
return $cache[$plugin_id][$langcode];
}
/**
* Returns the translated link of a menu title.
*
* If the underlying entity is a content menu item, load it to get the
* translated menu item title.
*
* @todo Remove this when there is a better way to get a translated menu
* item title in core: https://www.drupal.org/node/2795143
*
* @param \Drupal\Core\Menu\MenuLinkInterface $menu_link
* The menu link.
* @param string|null $langcode
* (optional) The langcode, defaults to the current language.
*
* @return string
* The menu link title.
*/
function token_menu_link_translated_title(MenuLinkInterface $menu_link, $langcode = NULL) {
$metadata = $menu_link->getMetaData();
if (isset($metadata['entity_id']) && $menu_link->getProvider() == 'menu_link_content') {
/** @var \Drupal\menu_link_content\MenuLinkContentInterface $entity */
$entity = \Drupal::entityTypeManager()->getStorage('menu_link_content')->load($metadata['entity_id']);
$entity = \Drupal::service('entity.repository')->getTranslationFromContext($entity, $langcode);
return $entity->getTitle();
}
return $menu_link->getTitle();
}
/**
* Loads all the parents of the term in the specified language.
*
* @param int $tid
* The term id.
* @param string $langcode
* The language code.
*
* @return string[]
* The term parents collection.
*/
function token_taxonomy_term_load_all_parents($tid, $langcode) {
$cache = &drupal_static(__FUNCTION__, array());
if (!is_numeric($tid)) {
return array();
}
if (!isset($cache[$langcode][$tid])) {
$cache[$langcode][$tid] = array();
/** @var \Drupal\taxonomy\TermStorageInterface $term_storage */
$term_storage = \Drupal::entityTypeManager()->getStorage('taxonomy_term');
$parents = $term_storage->loadAllParents($tid);
// Remove this term from the array.
array_shift($parents);
$parents = array_reverse($parents);
foreach ($parents as $term) {
$translation = \Drupal::service('entity.repository')->getTranslationFromContext($term, $langcode);
$cache[$langcode][$tid][$term->id()] = $translation->label();
}
}
return $cache[$langcode][$tid];
}
function token_element_children(&$elements, $sort = FALSE) {
// Do not attempt to sort elements which have already been sorted.
$sort = isset($elements['#sorted']) ? !$elements['#sorted'] : $sort;
// Filter out properties from the element, leaving only children.
$children = array();
$sortable = FALSE;
foreach ($elements as $key => $value) {
if ($key === '' || $key[0] !== '#') {
$children[$key] = $value;
if (is_array($value) && isset($value['#weight'])) {
$sortable = TRUE;
}
}
}
// Sort the children if necessary.
if ($sort && $sortable) {
uasort($children, 'Drupal\Component\Utility\SortArray::sortByWeightProperty');
// Put the sorted children back into $elements in the correct order, to
// preserve sorting if the same element is passed through
// element_children() twice.
foreach ($children as $key => $child) {
unset($elements[$key]);
$elements[$key] = $child;
}
$elements['#sorted'] = TRUE;
}
return array_keys($children);
}
/**
* Loads all the parents of the book page.
*
* @param array $book
* The book data. The 'nid' key points to the current page of the book.
* The 'p1' ... 'p9' keys point to parents of the page, if they exist, with 'p1'
* pointing to the book itself and the last defined pX to the current page.
*
* @return string[]
* List of node titles of the book parents.
*/
function token_book_load_all_parents(array $book) {
$cache = &drupal_static(__FUNCTION__, array());
if (empty($book['nid'])) {
return array();
}
$nid = $book['nid'];
if (!isset($cache[$nid])) {
$cache[$nid] = array();
$i = 1;
while ($book["p$i"] != $nid) {
$cache[$nid][] = Node::load($book["p$i"])->getTitle();
$i++;
}
}
return $cache[$nid];
}
/**
* Implements hook_entity_base_field_info().
*/
function token_entity_base_field_info(EntityTypeInterface $entity_type) {
// We add a psuedo entity-reference field to track the menu entry created
// from the node add/edit form so that tokens generated at that time that
// reference the menu link can access the yet to be saved menu link.
// @todo Revisit when https://www.drupal.org/node/2315773 is resolved.
if ($entity_type->id() === 'node' && \Drupal::moduleHandler()->moduleExists('menu_ui')) {
$fields['menu_link'] = BaseFieldDefinition::create('entity_reference')
->setLabel(t('Menu link'))
->setDescription(t('Computed menu link for the node (only available during node saving).'))
->setRevisionable(TRUE)
->setSetting('target_type', 'menu_link_content')
->setTranslatable(TRUE)
->setDisplayOptions('view', array(
'label' => 'hidden',
'type' => 'hidden',
))
->setComputed(TRUE)
->setDisplayOptions('form', array(
'type' => 'hidden',
));
return $fields;
}
return [];
}
/**
* Implements hook_form_BASE_FORM_ID_alter() for node_form.
*
* Populates menu_link field on nodes from the menu item on unsaved nodes.
*
* @see menu_ui_form_node_form_submit()
* @see token_entity_base_field_info()
*/
function token_form_node_form_alter(&$form, FormStateInterface $form_state) {
if (!\Drupal::moduleHandler()->moduleExists('menu_ui')) {
return;
}
/** @var \Drupal\node\NodeForm $form_object */
if (!\Drupal::currentUser()->hasPermission('administer menu')) {
// We're only interested in when the node is unsaved and the editor has
// permission to create new menu links.
return;
}
$form['#entity_builders'][] = 'token_node_menu_link_submit';
}
/**
* Entity builder.
*/
function token_node_menu_link_submit($entity_type, NodeInterface $node, &$form, FormStateInterface $form_state) {
// Entity builders run twice, once during validation and again during
// submission, so we only run this code after validation has been performed.
if (!$form_state->isValueEmpty('menu') && $form_state->getTemporaryValue('entity_validated')) {
$values = $form_state->getValue('menu');
if (!empty($values['enabled']) && trim($values['title'])) {
if (!empty($values['menu_parent'])) {
list($menu_name, $parent) = explode(':', $values['menu_parent'], 2);
$values['menu_name'] = $menu_name;
$values['parent'] = $parent;
}
// Construct an unsaved entity.
if ($entity_id = $form_state->getValue(['menu', 'entity_id'])) {
// Use the existing menu_link_content entity.
$entity = MenuLinkContent::load($entity_id);
// If the loaded MenuLinkContent doesn't have a translation for the
// Node's active langcode, create a new translation.
if ($entity->isTranslatable()) {
if (!$entity->hasTranslation($node->language()->getId())) {
$entity = $entity->addTranslation($node->language()->getId(), $entity->toArray());
}
else {
$entity = $entity->getTranslation($node->language()->getId());
}
}
}
else {
if ($node->isNew()) {
// Create a new menu_link_content entity.
$entity = MenuLinkContent::create(array(
// Lets just reference the UUID for now, the link is not important for
// token generation.
'link' => ['uri' => 'internal:/node/' . $node->uuid()],
'langcode' => $node->language()->getId(),
));
}
else {
// Create a new menu_link_content entity.
$entity = MenuLinkContent::create(array(
'link' => ['uri' => 'entity:node/' . $node->id()],
'langcode' => $node->language()->getId(),
));
}
}
$entity->title->value = trim($values['title']);
$entity->description->value = trim($values['description']);
$entity->menu_name->value = $values['menu_name'];
$entity->parent->value = $values['parent'];
$entity->weight->value = isset($values['weight']) ? $values['weight'] : 0;
$entity->save();
$node->menu_link = $entity;
// Leave this for _menu_ui_node_save() to pick up so we don't end up with
// duplicate menu-links.
$form_state->setValue(['menu', 'entity_id'], $entity->id());
}
}
}
/**
* Implements hook_ENTITY_TYPE_insert for node entities.
*/
function token_node_insert(NodeInterface $node) {
if ($node->hasField('menu_link') && $menu_link = $node->menu_link->entity) {
// Update the menu-link to point to the now saved node.
$menu_link->link = 'entity:node/' . $node->id();
$menu_link->save();
}
}
/**
* Implements hook_ENTITY_TYPE_presave() for menu_link_content.
*/
function token_menu_link_content_presave(MenuLinkContentInterface $menu_link_content) {
drupal_static_reset('token_menu_link_load_all_parents');
}

View file

@ -0,0 +1,61 @@
<?php
/**
* @file
* User page callbacks for the token module.
*/
use Drupal\Component\Serialization\Json;
use Drupal\Component\Utility\DiffArray;
use Drupal\Core\Link;
use Drupal\Core\Url;
/**
* Theme a link to a token tree shown as a dialog.
*/
function template_preprocess_token_tree_link(&$variables) {
if (empty($variables['text'])) {
$variables['text'] = t('Browse available tokens.');
}
$variables['#attached']['library'][] = 'core/drupal.dialog.ajax';
$variables['#attached']['library'][] = 'token/token';
$variables['options']['attributes']['class'][] = 'token-dialog';
$variables['options']['attributes']['class'][] = 'use-ajax';
$tree_variables = [
'token_types' => [],
'global_types' => TRUE,
'click_insert' => TRUE,
'show_restricted' => FALSE,
'show_nested' => FALSE,
'recursion_limit' => 3,
];
$query_options = array_intersect_key($variables, $tree_variables);
$query_options = DiffArray::diffAssocRecursive($query_options, $tree_variables);
if (!isset($variables['options']['query']['options'])) {
$variables['options']['query']['options'] = [];
}
$variables['options']['query']['options'] += $query_options;
// Because PHP converts query strings with arrays into a different syntax on
// the next request, the options have to be encoded with JSON in the query
// string so that we can reliably decode it for token comparison.
$variables['options']['query']['options'] = Json::encode($variables['options']['query']['options']);
// Set the token tree to open in a separate window.
$variables['options']['attributes'] += [
'data-dialog-type' => 'dialog',
'data-dialog-options' => json_encode([
'dialogClass' => 'token-tree-dialog',
'width' => 600,
'height' => 400,
'position' => ['my' => 'right bottom', 'at' => 'right-10 bottom-10'],
'draggable' => TRUE,
'autoResize' => FALSE,
]),
];
$variables['link'] = Link::createFromRoute($variables['text'], 'token.tree', [], $variables['options'])->toRenderable();
$variables['url'] = new Url('token.tree', [], $variables['options']);
$variables['attributes'] = $variables['options']['attributes'];
}

View file

@ -0,0 +1,22 @@
token.tree:
path: '/token/tree'
defaults:
_controller: '\Drupal\token\Controller\TokenTreeController::outputTree'
requirements:
_csrf_token: 'TRUE'
token.autocomplete:
path: '/token/autocomplete/{token_type}/{filter}'
defaults:
_controller: '\Drupal\token\Controller\TokenAutocompleteController::autocomplete'
requirements:
_access: 'TRUE'
token.flush_cache:
path: '/token/flush-cache'
defaults:
_controller: '\Drupal\token\Controller\TokenCacheController::flush'
requirements:
_permission: 'flush caches'
_csrf_token: 'TRUE'
_module_dependencies: 'admin_menu'

View file

@ -0,0 +1,12 @@
services:
token.entity_mapper:
class: Drupal\token\TokenEntityMapper
arguments: ['@entity_type.manager', '@module_handler']
token.tree_builder:
class: Drupal\token\TreeBuilder
arguments: ['@token', '@token.entity_mapper', '@cache.data', '@language_manager']
token.route_subscriber:
class: Drupal\token\Routing\RouteSubscriber
arguments: ['@entity_type.manager', '@module_handler']
tags:
- { name: event_subscriber }

File diff suppressed because it is too large Load diff