Compare commits

...
46 Commits
Author SHA1 Message Date
bachir 86573ebbe9 mignified assets for prod 2018-04-18 00:12:45 +02:00
bachir e31f0c34a7 moved logo to prefectly superpose with player timeline 2018-04-18 00:10:37 +02:00
bachir 452f41108b removed stopwords filter from searchapi index for better results 2018-04-17 23:58:07 +02:00
bachir d62d02201b added current language condition to searchapi query 2018-04-17 23:49:38 +02:00
bachir cfe15cebd8 forgot to remove debugger; 2018-04-17 23:12:34 +02:00
bachir 8133aa66f0 search bug fix with language field 2018-04-17 23:07:39 +02:00
bachir 94175c1df1 added autocomplete with documents title in search form 2018-04-17 23:00:18 +02:00
bachir 82d5409957 mignified assets for prod 2018-04-17 16:24:35 +02:00
bachir 07728aa425 shuffle player only on acive items in the map (entree, article, search results 2018-04-17 16:21:46 +02:00
bachir 29ad08648f auto load article on audio open if map's articl filter is on 2018-04-17 15:59:51 +02:00
bachir a3a4e3922c corpus carre vide et plus petit
hover audio link -> node_popup on map
index sort title
corpus map : white safety zone arround squares
aside squares not collisioning
square faded not interactive (article filtre)
node_popup show chutier icon only if action not add
fade not active entries in entrees block
closing notice|index doesn't close entree on block
and many little things
2018-04-17 15:07:57 +02:00
bachir 76ff40f8bb updated linkit contrib module to last beta version 2018-04-17 14:53:21 +02:00
bachir c0fbcb7706 added piwik contrib module 2018-04-16 22:25:17 +02:00
bachir 7e073fdfa0 js bug fix 2018-04-15 22:44:33 +02:00
bachir 1d0a95f022 js buf fix 2018-04-15 22:30:10 +02:00
bachir 9570ee1823 mignified assets for prod 2018-04-15 21:46:49 +02:00
bachir c5ae3d6b00 display imporovement (fonts) + agenda nav + prod linked events nav 2018-04-15 21:45:13 +02:00
bachir 4967111a0e added aside (related dates for prod) to edlp_ajax controller 2018-04-15 17:11:54 +02:00
bachir e27d1f1e5a fix custom module package 2018-04-15 15:20:05 +02:00
bachir df54d07169 created css text filter to clean all style attributes in content 2018-04-15 15:19:19 +02:00
bachir efca592b17 refactored edlp_history redirection system, incorporated audio links in it 2018-04-15 12:54:27 +02:00
bachir 95cd2c15cf css fix 2018-04-15 00:16:23 +02:00
bachir 00166e7917 mignified assets for prod 2018-04-15 00:04:04 +02:00
bachir ea435b7517 enregistrement transcription interface is ok 2018-04-15 00:00:40 +02:00
bachir 6d88ca7c8d audio cartel ids now complete 2018-04-14 22:47:25 +02:00
bachir d4087b2924 bug fix 2018-04-14 18:42:44 +02:00
bachir a01d52e62c mignified assets for prod 2018-04-14 16:16:18 +02:00
bachir 252149bfdc language siwtchers links are working now (reloading the whole site but with the right url) 2018-04-14 16:15:11 +02:00
bachir cb2ae7ee35 improved translation status display on admin views 2018-04-14 10:24:24 +02:00
Bachir Soussi Chiadmi 10f68b951d started to activate translations ajax nav 2018-04-13 19:23:02 +02:00
Bachir Soussi Chiadmi 0d555a596e mignified assets for prod 2018-04-12 23:56:16 +02:00
Bachir Soussi Chiadmi d485286289 fixed cloing entree history nav 2018-04-12 23:54:45 +02:00
Bachir Soussi Chiadmi 97dd8cdf97 refactored corpus map scrambling and setaside using ghost origin particule position as attracter 2018-04-12 23:37:49 +02:00
Bachir Soussi Chiadmi 97ec3584d8 fix footer menu white-sapce:nowrap 2018-04-12 22:43:17 +02:00
Bachir Soussi Chiadmi e9a00903a9 better close modal btn position sticky, don't backhome if production modal closed 2018-04-12 22:36:23 +02:00
Bachir Soussi Chiadmi 9c6fe22012 fixed close modal btn hidden when scrolling 2018-04-12 22:19:29 +02:00
Bachir Soussi Chiadmi 741849397e added link action create genre and langue 2018-04-12 16:07:56 +02:00
Bachir Soussi Chiadmi 679e1f09ad fixed admin views tabs name 2018-04-12 15:22:57 +02:00
Bachir Soussi Chiadmi 162ab2b949 improved taxo damin view 2018-04-11 19:59:36 +02:00
Bachir Soussi Chiadmi c0b6dc6b75 a commit for testing deployement 2018-04-11 18:05:51 +02:00
Bachir Soussi Chiadmi 848f03f4a4 minified assets for prod 2018-04-11 18:01:06 +02:00
Bachir Soussi Chiadmi ac821854ce js cleaning 2018-04-11 17:57:17 +02:00
Bachir Soussi Chiadmi 1ae73c1c12 added synonyms to search autocomplete and results, improved collection admin view 2018-04-11 17:55:57 +02:00
Bachir Soussi Chiadmi 07a138cfe6 admin view for taxo genres and langues, translation for taxo genres and langues, synonyms for genres 2018-04-11 16:23:58 +02:00
Bachir Soussi Chiadmi 1d3a3dd6fd included entree and entree#notice entree#index in ajax history nav 2018-04-11 14:36:24 +02:00
Bachir Soussi Chiadmi b33159e3d0 fixed entries translation bug 2018-04-11 12:03:30 +02:00
451 changed files with 33578 additions and 8635 deletions
@@ -0,0 +1,5 @@
{
"globals": {
"_paq": true
}
}
@@ -0,0 +1,339 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.
@@ -0,0 +1,109 @@
Module: Piwik - Web analytics
Author: Alexander Hass <http://drupal.org/user/85918>
Description
===========
Adds the Piwik tracking system to your website.
Requirements
============
* Piwik installation
* Piwik website ID
Installation
============
* Copy the 'piwik' module directory in to your Drupal 'modules'
directory as usual.
Usage
=====
In the settings page enter your Piwik website ID.
All pages will now have the required JavaScript added to the
HTML footer can confirm this by viewing the page source from
your browser.
Page specific tracking
====================================================
The default is set to "Add to every page except the listed pages". By
default the following pages are listed for exclusion:
/admin
/admin/*
/batch
/node/add*
/node/*/*
/user/*/*
These defaults are changeable by the website administrator or any other
user with 'Administer Piwik' permission.
Like the blocks visibility settings in Drupal core, there is a choice for
"Add if the following PHP code returns TRUE." Sample PHP snippets that can be
used in this textarea can be found on the handbook page "Overview-approach to
block visibility" at http://drupal.org/node/64135.
Custom variables
=================
One example for custom variables tracking is the "User roles" tracking. Enter
the below configuration data into the custom variables settings form under
admin/config/system/piwik.
Slot: 1
Name: User roles
Value: [current-user:piwik-role-names]
Scope: Visitor
Slot: 1
Name: User ids
Value: [current-user:piwik-role-ids]
Scope: Visitor
More details about custom variables can be found in the Piwik API documentation
at http://piwik.org/docs/javascript-tracking/#toc-custom-variables.
Advanced Settings
=================
You can include additional JavaScript snippets in the custom javascript
code textarea. These can be found on various blog posts, or on the
official Piwik pages. Support is not provided for any customisations
you include.
To speed up page loading you may also cache the Piwik "piwik.js"
file locally.
Known issues
============
Drupal requirements (http://drupal.org/requirements) tell you to configure
PHP with "session.save_handler = user", but your Piwik installation may
not work with this configuration and gives you a server error 500.
1. You are able to workaround with the PHP default in your php.ini:
[Session]
session.save_handler = files
2. With Apache you may overwrite the PHP setting for the Piwik directory only.
If Piwik is installed in /piwik you are able to create a .htaccess file in
this directory with the below code:
# PHP 4, Apache 1.
<IfModule mod_php4.c>
php_value session.save_handler files
</IfModule>
# PHP 4, Apache 2.
<IfModule sapi_apache2.c>
php_value session.save_handler files
</IfModule>
# PHP 5, Apache 1 and 2.
<IfModule mod_php5.c>
php_value session.save_handler files
</IfModule>
@@ -0,0 +1,22 @@
{
"name": "drupal/piwik",
"description": "Adds Piwik javascript tracking code to all your site's pages",
"type": "drupal-module",
"homepage": "https://www.drupal.org/project/piwik",
"authors": [
{
"name": "hass",
"homepage": "https://www.drupal.org/u/hass"
},
{
"name": "See other contributors",
"homepage":"https://www.drupal.org/node/247808/committers"
}
],
"support": {
"issues": "https://www.drupal.org/project/issues/piwik",
"source": "http://git.drupal.org/project/piwik.git"
},
"license": "GPL-2.0+",
"require": {}
}
@@ -0,0 +1,29 @@
site_id: ''
url_http: ''
url_https: ''
domain_mode: 0
visibility:
request_path_mode: 0
request_path_pages: "/admin\n/admin/*\n/batch\n/node/add*\n/node/*/*\n/user/*/*"
user_role_mode: 0
user_role_roles: { }
user_account_mode: 0
track:
mailto: true
files: true
files_extensions: '7z|aac|arc|arj|asf|asx|avi|bin|csv|doc(x|m)?|dot(x|m)?|exe|flv|gif|gz|gzip|hqx|jar|jpe?g|js|mp(2|3|4|e?g)|mov(ie)?|msi|msp|pdf|phps|png|ppt(x|m)?|pot(x|m)?|pps(x|m)?|ppam|sld(x|m)?|thmx|qtm?|ra(m|r)?|sea|sit|tar|tgz|torrent|txt|wav|wma|wmv|wpd|xls(x|m|b)?|xlt(x|m)|xlam|xml|z|zip'
colorbox: true
userid: false
messages: { }
site_search: false
privacy:
donottrack: true
custom:
variable: { }
codesnippet:
before: ''
after: ''
translation_set: false
cache: false
page_title_hierarchy: false
page_title_hierarchy_exclude_home: true
@@ -0,0 +1,120 @@
# Schema for the configuration files of the piwik module.
piwik.settings:
type: config_object
label: 'Piwik settings'
mapping:
site_id:
type: string
label: 'Piwik site ID'
url_http:
type: string
label: 'Piwik HTTP URL'
url_https:
type: string
label: 'Piwik HTTPS URL'
domain_mode:
type: integer
label: 'What are you tracking?'
visibility:
type: mapping
label: 'Visibility'
mapping:
request_path_mode:
type: integer
label: 'Add tracking to specific pages'
request_path_pages:
type: string
label: 'Pages by their paths'
user_role_mode:
type: integer
label: 'Add tracking for specific roles'
user_role_roles:
type: sequence
label: 'Roles'
sequence:
type: string
label: 'Role'
user_account_mode:
type: integer
label: 'Allow users to customize tracking on their account page'
track:
type: mapping
label: 'Links and downloads'
mapping:
mailto:
type: boolean
label: 'Track clicks on mailto links'
files:
type: boolean
label: 'Track downloads'
files_extensions:
type: string
label: 'File extension list'
colorbox:
type: boolean
label: 'Track content in colorbox modal dialogs'
userid:
type: boolean
label: 'Track User ID'
messages:
type: sequence
label: 'Track messages of type'
sequence:
type: string
label: 'Message type'
site_search:
type: boolean
label: 'Track internal search'
privacy:
type: mapping
label: 'Privacy'
mapping:
donottrack:
type: boolean
label: 'Universal web tracking opt-out'
custom:
type: mapping
label: 'Custom variables'
mapping:
variable:
type: sequence
label: 'Custom variable'
sequence:
type: mapping
label: 'Variable'
mapping:
slot:
type: integer
label: 'Slot number'
name:
type: string
label: 'The custom variable name.'
value:
type: string
label: 'The custom variable value.'
scope:
type: string
label: 'The scope for the custom variable.'
codesnippet:
type: mapping
label: 'Custom code snippets'
mapping:
before:
type: string
label: 'Code snippet (before)'
after:
type: string
label: 'Code snippet (after)'
translation_set:
type: boolean
label: 'Track translation sets as one unit'
cache:
type: boolean
label: 'Locally cache tracking code file'
page_title_hierarchy:
type: boolean
label: 'Show page titles as hierarchy like breadcrumbs'
page_title_hierarchy_exclude_home:
type: boolean
label: 'Hide home page from hierarchy'
@@ -0,0 +1,127 @@
/**
* @file
* Piwik admin behaviors.
*/
(function ($, window) {
"use strict";
/**
* Provide the summary information for the tracking settings vertical tabs.
*/
Drupal.behaviors.trackingSettingsSummary = {
attach: function () {
// Make sure this behavior is processed only if drupalSetSummary is defined.
if (typeof jQuery.fn.drupalSetSummary === 'undefined') {
return;
}
$('#edit-page-visibility-settings').drupalSetSummary(function (context) {
var $radio = $('input[name="piwik_visibility_request_path_mode"]:checked', context);
if ($radio.val() === '0') {
if (!$('textarea[name="piwik_visibility_request_path_pages"]', context).val()) {
return Drupal.t('Not restricted');
}
else {
return Drupal.t('All pages with exceptions');
}
}
else {
return Drupal.t('Restricted to certain pages');
}
});
$('#edit-role-visibility-settings').drupalSetSummary(function (context) {
var vals = [];
$('input[type="checkbox"]:checked', context).each(function () {
vals.push($.trim($(this).next('label').text()));
});
if (!vals.length) {
return Drupal.t('Not restricted');
}
else if ($('input[name="piwik_visibility_user_role_mode"]:checked', context).val() === '1') {
return Drupal.t('Excepted: @roles', {'@roles': vals.join(', ')});
}
else {
return vals.join(', ');
}
});
$('#edit-user-visibility-settings').drupalSetSummary(function (context) {
var $radio = $('input[name="piwik_visibility_user_account_mode"]:checked', context);
if ($radio.val() === '0') {
return Drupal.t('Not customizable');
}
else if ($radio.val() === '1') {
return Drupal.t('On by default with opt out');
}
else {
return Drupal.t('Off by default with opt in');
}
});
$('#edit-linktracking').drupalSetSummary(function (context) {
var vals = [];
if ($('input#edit-piwik-trackmailto', context).is(':checked')) {
vals.push(Drupal.t('Mailto links'));
}
if ($('input#edit-piwik-trackfiles', context).is(':checked')) {
vals.push(Drupal.t('Outbound links'));
vals.push(Drupal.t('Downloads'));
}
if ($('input#edit-piwik-trackcolorbox', context).is(':checked')) {
vals.push(Drupal.t('Colorbox'));
}
if (!vals.length) {
return Drupal.t('Not tracked');
}
return Drupal.t('@items enabled', {'@items': vals.join(', ')});
});
$('#edit-messagetracking').drupalSetSummary(function (context) {
var vals = [];
$('input[type="checkbox"]:checked', context).each(function () {
vals.push($.trim($(this).next('label').text()));
});
if (!vals.length) {
return Drupal.t('Not tracked');
}
return Drupal.t('@items enabled', {'@items': vals.join(', ')});
});
$('#edit-search').drupalSetSummary(function (context) {
var vals = [];
if ($('input#edit-piwik-site-search', context).is(':checked')) {
vals.push(Drupal.t('Site search'));
}
if (!vals.length) {
return Drupal.t('Not tracked');
}
return Drupal.t('@items enabled', {'@items': vals.join(', ')});
});
$('#edit-domain-tracking').drupalSetSummary(function (context) {
var $radio = $('input[name="piwik_domain_mode"]:checked', context);
if ($radio.val() === '0') {
return Drupal.t('A single domain');
}
else if ($radio.val() === '1') {
return Drupal.t('One domain with multiple subdomains');
}
});
$('#edit-privacy').drupalSetSummary(function (context) {
var vals = [];
if ($('input#edit-piwik-privacy-donottrack', context).is(':checked')) {
vals.push(Drupal.t('Universal web tracking opt-out'));
}
if (!vals.length) {
return Drupal.t('No privacy');
}
return Drupal.t('@items enabled', {'@items': vals.join(', ')});
});
}
};
})(jQuery, window);
@@ -0,0 +1,41 @@
/**
* @file
* Attaches several event listener to a web page.
*/
(function ($, drupalSettings) {
"use strict";
$(document).ready(function () {
// Attach mousedown, keyup, touchstart events to document only and catch
// clicks on all elements.
$(document.body).bind("mousedown keyup touchstart", function (event) {
// Catch the closest surrounding link of a clicked element.
$(event.target).closest("a,area").each(function () {
if (drupalSettings.piwik.trackMailto && $(this).is("a[href^='mailto:'],area[href^='mailto:']")) {
// Mailto link clicked.
_paq.push(["trackEvent", "Mails", "Click", this.href.substring(7)]);
}
});
});
// Colorbox: This event triggers when the transition has completed and the
// newly loaded content has been revealed.
if (drupalSettings.piwik.trackColorbox) {
$(document).bind("cbox_complete", function () {
var href = $.colorbox.element().attr("href");
if (href) {
_paq.push(["setCustomUrl", href]);
_paq.push(["trackPageView"]);
}
});
}
});
})(jQuery, drupalSettings);
@@ -0,0 +1,68 @@
id: d6_piwik_settings
label: Piwik 6 configuration
migration_tags:
- Drupal 6
source:
plugin: variable
variables:
- piwik_cache
- piwik_codesnippet_after
- piwik_codesnippet_before
- piwik_custom
- piwik_domain_mode
- piwik_pages
- piwik_privacy_donottrack
- piwik_roles
- piwik_site_id
- piwik_site_search
- piwik_trackmessages
- piwik_track
- piwik_trackfiles_extensions
- piwik_trackmailto
- piwik_trackuserid
- piwik_translation_set
- piwik_url_http
- piwik_url_https
- piwik_visibility_pages
- piwik_visibility_roles
- piwik_page_title_hierarchy
- piwik_page_title_hierarchy_exclude_home
process:
cache: piwik_cache
'codesnippet/after': piwik_codesnippet_after
'codesnippet/before': piwik_codesnippet_before
domain_mode: piwik_domain_mode
'privacy/donottrack': piwik_privacy_donottrack
site_id: piwik_site_id
'track/files': piwik_track
'track/files_extensions': piwik_trackfiles_extensions
'track/mailto': piwik_trackmailto
'track/messages': piwik_trackmessages
'track/site_search': piwik_site_search
'track/userid': piwik_trackuserid
'translation_set': piwik_translation_set
url_http: piwik_url_http
url_https: piwik_url_https
'visibility/user_account_mode': piwik_custom
'visibility/request_path_mode': piwik_visibility_pages
'visibility/request_path_pages':
plugin: piwik_visibility_pages
source:
- piwik_visibility_pages
- piwik_pages
# If Piwik uses PHP visibility, don't migrate it unless the PHP module is
# enabled.
skip_php: true
'visibility/user_role_mode': piwik_visibility_roles
'visibility/user_role_roles':
plugin: piwik_visibility_roles
source:
- piwik_roles
page_title_hierarchy: piwik_page_title_hierarchy
page_title_hierarchy_exclude_home: piwik_page_title_hierarchy_exclude_home
destination:
plugin: config
config_name: piwik.settings
migration_dependencies:
optional:
- d6_user_role
@@ -0,0 +1,24 @@
id: d6_piwik_user_settings
label: Piwik user settings
migration_tags:
- Drupal 6
source:
plugin: d6_user
constants:
key: user_account_users
module: piwik
process:
uid: uid
key: 'constants/key'
module: 'constants/module'
settings:
# Based on skip_row_if_not_set
plugin: piwik_skip_row_if_not_set
key: custom
module: piwik
source: data
destination:
plugin: user_data
migration_dependencies:
required:
- d6_user
@@ -0,0 +1,75 @@
id: d7_piwik_settings
label: Piwik 7 configuration
migration_tags:
- Drupal 7
source:
plugin: variable
variables:
- piwik_cache
- piwik_codesnippet_after
- piwik_codesnippet_before
- piwik_custom
- piwik_custom_var
- piwik_domain_mode
- piwik_pages
- piwik_privacy_donottrack
- piwik_roles
- piwik_site_id
- piwik_site_search
- piwik_track
- piwik_trackcolorbox
- piwik_trackfiles_extensions
- piwik_trackmailto
- piwik_trackmessages
- piwik_trackuserid
- piwik_translation_set
- piwik_url_http
- piwik_url_https
- piwik_visibility_pages
- piwik_visibility_roles
- piwik_page_title_hierarchy
- piwik_page_title_hierarchy_exclude_home
process:
cache: piwik_cache
'codesnippet/after': piwik_codesnippet_after
'codesnippet/before': piwik_codesnippet_before
'custom/variable':
plugin: piwik_custom_vars
source:
- piwik_custom_var
domain_mode: piwik_domain_mode
'privacy/donottrack': piwik_privacy_donottrack
site_id: piwik_site_id
'track/colorbox': piwik_trackcolorbox
'track/files': piwik_track
'track/files_extensions': piwik_trackfiles_extensions
'track/mailto': piwik_trackmailto
'track/messages': piwik_trackmessages
'track/site_search': piwik_site_search
'track/userid': piwik_trackuserid
'translation_set': piwik_translation_set
url_http: piwik_url_http
url_https: piwik_url_https
'visibility/user_account_mode': piwik_custom
'visibility/request_path_mode': piwik_visibility_pages
'visibility/request_path_pages':
plugin: piwik_visibility_pages
source:
- piwik_visibility_pages
- piwik_pages
# If Piwik uses PHP visibility, don't migrate it unless the PHP module is
# enabled.
skip_php: true
'visibility/user_role_mode': piwik_visibility_roles
'visibility/user_role_roles':
plugin: piwik_visibility_roles
source:
- piwik_roles
page_title_hierarchy: piwik_page_title_hierarchy
page_title_hierarchy_exclude_home: piwik_page_title_hierarchy_exclude_home
destination:
plugin: config
config_name: piwik.settings
migration_dependencies:
optional:
- d7_user_role
@@ -0,0 +1,24 @@
id: d7_piwik_user_settings
label: Piwik user settings
migration_tags:
- Drupal 7
source:
plugin: d7_user
constants:
key: user_account_users
module: piwik
process:
uid: uid
key: 'constants/key'
module: 'constants/module'
settings:
# Based on skip_row_if_not_set
plugin: piwik_skip_row_if_not_set
key: custom
module: piwik
source: data
destination:
plugin: user_data
migration_dependencies:
required:
- d7_user
@@ -0,0 +1,15 @@
name: 'Piwik Web Analytics'
type: module
description: "Adds Piwik javascript tracking code to all your site's pages."
package: Statistics
# core: 8.x
configure: piwik.admin_settings_form
test_dependencies:
- php:php
- token:token
# Information added by Drupal.org packaging script on 2017-11-11
version: '8.x-1.2'
core: '8.x'
project: 'piwik'
datestamp: 1510404815
@@ -0,0 +1,40 @@
<?php
/**
* @file
* Installation file for Piwik - Web analytics module.
*/
use Drupal\Core\Url;
/**
* Implements hook_uninstall().
*
* Remove cache directory if module is uninstalled.
*/
function piwik_uninstall() {
piwik_clear_js_cache();
}
/**
* Implements hook_requirements().
*/
function piwik_requirements($phase) {
$requirements = [];
if ($phase == 'runtime') {
$config = \Drupal::config('piwik.settings');
// Raise warning if Piwik user account has not been set yet.
if (!preg_match('/^\d{1,}$/', $config->get('site_id'))) {
$requirements['piwik_site_id'] = [
'title' => t('Piwik module'),
'description' => t('Piwik module has not been configured yet. Please configure its settings from the <a href=":url">Piwik settings page</a>.', [':url' => Url::fromRoute('piwik.admin_settings_form')->toString()]),
'severity' => REQUIREMENT_WARNING,
'value' => t('Not configured'),
];
}
}
return $requirements;
}
@@ -0,0 +1,15 @@
piwik:
version: VERSION
js:
js/piwik.js: {}
dependencies:
- core/jquery
- core/drupalSettings
piwik.admin:
version: VERSION
js:
js/piwik.admin.js: {}
dependencies:
- core/jquery
- core/drupal
@@ -0,0 +1,5 @@
piwik.admin_settings_form:
title: 'Piwik'
parent: system.admin_config_system
description: 'Configure the settings used to generate your Piwik tracking code.'
route_name: piwik.admin_settings_form
@@ -0,0 +1,4 @@
piwik.admin_settings_form_tab:
route_name: piwik.admin_settings_form
title: Settings
base_route: piwik.admin_settings_form
@@ -0,0 +1,727 @@
<?php
/**
* @file
* Drupal Module: Piwik.
*
* Adds the required Javascript to all your Drupal pages to allow tracking by
* the Piwik statistics package.
*
* @author: Alexander Hass <http://drupal.org/user/85918>
*/
use Drupal\Component\Serialization\Json;
use Drupal\Component\Utility\Crypt;
use Drupal\Component\Utility\Unicode;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Site\Settings;
use Drupal\Core\Url;
use Drupal\node\NodeInterface;
use GuzzleHttp\Exception\RequestException;
use Drupal\piwik\Component\Render\PiwikJavaScriptSnippet;
/**
* Define the default file extension list that should be tracked as download.
*/
define('PIWIK_TRACKFILES_EXTENSIONS', '7z|aac|arc|arj|asf|asx|avi|bin|csv|doc(x|m)?|dot(x|m)?|exe|flv|gif|gz|gzip|hqx|jar|jpe?g|js|mp(2|3|4|e?g)|mov(ie)?|msi|msp|pdf|phps|png|ppt(x|m)?|pot(x|m)?|pps(x|m)?|ppam|sld(x|m)?|thmx|qtm?|ra(m|r)?|sea|sit|tar|tgz|torrent|txt|wav|wma|wmv|wpd|xls(x|m|b)?|xlt(x|m)|xlam|xml|z|zip');
/**
* Implements hook_help().
*/
function piwik_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
case 'piwik.admin_settings_form':
return t('<a href=":pk_url">Piwik - Web analytics</a> is an open source (GPL license) web analytics software. It gives interesting reports on your website visitors, your popular pages, the search engines keywords they used, the language they speak... and so much more. Piwik aims to be an open source alternative to Google Analytics.', [':pk_url' => 'http://www.piwik.org/']);
}
}
/**
* Implements hook_page_attachments().
*
* Insert JavaScript to the appropriate scope/region of the page.
*/
function piwik_page_attachments(array &$page) {
$account = \Drupal::currentUser();
$config = \Drupal::config('piwik.settings');
$id = $config->get('site_id');
$request = \Drupal::request();
// Add module cache tags.
$page['#cache']['tags'] = Cache::mergeTags(isset($page['#cache']['tags']) ? $page['#cache']['tags'] : [], $config->getCacheTags());
// Get page http status code for visibility filtering.
$status = NULL;
if ($exception = $request->attributes->get('exception')) {
$status = $exception->getStatusCode();
}
$trackable_status_codes = [
// "Forbidden" status code.
'403',
// "Not Found" status code.
'404',
];
// 1. Check if the piwik account number has a valid value.
// 2. Track page views based on visibility value.
// 3. Check if we should track the currently active user's role.
if (preg_match('/^\d{1,}$/', $id) && (_piwik_visibility_pages() || in_array($status, $trackable_status_codes)) && _piwik_visibility_user($account)) {
$url_http = $config->get('url_http');
$url_https = $config->get('url_https');
$set_custom_url = '';
$set_document_title = '';
$set_custom_data = [];
// Add link tracking.
$link_settings = [];
$link_settings['trackMailto'] = $config->get('track.mailto');
if ((\Drupal::moduleHandler()->moduleExists('colorbox')) && $track_colorbox = $config->get('track.colorbox')) {
$link_settings['trackColorbox'] = $track_colorbox;
}
$page['#attached']['drupalSettings']['piwik'] = $link_settings;
$page['#attached']['library'][] = 'piwik/piwik';
// Piwik can show a tree view of page titles that represents the site
// structure if setDocumentTitle() provides the page titles as a "/"
// delimited list. This may makes it easier to browse through the statistics
// of page titles on larger sites.
if ($config->get('page_title_hierarchy') == TRUE) {
$titles = _piwik_get_hierarchy_titles();
// Remove all empty titles.
$titles = array_filter($titles);
if (!empty($titles)) {
// Encode title, at least to keep "/" intact.
$titles = array_map('rawurlencode', $titles);
$set_document_title = Json::encode(implode('/', $titles));
}
}
// Add messages tracking.
$message_events = '';
if ($message_types = $config->get('track.messages')) {
$message_types = array_values(array_filter($message_types));
$status_heading = [
'status' => t('Status message'),
'warning' => t('Warning message'),
'error' => t('Error message'),
];
foreach (drupal_get_messages(NULL, FALSE) as $type => $messages) {
// Track only the selected message types.
if (in_array($type, $message_types)) {
foreach ($messages as $message) {
$message_events .= '_paq.push(["trackEvent", ' . Json::encode(t('Messages')) . ', ' . Json::encode($status_heading[$type]) . ', ' . Json::encode(strip_tags($message)) . ']);';
}
}
}
}
// If this node is a translation of another node, pass the original
// node instead.
if (\Drupal::moduleHandler()->moduleExists('content_translation') && $config->get('translation_set')) {
// Check if we have a node object, it has translation enabled, and its
// language code does not match its source language code.
if ($request->attributes->has('node')) {
$node = $request->attributes->get('node');
if ($node instanceof NodeInterface && \Drupal::service('entity.repository')->getTranslationFromContext($node) !== $node->getUntranslated()) {
$set_custom_url = Json::encode(Url::fromRoute('entity.node.canonical', ['node' => $node->id()], ['language' => $node->getUntranslated()->language()])->toString());
}
}
}
// Track access denied (403) and file not found (404) pages.
if ($status == '403') {
$set_document_title = '"403/URL = " + encodeURIComponent(document.location.pathname+document.location.search) + "/From = " + encodeURIComponent(document.referrer)';
}
elseif ($status == '404') {
$set_document_title = '"404/URL = " + encodeURIComponent(document.location.pathname+document.location.search) + "/From = " + encodeURIComponent(document.referrer)';
}
// #2693595: User has entered an invalid login and clicked on forgot
// password link. This link contains the username or email address and may
// get send to Google if we do not override it. Override only if 'name'
// query param exists. Last custom url condition, this need to win.
//
// URLs to protect are:
// - user/password?name=username
// - user/password?name=foo@example.com
if (\Drupal::routeMatch()->getRouteName() == 'user.pass' && $request->query->has('name')) {
$set_custom_url = Json::encode(Url::fromRoute('user.pass')->toString());
}
// Add custom variables.
$piwik_custom_vars = $config->get('custom.variable');
$custom_variable = '';
for ($i = 1; $i < 6; $i++) {
$custom_var_name = !empty($piwik_custom_vars[$i]['name']) ? $piwik_custom_vars[$i]['name'] : '';
if (!empty($custom_var_name)) {
$custom_var_value = !empty($piwik_custom_vars[$i]['value']) ? $piwik_custom_vars[$i]['value'] : '';
$custom_var_scope = !empty($piwik_custom_vars[$i]['scope']) ? $piwik_custom_vars[$i]['scope'] : 'visit';
$types = [];
if ($request->attributes->has('node')) {
$node = $request->attributes->get('node');
if ($node instanceof NodeInterface) {
$types += ['node' => $node];
}
}
$custom_var_name = \Drupal::token()->replace($custom_var_name, $types, ['clear' => TRUE]);
$custom_var_value = \Drupal::token()->replace($custom_var_value, $types, ['clear' => TRUE]);
// Suppress empty custom names and/or variables.
if (!Unicode::strlen(trim($custom_var_name)) || !Unicode::strlen(trim($custom_var_value))) {
continue;
}
// Custom variables names and values are limited to 200 characters in
// length. It is recommended to store values that are as small as
// possible to ensure that the Piwik Tracking request URL doesn't go
// over the URL limit for the webserver or browser.
$custom_var_name = rtrim(substr($custom_var_name, 0, 200));
$custom_var_value = rtrim(substr($custom_var_value, 0, 200));
// Add variables to tracker.
$custom_variable .= '_paq.push(["setCustomVariable", ' . Json::encode($i) . ', ' . Json::encode($custom_var_name) . ', ' . Json::encode($custom_var_value) . ', ' . Json::encode($custom_var_scope) . ']);';
}
}
// Add any custom code snippets if specified.
$codesnippet_before = $config->get('codesnippet.before');
$codesnippet_after = $config->get('codesnippet.after');
// Build tracker code.
// @see http://piwik.org/docs/javascript-tracking/#toc-asynchronous-tracking
$script = 'var _paq = _paq || [];';
$script .= '(function(){';
$script .= 'var u=(("https:" == document.location.protocol) ? "' . UrlHelper::filterBadProtocol($url_https) . '" : "' . UrlHelper::filterBadProtocol($url_http) . '");';
$script .= '_paq.push(["setSiteId", ' . Json::encode($id) . ']);';
$script .= '_paq.push(["setTrackerUrl", u+"piwik.php"]);';
// Track logged in users across all devices.
if ($config->get('track.userid') && $account->isAuthenticated()) {
$script .= '_paq.push(["setUserId", ' . Json::encode(piwik_user_id_hash($account->id())) . ']);';
}
// Set custom data.
if (!empty($set_custom_data)) {
foreach ($set_custom_data as $custom_data) {
$script .= '_paq.push(["setCustomData", ' . $custom_data . ']);';
}
}
// Set custom url.
if (!empty($set_custom_url)) {
$script .= '_paq.push(["setCustomUrl", ' . $set_custom_url . ']);';
}
// Set custom document title.
if (!empty($set_document_title)) {
$script .= '_paq.push(["setDocumentTitle", ' . $set_document_title . ']);';
}
// Custom file download extensions.
if ($config->get('track.files') && !($config->get('track.files_extensions') == PIWIK_TRACKFILES_EXTENSIONS)) {
$script .= '_paq.push(["setDownloadExtensions", ' . Json::encode($config->get('track.files_extensions')) . ']);';
}
// Disable tracking for visitors who have opted out from tracking via DNT
// (Do-Not-Track) header.
if ($config->get('privacy.donottrack')) {
$script .= '_paq.push(["setDoNotTrack", 1]);';
}
// Domain tracking type.
global $cookie_domain;
$domain_mode = $config->get('domain_mode');
// Per RFC 2109, cookie domains must contain at least one dot other than the
// first. For hosts such as 'localhost' or IP Addresses we don't set a
// cookie domain.
if ($domain_mode == 1 && count(explode('.', $cookie_domain)) > 2 && !is_numeric(str_replace('.', '', $cookie_domain))) {
$script .= '_paq.push(["setCookieDomain", ' . Json::encode($cookie_domain) . ']);';
}
// Ordering $custom_variable before $codesnippet_before allows users to add
// custom code snippets that may use deleteCustomVariable() and/or
// getCustomVariable().
if (!empty($custom_variable)) {
$script .= $custom_variable;
}
if (!empty($codesnippet_before)) {
$script .= $codesnippet_before;
}
// Site search tracking support.
// NOTE: It's recommended not to call trackPageView() on the Site Search
// Result page.
if (\Drupal::moduleHandler()->moduleExists('search') && $config->get('track.site_search') && (strpos(\Drupal::routeMatch()->getRouteName(), 'search.view') === 0) && $keys = ($request->query->has('keys') ? trim($request->get('keys')) : '')) {
// Parameters:
// 1. Search keyword searched for. Example: "Banana"
// 2. Search category selected in your search engine. If you do not need
// this, set to false. Example: "Organic Food"
// 3. Number of results on the Search results page. Zero indicates a
// 'No Result Search Keyword'. Set to false if you don't know.
//
// hook_preprocess_search_results() is not executed if search result is
// empty. Make sure the counter is set to 0 if there are no results.
$script .= '_paq.push(["trackSiteSearch", ' . Json::encode($keys) . ', false, (window.piwik_search_results) ? window.piwik_search_results : 0]);';
}
else {
$script .= '_paq.push(["trackPageView"]);';
}
// Add link tracking.
if ($config->get('track.files')) {
// Disable tracking of links with ".no-tracking" and ".colorbox" classes.
$ignore_classes = [
'no-tracking',
'colorbox',
];
// Disable the download & outlink tracking for specific CSS classes.
// Custom code snippets with 'setIgnoreClasses' will override the value.
// @see http://developer.piwik.org/api-reference/tracking-javascript#disable-the-download-amp-outlink-tracking-for-specific-css-classes
$script .= '_paq.push(["setIgnoreClasses", ' . Json::encode($ignore_classes) . ']);';
// Enable download & outlink link tracking.
$script .= '_paq.push(["enableLinkTracking"]);';
}
if (!empty($message_events)) {
$script .= $message_events;
}
if (!empty($codesnippet_after)) {
$script .= $codesnippet_after;
}
$script .= 'var d=document,';
$script .= 'g=d.createElement("script"),';
$script .= 's=d.getElementsByTagName("script")[0];';
$script .= 'g.type="text/javascript";';
$script .= 'g.defer=true;';
$script .= 'g.async=true;';
// Should a local cached copy of the tracking code be used?
if ($config->get('cache') && $url = _piwik_cache($url_http . 'piwik.js')) {
// A dummy query-string is added to filenames, to gain control over
// browser-caching. The string changes on every update or full cache
// flush, forcing browsers to load a new copy of the files, as the
// URL changed.
$query_string = '?' . (\Drupal::state()->get('system.css_js_query_string') ?: '0');
$script .= 'g.src="' . $url . $query_string . '";';
}
else {
$script .= 'g.src=u+"piwik.js";';
}
$script .= 's.parentNode.insertBefore(g,s);';
$script .= '})();';
// Add tracker code.
$page['#attached']['html_head'][] = [
[
'#tag' => 'script',
'#value' => new PiwikJavaScriptSnippet($script),
],
'piwik_tracking_script',
];
}
}
/**
* Generate user id hash to implement USER_ID.
*
* The USER_ID value should be a unique, persistent, and non-personally
* identifiable string identifier that represents a user or signed-in
* account across devices.
*
* @param int $uid
* User id.
*
* @return string
* User id hash.
*/
function piwik_user_id_hash($uid) {
return Crypt::hmacBase64($uid, \Drupal::service('private_key')->get() . Settings::getHashSalt());
}
/**
* Implements hook_entity_extra_field_info().
*/
function piwik_entity_extra_field_info() {
$extra['user']['user']['form']['piwik'] = [
'label' => t('Piwik settings'),
'description' => t('Piwik module form element.'),
'weight' => 3,
];
return $extra;
}
/**
* Implements hook_form_FORM_ID_alter().
*
* Allow users to decide if tracking code will be added to pages or not.
*/
function piwik_form_user_form_alter(&$form, FormStateInterface $form_state) {
$config = \Drupal::config('piwik.settings');
$account = $form_state->getFormObject()->getEntity();
if ($account->hasPermission('opt-in or out of piwik tracking') && ($visibility_users = $config->get('visibility.user_account_mode')) != 0 && _piwik_visibility_roles($account)) {
$account_data_piwik = \Drupal::service('user.data')->get('piwik', $account->id());
$form['piwik'] = [
'#type' => 'details',
'#title' => t('Piwik settings'),
'#weight' => 3,
'#open' => TRUE,
];
switch ($visibility_users) {
case 1:
$description = t('Users are tracked by default, but you are able to opt out.');
break;
case 2:
$description = t('Users are <em>not</em> tracked by default, but you are able to opt in.');
break;
}
// Disable tracking for visitors who have opted out from tracking via DNT
// (Do-Not-Track) header.
$disabled = FALSE;
if ($config->get('privacy.donottrack') && !empty($_SERVER['HTTP_DNT'])) {
$disabled = TRUE;
// Override settings value.
$account_data_piwik['users'] = FALSE;
$description .= '<span class="admin-missing">';
$description .= ' ' . t('You have opted out from tracking via browser privacy settings.');
$description .= '</span>';
}
$form['piwik']['user_account_users'] = [
'#type' => 'checkbox',
'#title' => t('Enable user tracking'),
'#description' => $description,
'#default_value' => isset($account_data_piwik['user_account_users']) ? $account_data_piwik['user_account_users'] : ($visibility_users == 1),
'#disabled' => $disabled,
];
// hook_user_update() is missing in D8, add custom submit handler.
$form['actions']['submit']['#submit'][] = 'piwik_user_profile_form_submit';
}
}
/**
* Submit callback for user profile form to save the Piwik setting.
*/
function piwik_user_profile_form_submit($form, FormStateInterface $form_state) {
$account = $form_state->getFormObject()->getEntity();
if ($account->id() && $form_state->hasValue('user_account_users')) {
\Drupal::service('user.data')->set('piwik', $account->id(), 'user_account_users', (int) $form_state->getValue('user_account_users'));
}
}
/**
* Implements hook_cron().
*/
function piwik_cron() {
$config = \Drupal::config('piwik.settings');
// Regenerate the piwik.js every day.
if (REQUEST_TIME - \Drupal::state()->get('piwik.last_cache') >= 86400 && $config->get('cache')) {
_piwik_cache($config->get('url_http') . 'piwik.js', TRUE);
\Drupal::state()->set('piwik.last_cache', REQUEST_TIME);
}
}
/**
* Implements hook_preprocess_item_list__search_results().
*
* Collects and adds the number of search results to the head.
*/
function piwik_preprocess_item_list__search_results(&$variables) {
$config = \Drupal::config('piwik.settings');
// Only run on search results list.
if ($config->get('track.site_search')) {
// There is no search result $variable available that hold the number of
// items found. The variable $variables['items'] has the current page items
// only. But the pager item mumber can tell the number of search results.
global $pager_total_items;
$variables['#attached']['html_head'][] = [
[
'#tag' => 'script',
'#value' => 'window.piwik_search_results = ' . intval($pager_total_items[0]) . ';',
'#weight' => JS_LIBRARY - 1,
],
'piwik_search_script',
];
}
}
/**
* Download/Synchronize/Cache tracking code file locally.
*
* @param string $location
* The full URL to the external javascript file.
* @param bool $synchronize
* Synchronize to local cache if remote file has changed.
*
* @return mixed
* The path to the local javascript file on success, boolean FALSE on failure.
*/
function _piwik_cache($location, $synchronize = FALSE) {
$path = 'public://piwik';
$file_destination = $path . '/' . basename($location);
if (!file_exists($file_destination) || $synchronize) {
// Download the latest tracking code.
try {
$data = \Drupal::httpClient()
->get($location)
->getBody(TRUE);
if (file_exists($file_destination)) {
// Synchronize tracking code and and replace local file if outdated.
$data_hash_local = Crypt::hashBase64(file_get_contents($file_destination));
$data_hash_remote = Crypt::hashBase64($data);
// Check that the files directory is writable.
if ($data_hash_local != $data_hash_remote && file_prepare_directory($path)) {
// Save updated tracking code file to disk.
file_unmanaged_save_data($data, $file_destination, FILE_EXISTS_REPLACE);
// Based on Drupal Core class AssetDumper.
if (extension_loaded('zlib') && \Drupal::config('system.performance')->get('js.gzip')) {
file_unmanaged_save_data(gzencode($data, 9, FORCE_GZIP), $file_destination . '.gz', FILE_EXISTS_REPLACE);
}
\Drupal::logger('piwik')->info('Locally cached tracking code file has been updated.');
// Change query-strings on css/js files to enforce reload for all
// users.
_drupal_flush_css_js();
}
}
else {
// Check that the files directory is writable.
if (file_prepare_directory($path, FILE_CREATE_DIRECTORY)) {
// There is no need to flush JS here as core refreshes JS caches
// automatically, if new files are added.
file_unmanaged_save_data($data, $file_destination, FILE_EXISTS_REPLACE);
// Based on Drupal Core class AssetDumper.
if (extension_loaded('zlib') && \Drupal::config('system.performance')->get('js.gzip')) {
file_unmanaged_save_data(gzencode($data, 9, FORCE_GZIP), $file_destination . '.gz', FILE_EXISTS_REPLACE);
}
\Drupal::logger('piwik')->info('Locally cached tracking code file has been saved.');
// Return the local JS file path.
return file_url_transform_relative(file_create_url($file_destination));
}
}
}
catch (RequestException $exception) {
watchdog_exception('piwik', $exception);
}
}
else {
// Return the local JS file path.
return file_url_transform_relative(file_create_url($file_destination));
}
}
/**
* Delete cached files and directory.
*/
function piwik_clear_js_cache() {
$path = 'public://piwik';
if (file_prepare_directory($path)) {
file_scan_directory($path, '/.*/', ['callback' => 'file_unmanaged_delete']);
\Drupal::service('file_system')->rmdir($path);
// Change query-strings on css/js files to enforce reload for all users.
_drupal_flush_css_js();
\Drupal::logger('piwik')->info('Local cache has been purged.');
}
}
/**
* Tracking visibility check for an user object.
*
* @param object $account
* A user object containing an array of roles to check.
*
* @return bool
* TRUE if the current user is being tracked by Piwik, otherwise FALSE.
*/
function _piwik_visibility_user($account) {
$config = \Drupal::config('piwik.settings');
$enabled = FALSE;
// Is current user a member of a role that should be tracked?
if (_piwik_visibility_roles($account)) {
// Use the user's block visibility setting, if necessary.
if (($visibility_user_account_mode = $config->get('visibility.user_account_mode')) != 0) {
$user_data_piwik = \Drupal::service('user.data')->get('piwik', $account->id());
if ($account->id() && isset($user_data_piwik['user_account_users'])) {
$enabled = $user_data_piwik['user_account_users'];
}
else {
$enabled = ($visibility_user_account_mode == 1);
}
}
else {
$enabled = TRUE;
}
}
return $enabled;
}
/**
* Tracking visibility check for user roles.
*
* Based on visibility setting this function returns TRUE if Piwik code should
* be added for the current role and otherwise FALSE.
*
* @param object $account
* A user object containing an array of roles to check.
*
* @return bool
* TRUE if JS code should be added for the current role and otherwise FALSE.
*/
function _piwik_visibility_roles($account) {
$config = \Drupal::config('piwik.settings');
$enabled = $visibility_user_role_mode = $config->get('visibility.user_role_mode');
$user_role_roles = $config->get('visibility.user_role_roles');
if (count($user_role_roles) > 0) {
// One or more roles are selected.
foreach (array_values($account->getRoles()) as $user_role) {
// Is the current user a member of one of these roles?
if (in_array($user_role, $user_role_roles)) {
// Current user is a member of a role that should be tracked/excluded
// from tracking.
$enabled = !$visibility_user_role_mode;
break;
}
}
}
else {
// No role is selected for tracking, therefore all roles should be tracked.
$enabled = TRUE;
}
return $enabled;
}
/**
* Tracking visibility check for pages.
*
* Based on visibility setting this function returns TRUE if JS code should
* be added to the current page and otherwise FALSE.
*/
function _piwik_visibility_pages() {
static $page_match;
// Cache visibility result if function is called more than once.
if (!isset($page_match)) {
$config = \Drupal::config('piwik.settings');
$visibility_request_path_mode = $config->get('visibility.request_path_mode');
$visibility_request_path_pages = $config->get('visibility.request_path_pages');
// Match path if necessary.
if (!empty($visibility_request_path_pages)) {
// Convert path to lowercase. This allows comparison of the same path
// with different case. Ex: /Page, /page, /PAGE.
$pages = Unicode::strtolower($visibility_request_path_pages);
if ($visibility_request_path_mode < 2) {
// Compare the lowercase path alias (if any) and internal path.
$path = \Drupal::service('path.current')->getPath();
$path_alias = Unicode::strtolower(\Drupal::service('path.alias_manager')->getAliasByPath($path));
$page_match = \Drupal::service('path.matcher')->matchPath($path_alias, $pages) || (($path != $path_alias) && \Drupal::service('path.matcher')->matchPath($path, $pages));
// When $visibility_request_path_mode has a value of 0, the tracking
// code is displayed on all pages except those listed in $pages. When
// set to 1, it is displayed only on those pages listed in $pages.
$page_match = !($visibility_request_path_mode xor $page_match);
}
elseif (\Drupal::moduleHandler()->moduleExists('php')) {
$page_match = php_eval($visibility_request_path_pages);
}
else {
$page_match = FALSE;
}
}
else {
$page_match = TRUE;
}
}
return $page_match;
}
/**
* Get the page titles trail for the current page.
*
* Based on menu_get_active_breadcrumb().
*
* @return array
* All page titles, including current page.
*/
function _piwik_get_hierarchy_titles() {
$titles = [];
$path = Url::fromRoute('<current>')->toString();
$config = \Drupal::config('system.site');
$piwik_conf = \Drupal::config('piwik.settings');
// No breadcrumb for the front page.
if (($path === Url::fromUserInput($config->get('page.front'))->toString())) {
return $titles;
}
// Load up the menu tree.
// TODO: Check this is a sane approach.
$menu_tree = \Drupal::menuTree();
$menu_name = \Drupal::config('system.menu.main')->get('id');
// Build the typical default set of menu tree parameters.
$parameters = $menu_tree->getCurrentRouteMenuTreeParameters($menu_name);
// Load the tree based on this set of parameters.
$tree = $menu_tree->load($menu_name, $parameters);
// Transform the tree using the manipulators.
$manipulators = [
// Only show links that are accessible for the current user.
['callable' => 'menu.default_tree_manipulators:checkAccess'],
// Use the default sorting of menu links.
['callable' => 'menu.default_tree_manipulators:generateIndexAndSort'],
// Fatten the menu.
['callable' => 'menu.default_tree_manipulators:flatten'],
];
$tree = $menu_tree->transform($tree, $manipulators);
if (!empty($tree)) {
foreach ($tree as $menu_item) {
// If the item is in the active trail and we don't have a front page link
// when we have set to exclude home from the breadcrumbs then add the
// title.
if ($menu_item->inActiveTrail && !($piwik_conf->get('page_title_hierarchy_exclude_home') && $menu_item->link->getRouteName() == '<front>')) {
$titles[] = $menu_item->link->getTitle();
}
}
}
return $titles;
}
@@ -0,0 +1,14 @@
administer piwik:
title: 'Administer Piwik'
description: 'Perform maintenance tasks for Piwik.'
opt-in or out of piwik tracking:
title: 'Opt-in or out of tracking'
description: 'Allow users to decide if tracking code will be added to pages or not.'
use PHP for piwik tracking visibility:
title: 'Use PHP for tracking visibility'
description: 'Enter PHP code in the field for tracking visibility settings.'
restrict access: true
add JS snippets for piwik:
title: 'Add JavaScript snippets'
description: 'Enter JavaScript code snippets for advanced Piwik functionality.'
restrict access: true
@@ -0,0 +1,7 @@
piwik.admin_settings_form:
path: '/admin/config/system/piwik'
defaults:
_form: '\Drupal\piwik\Form\PiwikAdminSettingsForm'
_title: 'Piwik'
requirements:
_permission: 'administer piwik'
@@ -0,0 +1,60 @@
<?php
/**
* @file
* Builds placeholder replacement tokens for user-related data.
*/
use Drupal\Component\Utility\Html;
use Drupal\Core\Render\BubbleableMetadata;
/**
* Implements hook_token_info().
*/
function piwik_token_info() {
$user['piwik-role-names'] = [
'name' => t('User role names'),
'description' => t('The role names the user account is a member of as comma separated list.'),
'needs-data' => 'user',
];
$user['piwik-role-ids'] = [
'name' => t('User role ids'),
'description' => t('The role ids the user account is a member of as comma separated list.'),
'needs-data' => 'user',
];
return [
'tokens' => ['user' => $user],
];
}
/**
* Implements hook_tokens().
*/
function piwik_tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) {
$sanitize = !empty($options['sanitize']);
$replacements = [];
if ($type == 'user' && !empty($data['user'])) {
$account = $data['user'];
foreach ($tokens as $name => $original) {
switch ($name) {
// Basic user account information.
case 'piwik-role-names':
$names = implode(',', $account->getRoles());
$replacements[$original] = $sanitize ? Html::escape($names) : $names;
$bubbleable_metadata->addCacheableDependency($account);
break;
case 'piwik-role-ids':
$ids = implode(',', array_keys($account->getRoles()));
$replacements[$original] = $sanitize ? Html::escape($ids) : $ids;
$bubbleable_metadata->addCacheableDependency($account);
break;
}
}
}
return $replacements;
}
@@ -0,0 +1,43 @@
<?php
namespace Drupal\piwik\Component\Render;
use Drupal\Component\Render\MarkupInterface;
/**
* Formats a string for JavaScript display.
*/
class PiwikJavaScriptSnippet implements MarkupInterface {
/**
* The string to escape.
*
* @var string
*/
protected $string;
/**
* Constructs an HtmlEscapedText object.
*
* @param string $string
* The string to escape. This value will be cast to a string.
*/
public function __construct($string) {
$this->string = (string) $string;
}
/**
* {@inheritdoc}
*/
public function __toString() {
return $this->string;
}
/**
* {@inheritdoc}
*/
public function jsonSerialize() {
return $this->__toString();
}
}
@@ -0,0 +1,717 @@
<?php
namespace Drupal\piwik\Form;
use Drupal\Component\Utility\Unicode;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
use GuzzleHttp\Exception\RequestException;
/**
* Configure Piwik settings for this site.
*/
class PiwikAdminSettingsForm extends ConfigFormBase {
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'piwik_admin_settings';
}
/**
* {@inheritdoc}
*/
protected function getEditableConfigNames() {
return ['piwik.settings'];
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$config = $this->config('piwik.settings');
$form['general'] = [
'#type' => 'details',
'#title' => $this->t('General settings'),
'#open' => TRUE,
];
$form['general']['piwik_site_id'] = [
'#default_value' => $config->get('site_id'),
'#description' => $this->t('The user account number is unique to the websites domain. Click the <strong>Settings</strong> link in your Piwik account, then the <strong>Websites</strong> tab and enter the appropriate site <strong>ID</strong> into this field.'),
'#maxlength' => 20,
'#required' => TRUE,
'#size' => 15,
'#title' => $this->t('Piwik site ID'),
'#type' => 'textfield',
];
$form['general']['piwik_url_http'] = [
'#default_value' => $config->get('url_http'),
'#description' => $this->t('The URL to your Piwik base directory. Example: "http://www.example.com/piwik/".'),
'#maxlength' => 255,
'#required' => TRUE,
'#size' => 80,
'#title' => $this->t('Piwik HTTP URL'),
'#type' => 'textfield',
];
$form['general']['piwik_url_https'] = [
'#default_value' => $config->get('url_https'),
'#description' => $this->t('The URL to your Piwik base directory with SSL certificate installed. Required if you track a SSL enabled website. Example: "https://www.example.com/piwik/".'),
'#maxlength' => 255,
'#size' => 80,
'#title' => $this->t('Piwik HTTPS URL'),
'#type' => 'textfield',
];
// Required for automated form save testing only.
$form['general']['piwik_url_skiperror'] = [
'#type' => 'hidden',
'#default_value' => FALSE,
];
// Visibility settings.
$form['tracking_scope'] = [
'#type' => 'vertical_tabs',
'#title' => $this->t('Tracking scope'),
'#attached' => [
'library' => [
'piwik/piwik.admin',
],
],
];
$form['tracking']['domain_tracking'] = [
'#type' => 'details',
'#title' => $this->t('Domains'),
'#group' => 'tracking_scope',
];
global $cookie_domain;
$multiple_sub_domains = [];
foreach (['www', 'app', 'shop'] as $subdomain) {
if (count(explode('.', $cookie_domain)) > 2 && !is_numeric(str_replace('.', '', $cookie_domain))) {
$multiple_sub_domains[] = $subdomain . $cookie_domain;
}
// IP addresses or localhost.
else {
$multiple_sub_domains[] = $subdomain . '.example.com';
}
}
$form['tracking']['domain_tracking']['piwik_domain_mode'] = [
'#type' => 'radios',
'#title' => $this->t('What are you tracking?'),
'#options' => [
0 => $this->t('A single domain (default)'),
1 => $this->t('One domain with multiple subdomains'),
],
0 => [
'#description' => $this->t('Domain: @domain', ['@domain' => $_SERVER['HTTP_HOST']]),
],
1 => [
'#description' => $this->t('Examples: @domains', ['@domains' => implode(', ', $multiple_sub_domains)]),
],
'#default_value' => $config->get('domain_mode'),
];
// Page specific visibility configurations.
$account = \Drupal::currentUser();
$php_access = $account->hasPermission('use PHP for piwik tracking visibility');
$visibility_request_path_pages = $config->get('visibility.request_path_pages');
$form['tracking']['page_visibility_settings'] = [
'#type' => 'details',
'#title' => $this->t('Pages'),
'#group' => 'tracking_scope',
];
if ($config->get('visibility.request_path_mode') == 2 && !$php_access) {
// No permission to change PHP snippets, but keep existing settings.
$form['tracking']['page_visibility_settings'] = [];
$form['tracking']['page_visibility_settings']['piwik_visibility_request_path_mode'] = ['#type' => 'value', '#value' => 2];
$form['tracking']['page_visibility_settings']['piwik_visibility_request_path_pages'] = ['#type' => 'value', '#value' => $visibility_request_path_pages];
}
else {
$options = [
t('Every page except the listed pages'),
t('The listed pages only'),
];
$description = t("Specify pages by using their paths. Enter one path per line. The '*' character is a wildcard. Example paths are %blog for the blog page and %blog-wildcard for every personal blog. %front is the front page.", ['%blog' => '/blog', '%blog-wildcard' => '/blog/*', '%front' => '<front>']);
if (\Drupal::moduleHandler()->moduleExists('php') && $php_access) {
$options[] = t('Pages on which this PHP code returns <code>TRUE</code> (experts only)');
$title = t('Pages or PHP code');
$description .= ' ' . t('If the PHP option is chosen, enter PHP code between %php. Note that executing incorrect PHP code can break your Drupal site.', ['%php' => '<?php ?>']);
}
else {
$title = t('Pages');
}
$form['tracking']['page_visibility_settings']['piwik_visibility_request_path_mode'] = [
'#type' => 'radios',
'#title' => $this->t('Add tracking to specific pages'),
'#options' => $options,
'#default_value' => $config->get('visibility.request_path_mode'),
];
$form['tracking']['page_visibility_settings']['piwik_visibility_request_path_pages'] = [
'#type' => 'textarea',
'#title' => $title,
'#title_display' => 'invisible',
'#default_value' => !empty($visibility_request_path_pages) ? $visibility_request_path_pages : '',
'#description' => $description,
'#rows' => 10,
];
}
// Render the role overview.
$visibility_user_role_roles = $config->get('visibility.user_role_roles');
$form['tracking']['role_visibility_settings'] = [
'#type' => 'details',
'#title' => $this->t('Roles'),
'#group' => 'tracking_scope',
];
$form['tracking']['role_visibility_settings']['piwik_visibility_user_role_mode'] = [
'#type' => 'radios',
'#title' => $this->t('Add tracking for specific roles'),
'#options' => [
t('Add to the selected roles only'),
t('Add to every role except the selected ones'),
],
'#default_value' => $config->get('visibility.user_role_mode'),
];
$form['tracking']['role_visibility_settings']['piwik_visibility_user_role_roles'] = [
'#type' => 'checkboxes',
'#title' => $this->t('Roles'),
'#default_value' => !empty($visibility_user_role_roles) ? $visibility_user_role_roles : [],
'#options' => array_map('\Drupal\Component\Utility\Html::escape', user_role_names()),
'#description' => $this->t('If none of the roles are selected, all users will be tracked. If a user has any of the roles checked, that user will be tracked (or excluded, depending on the setting above).'),
];
// Standard tracking configurations.
$visibility_user_account_mode = $config->get('visibility.user_account_mode');
$form['tracking']['user_visibility_settings'] = [
'#type' => 'details',
'#title' => $this->t('Users'),
'#group' => 'tracking_scope',
];
$t_permission = ['%permission' => $this->t('opt-in or out of tracking')];
$form['tracking']['user_visibility_settings']['piwik_visibility_user_account_mode'] = [
'#type' => 'radios',
'#title' => $this->t('Allow users to customize tracking on their account page'),
'#options' => [
t('No customization allowed'),
t('Tracking on by default, users with %permission permission can opt out', $t_permission),
t('Tracking off by default, users with %permission permission can opt in', $t_permission),
],
'#default_value' => !empty($visibility_user_account_mode) ? $visibility_user_account_mode : 0,
];
$form['tracking']['user_visibility_settings']['piwik_trackuserid'] = [
'#type' => 'checkbox',
'#title' => $this->t('Track User ID'),
'#default_value' => $config->get('track.userid'),
'#description' => $this->t('User ID enables the analysis of groups of sessions, across devices, using a unique, persistent, and non-personally identifiable ID string representing a user. <a href=":url">Learn more about the benefits of using User ID</a>.', [':url' => 'http://piwik.org/docs/user-id/']),
];
// Link specific configurations.
$form['tracking']['linktracking'] = [
'#type' => 'details',
'#title' => $this->t('Links and downloads'),
'#group' => 'tracking_scope',
];
$form['tracking']['linktracking']['piwik_trackmailto'] = [
'#type' => 'checkbox',
'#title' => $this->t('Track clicks on mailto links'),
'#default_value' => $config->get('track.mailto'),
];
$form['tracking']['linktracking']['piwik_trackfiles'] = [
'#type' => 'checkbox',
'#title' => $this->t('Track clicks on outbound links and downloads (clicks on file links) for the following extensions'),
'#default_value' => $config->get('track.files'),
];
$form['tracking']['linktracking']['piwik_trackfiles_extensions'] = [
'#title' => $this->t('List of download file extensions'),
'#title_display' => 'invisible',
'#type' => 'textfield',
'#default_value' => $config->get('track.files_extensions'),
'#description' => $this->t('A file extension list separated by the | character that will be tracked as download when clicked. Regular expressions are supported. For example: @extensions', ['@extensions' => PIWIK_TRACKFILES_EXTENSIONS]),
'#maxlength' => 500,
'#states' => [
'enabled' => [
':input[name="piwik_trackfiles"]' => ['checked' => TRUE],
],
// Note: Form required marker is not visible as title is invisible.
'required' => [
':input[name="piwik_trackfiles"]' => ['checked' => TRUE],
],
],
];
$colorbox_dependencies = '<div class="admin-requirements">';
$colorbox_dependencies .= t('Requires: @module-list', ['@module-list' => (\Drupal::moduleHandler()->moduleExists('colorbox') ? t('@module (<span class="admin-enabled">enabled</span>)', ['@module' => 'Colorbox']) : t('@module (<span class="admin-missing">disabled</span>)', ['@module' => 'Colorbox']))]);
$colorbox_dependencies .= '</div>';
$form['tracking']['linktracking']['piwik_trackcolorbox'] = [
'#type' => 'checkbox',
'#title' => t('Track content in colorbox modal dialogs'),
'#description' => t('Enable to track the content shown in colorbox modal windows.') . $colorbox_dependencies,
'#default_value' => $config->get('track.colorbox'),
'#disabled' => (\Drupal::moduleHandler()->moduleExists('colorbox') ? FALSE : TRUE),
];
// Message specific configurations.
$form['tracking']['messagetracking'] = [
'#type' => 'details',
'#title' => $this->t('Messages'),
'#group' => 'tracking_scope',
];
$track_messages = $config->get('track.messages');
$form['tracking']['messagetracking']['piwik_trackmessages'] = [
'#type' => 'checkboxes',
'#title' => $this->t('Track messages of type'),
'#default_value' => !empty($track_messages) ? $track_messages : [],
'#description' => $this->t('This will track the selected message types shown to users. Tracking of form validation errors may help you identifying usability issues in your site. Every message is tracked as one individual event. Messages from excluded pages cannot be tracked.'),
'#options' => [
'status' => $this->t('Status message'),
'warning' => $this->t('Warning message'),
'error' => $this->t('Error message'),
],
];
$form['tracking']['search'] = [
'#type' => 'details',
'#title' => $this->t('Search'),
'#group' => 'tracking_scope',
];
$site_search_dependencies = '<div class="admin-requirements">';
$site_search_dependencies .= t('Requires: @module-list', ['@module-list' => (\Drupal::moduleHandler()->moduleExists('search') ? t('@module (<span class="admin-enabled">enabled</span>)', ['@module' => 'Search']) : t('@module (<span class="admin-missing">disabled</span>)', ['@module' => 'Search']))]);
$site_search_dependencies .= '</div>';
$form['tracking']['search']['piwik_site_search'] = [
'#type' => 'checkbox',
'#title' => $this->t('Track internal search'),
'#description' => $this->t('If checked, internal search keywords are tracked.') . $site_search_dependencies,
'#default_value' => $config->get('track.site_search'),
'#disabled' => (\Drupal::moduleHandler()->moduleExists('search') ? FALSE : TRUE),
];
// Privacy specific configurations.
$form['tracking']['privacy'] = [
'#type' => 'details',
'#title' => $this->t('Privacy'),
'#group' => 'tracking_scope',
];
$form['tracking']['privacy']['piwik_privacy_donottrack'] = [
'#type' => 'checkbox',
'#title' => $this->t('Universal web tracking opt-out'),
'#description' => $this->t('If enabled and your Piwik server receives the <a href="http://donottrack.us/">Do-Not-Track</a> header from the client browser, the Piwik server will not track the user. Compliance with Do Not Track could be purely voluntary, enforced by industry self-regulation, or mandated by state or federal law. Please accept your visitors privacy. If they have opt-out from tracking and advertising, you should accept their personal decision.'),
'#default_value' => $config->get('privacy.donottrack'),
];
// Piwik page title tree view settings.
$form['page_title_hierarchy'] = [
'#type' => 'details',
'#title' => $this->t('Page titles hierarchy'),
'#description' => $this->t('This functionality enables a dynamically expandable tree view of your site page titles in your Piwik statistics. See in Piwik statistics under <em>Actions</em> > <em>Page titles</em>.'),
'#group' => 'page_title_hierarchy',
];
$form['page_title_hierarchy']['piwik_page_title_hierarchy'] = [
'#type' => 'checkbox',
'#title' => $this->t("Show page titles as hierarchy like breadcrumbs"),
'#description' => $this->t('By default Piwik tracks the current page title and shows you a flat list of the most popular titles. This enables a breadcrumbs like tree view.'),
'#default_value' => $config->get('page_title_hierarchy'),
];
$form['page_title_hierarchy']['piwik_page_title_hierarchy_exclude_home'] = [
'#type' => 'checkbox',
'#title' => $this->t('Hide home page from hierarchy'),
'#description' => $this->t('If enabled, the "Home" item will be removed from the hierarchy to flatten the structure in the Piwik statistics. Hits to the home page will still be counted, but for other pages the hierarchy will start at level Home+1.'),
'#default_value' => $config->get('page_title_hierarchy_exclude_home'),
];
// Custom variables.
$form['piwik_custom_var'] = [
'#description' => $this->t('You can add Piwiks <a href=":custom_var_documentation">Custom Variables</a> here. These will be added to every page that Piwik tracking code appears on. Custom variable names and values are limited to 200 characters in length. Keep the names and values as short as possible and expect long values to get trimmed. You may use tokens in custom variable names and values. Global and user tokens are always available; on node pages, node tokens are also available.', [':custom_var_documentation' => 'http://piwik.org/docs/custom-variables/']),
'#title' => $this->t('Custom variables'),
'#tree' => TRUE,
'#type' => 'details',
];
$form['piwik_custom_var']['slots'] = [
'#type' => 'table',
'#header' => [
['data' => $this->t('Slot')],
['data' => $this->t('Name')],
['data' => $this->t('Value')],
['data' => $this->t('Scope')],
],
];
$piwik_custom_vars = $config->get('custom.variable');
// Piwik supports up to 5 custom variables.
for ($i = 1; $i < 6; $i++) {
$form['piwik_custom_var']['slots'][$i]['slot'] = [
'#default_value' => $i,
'#description' => $this->t('Slot number'),
'#disabled' => TRUE,
'#size' => 1,
'#title' => $this->t('Custom variable slot #@slot', ['@slot' => $i]),
'#title_display' => 'invisible',
'#type' => 'textfield',
];
$form['piwik_custom_var']['slots'][$i]['name'] = [
'#default_value' => isset($piwik_custom_vars[$i]['name']) ? $piwik_custom_vars[$i]['name'] : '',
'#description' => $this->t('The custom variable name.'),
'#maxlength' => 100,
'#size' => 20,
'#title' => $this->t('Custom variable name #@slot', ['@slot' => $i]),
'#title_display' => 'invisible',
'#type' => 'textfield',
];
$form['piwik_custom_var']['slots'][$i]['value'] = [
'#default_value' => isset($piwik_custom_vars[$i]['value']) ? $piwik_custom_vars[$i]['value'] : '',
'#description' => $this->t('The custom variable value.'),
'#maxlength' => 255,
'#title' => $this->t('Custom variable value #@slot', ['@slot' => $i]),
'#title_display' => 'invisible',
'#type' => 'textfield',
'#element_validate' => [[get_class($this), 'tokenElementValidate']],
'#token_types' => ['node'],
];
if (\Drupal::moduleHandler()->moduleExists('token')) {
$form['piwik_custom_var']['slots'][$i]['value']['#element_validate'][] = 'token_element_validate';
}
$form['piwik_custom_var']['slots'][$i]['scope'] = [
'#default_value' => isset($piwik_custom_vars[$i]['scope']) ? $piwik_custom_vars[$i]['scope'] : '',
'#description' => $this->t('The scope for the custom variable.'),
'#title' => $this->t('Custom variable slot #@slot', ['@slot' => $i]),
'#title_display' => 'invisible',
'#type' => 'select',
'#options' => [
'visit' => $this->t('Visit'),
'page' => $this->t('Page'),
],
];
}
$form['piwik_custom_var']['piwik_custom_var_description'] = [
'#type' => 'item',
'#description' => $this->t("You can supplement Piwiks' basic IP address tracking of visitors by segmenting users based on custom variables. Make sure you will not associate (or permit any third party to associate) any data gathered from your websites (or such third parties' websites) with any personally identifying information from any source as part of your use (or such third parties' use) of the Piwik' service."),
];
if (\Drupal::moduleHandler()->moduleExists('token')) {
$form['piwik_custom_var']['piwik_custom_var_token_tree'] = [
'#theme' => 'token_tree_link',
'#token_types' => ['node'],
];
}
// Advanced feature configurations.
$form['advanced'] = [
'#type' => 'details',
'#title' => $this->t('Advanced settings'),
'#open' => FALSE,
];
$form['advanced']['piwik_cache'] = [
'#type' => 'checkbox',
'#title' => $this->t('Locally cache tracking code file'),
'#description' => $this->t('If checked, the tracking code file is retrieved from your Piwik site and cached locally. It is updated daily to ensure updates to tracking code are reflected in the local copy.'),
'#default_value' => $config->get('cache'),
];
// Allow for tracking of the originating node when viewing translation sets.
if (\Drupal::moduleHandler()->moduleExists('content_translation')) {
$form['advanced']['piwik_translation_set'] = [
'#type' => 'checkbox',
'#title' => $this->t('Track translation sets as one unit'),
'#description' => $this->t('When a node is part of a translation set, record statistics for the originating node instead. This allows for a translation set to be treated as a single unit.'),
'#default_value' => $config->get('translation_set'),
];
}
$user_access_add_js_snippets = !$this->currentUser()->hasPermission('add JS snippets for piwik');
$user_access_add_js_snippets_permission_warning = $user_access_add_js_snippets ? ' <em>' . $this->t('This field has been disabled because you do not have sufficient permissions to edit it.') . '</em>' : '';
$form['advanced']['codesnippet'] = [
'#type' => 'details',
'#title' => $this->t('Custom JavaScript code'),
'#open' => TRUE,
'#description' => $this->t('You can add custom Piwik <a href=":snippets">code snippets</a> here. These will be added to every page that Piwik appears on. <strong>Do not include the &lt;script&gt; tags</strong>, and always end your code with a semicolon (;).', [':snippets' => 'http://piwik.org/docs/javascript-tracking/']),
];
$form['advanced']['codesnippet']['piwik_codesnippet_before'] = [
'#type' => 'textarea',
'#title' => $this->t('Code snippet (before)'),
'#default_value' => $config->get('codesnippet.before'),
'#disabled' => $user_access_add_js_snippets,
'#rows' => 5,
'#description' => $this->t('Code in this textarea will be added <strong>before</strong> _paq.push(["trackPageView"]).') . $user_access_add_js_snippets_permission_warning,
];
$form['advanced']['codesnippet']['piwik_codesnippet_after'] = [
'#type' => 'textarea',
'#title' => $this->t('Code snippet (after)'),
'#default_value' => $config->get('codesnippet.after'),
'#disabled' => $user_access_add_js_snippets,
'#rows' => 5,
'#description' => $this->t('Code in this textarea will be added <strong>after</strong> _paq.push(["trackPageView"]). This is useful if you\'d like to track a site in two accounts.') . $user_access_add_js_snippets_permission_warning,
];
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
parent::validateForm($form, $form_state);
// Custom variables validation.
foreach ($form_state->getValue(['piwik_custom_var', 'slots']) as $custom_var) {
$form_state->setValue(['piwik_custom_var', 'slots', $custom_var['slot'], 'name'], trim($custom_var['name']));
$form_state->setValue(['piwik_custom_var', 'slots', $custom_var['slot'], 'value'], trim($custom_var['value']));
// Validate empty names/values.
if (empty($custom_var['name']) && !empty($custom_var['value'])) {
$form_state->setErrorByName("piwik_custom_var][slots][" . $custom_var['slot'] . "][name", t('The custom variable @slot-number requires a <em>Name</em> if a <em>Value</em> has been provided.', ['@slot-number' => $custom_var['slot']]));
}
elseif (!empty($custom_var['name']) && empty($custom_var['value'])) {
$form_state->setErrorByName("piwik_custom_var][slots][" . $custom_var['slot'] . "][value", t('The custom variable @slot-number requires a <em>Value</em> if a <em>Name</em> has been provided.', ['@slot-number' => $custom_var['slot']]));
}
}
$form_state->setValue('piwik_custom_var', $form_state->getValue(['piwik_custom_var', 'slots']));
// Trim some text area values.
$form_state->setValue('piwik_site_id', trim($form_state->getValue('piwik_site_id')));
$form_state->setValue('piwik_visibility_request_path_pages', trim($form_state->getValue('piwik_visibility_request_path_pages')));
$form_state->setValue('piwik_codesnippet_before', trim($form_state->getValue('piwik_codesnippet_before')));
$form_state->setValue('piwik_codesnippet_after', trim($form_state->getValue('piwik_codesnippet_after')));
$form_state->setValue('piwik_visibility_user_role_roles', array_filter($form_state->getValue('piwik_visibility_user_role_roles')));
$form_state->setValue('piwik_trackmessages', array_filter($form_state->getValue('piwik_trackmessages')));
if (!preg_match('/^\d{1,}$/', $form_state->getValue('piwik_site_id'))) {
$form_state->setErrorByName('piwik_site_id', t('A valid Piwik site ID is an integer only.'));
}
$url = $form_state->getValue('piwik_url_http') . 'piwik.php';
try {
$result = \Drupal::httpClient()->get($url);
if ($result->getStatusCode() != 200 && $form_state->getValue('piwik_url_skiperror') == FALSE) {
$form_state->setErrorByName('piwik_url_http', t('The validation of "@url" failed with error "@error" (HTTP code @code).', [
'@url' => UrlHelper::filterBadProtocol($url),
'@error' => $result->getReasonPhrase(),
'@code' => $result->getStatusCode(),
]));
}
}
catch (RequestException $exception) {
$form_state->setErrorByName('piwik_url_http', t('The validation of "@url" failed with an exception "@error" (HTTP code @code).', [
'@url' => UrlHelper::filterBadProtocol($url),
'@error' => $exception->getMessage(),
'@code' => $exception->getCode(),
]));
}
$piwik_url_https = $form_state->getValue('piwik_url_https');
if (!empty($piwik_url_https)) {
$url = $piwik_url_https . 'piwik.php';
try {
$result = \Drupal::httpClient()->get($url);
if ($result->getStatusCode() != 200 && $form_state->getValue('piwik_url_skiperror') == FALSE) {
$form_state->setErrorByName('piwik_url_https', t('The validation of "@url" failed with error "@error" (HTTP code @code).', [
'@url' => UrlHelper::filterBadProtocol($url),
'@error' => $result->getReasonPhrase(),
'@code' => $result->getStatusCode(),
]));
}
}
catch (RequestException $exception) {
$form_state->setErrorByName('piwik_url_https', t('The validation of "@url" failed with an exception "@error" (HTTP code @code).', [
'@url' => UrlHelper::filterBadProtocol($url),
'@error' => $exception->getMessage(),
'@code' => $exception->getCode(),
]));
}
}
// Verify that every path is prefixed with a slash, but don't check PHP
// code snippets.
if ($form_state->getValue('piwik_visibility_request_path_mode') != 2) {
$pages = preg_split('/(\r\n?|\n)/', $form_state->getValue('piwik_visibility_request_path_pages'));
foreach ($pages as $page) {
if (strpos($page, '/') !== 0 && $page !== '<front>') {
$form_state->setErrorByName('piwik_visibility_request_path_pages', t('Path "@page" not prefixed with slash.', ['@page' => $page]));
// Drupal forms show one error only.
break;
}
}
}
// Clear obsolete local cache if cache has been disabled.
if ($form_state->isValueEmpty('piwik_cache') && $form['advanced']['piwik_cache']['#default_value']) {
piwik_clear_js_cache();
}
// This is for the Newbie's who cannot read a text area description.
if (preg_match('/(.*)<\/?script(.*)>(.*)/i', $form_state->getValue('piwik_codesnippet_before'))) {
$form_state->setErrorByName('piwik_codesnippet_before', t('Do not include the &lt;script&gt; tags in the javascript code snippets.'));
}
if (preg_match('/(.*)<\/?script(.*)>(.*)/i', $form_state->getValue('piwik_codesnippet_after'))) {
$form_state->setErrorByName('piwik_codesnippet_after', t('Do not include the &lt;script&gt; tags in the javascript code snippets.'));
}
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$config = $this->config('piwik.settings');
$config
->set('site_id', $form_state->getValue('piwik_site_id'))
->set('url_http', $form_state->getValue('piwik_url_http'))
->set('url_https', $form_state->getValue('piwik_url_https'))
->set('codesnippet.before', $form_state->getValue('piwik_codesnippet_before'))
->set('codesnippet.after', $form_state->getValue('piwik_codesnippet_after'))
->set('custom.variable', $form_state->getValue('piwik_custom_var'))
->set('domain_mode', $form_state->getValue('piwik_domain_mode'))
->set('track.files', $form_state->getValue('piwik_trackfiles'))
->set('track.files_extensions', $form_state->getValue('piwik_trackfiles_extensions'))
->set('track.colorbox', $form_state->getValue('piwik_trackcolorbox'))
->set('track.userid', $form_state->getValue('piwik_trackuserid'))
->set('track.mailto', $form_state->getValue('piwik_trackmailto'))
->set('track.messages', $form_state->getValue('piwik_trackmessages'))
->set('track.site_search', $form_state->getValue('piwik_site_search'))
->set('privacy.donottrack', $form_state->getValue('piwik_privacy_donottrack'))
->set('cache', $form_state->getValue('piwik_cache'))
->set('visibility.request_path_mode', $form_state->getValue('piwik_visibility_request_path_mode'))
->set('visibility.request_path_pages', $form_state->getValue('piwik_visibility_request_path_pages'))
->set('visibility.user_account_mode', $form_state->getValue('piwik_visibility_user_account_mode'))
->set('visibility.user_role_mode', $form_state->getValue('piwik_visibility_user_role_mode'))
->set('visibility.user_role_roles', $form_state->getValue('piwik_visibility_user_role_roles'))
->save();
if ($form_state->hasValue('piwik_translation_set')) {
$config->set('translation_set', $form_state->getValue('piwik_translation_set'))->save();
}
parent::submitForm($form, $form_state);
}
/**
* Validate a form element that should have tokens in it.
*
* For example:
* @code
* $form['my_node_text_element'] = [
* '#type' => 'textfield',
* '#title' => $this->t('Some text to token-ize that has a node context.'),
* '#default_value' => 'The title of this node is [node:title].',
* '#element_validate' => [[get_class($this), 'tokenElementValidate']],
* ];
* @endcode
*/
public static function tokenElementValidate(&$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);
$invalid_tokens = static::getForbiddenTokens($tokens);
if ($invalid_tokens) {
$form_state->setError($element, t('The %element-title is using the following forbidden tokens with personal identifying information: @invalid-tokens.', ['%element-title' => $element['#title'], '@invalid-tokens' => implode(', ', $invalid_tokens)]));
}
return $element;
}
/**
* Get an array of all forbidden tokens.
*
* @param array $value
* An array of token values.
*
* @return array
* A unique array of invalid tokens.
*/
protected static function getForbiddenTokens(array $value) {
$invalid_tokens = [];
$value_tokens = is_string($value) ? \Drupal::token()->scan($value) : $value;
foreach ($value_tokens as $tokens) {
if (array_filter($tokens, 'static::containsForbiddenToken')) {
$invalid_tokens = array_merge($invalid_tokens, array_values($tokens));
}
}
array_unique($invalid_tokens);
return $invalid_tokens;
}
/**
* Validate if string contains forbidden tokens not allowed by privacy rules.
*
* @param string $token_string
* A string with one or more tokens to be validated.
*
* @return bool
* TRUE if blacklisted token has been found, otherwise FALSE.
*/
protected static function containsForbiddenToken($token_string) {
// List of strings in tokens with personal identifying information not
// allowed for privacy reasons. See section 8.1 of the Google Analytics
// terms of use for more detailed information.
//
// This list can never ever be complete. For this reason it tries to use a
// regex and may kill a few other valid tokens, but it's the only way to
// protect users as much as possible from admins with illegal ideas.
//
// User tokens are not prefixed with colon to catch 'current-user' and
// 'user'.
//
// TODO: If someone have better ideas, share them, please!
$token_blacklist = [
':account-name]',
':author]',
':author:edit-url]',
':author:url]',
':author:path]',
':current-user]',
':current-user:original]',
':display-name]',
':fid]',
':mail]',
':name]',
':uid]',
':one-time-login-url]',
':owner]',
':owner:cancel-url]',
':owner:edit-url]',
':owner:url]',
':owner:path]',
'user:cancel-url]',
'user:edit-url]',
'user:url]',
'user:path]',
'user:picture]',
// addressfield_tokens.module
':first-name]',
':last-name]',
':name-line]',
':mc-address]',
':thoroughfare]',
':premise]',
// realname.module
':name-raw]',
// token.module
':ip-address]',
];
return preg_match('/' . implode('|', array_map('preg_quote', $token_blacklist)) . '/i', $token_string);
}
}
@@ -0,0 +1,27 @@
<?php
namespace Drupal\piwik\Plugin\migrate\process;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
/**
* This plugin flattens the custom variables array.
*
* @MigrateProcessPlugin(
* id = "piwik_custom_vars"
* )
*/
class PiwikCustomVars extends ProcessPluginBase {
/**
* Flatten custom vars array.
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
list($piwik_custom_vars) = $value;
return isset($piwik_custom_vars['slots']) ? $piwik_custom_vars['slots'] : [];
}
}
@@ -0,0 +1,30 @@
<?php
namespace Drupal\piwik\Plugin\migrate\process;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\Row;
use Drupal\migrate\MigrateSkipRowException;
/**
* If the source evaluates to empty, we skip the current row.
*
* @MigrateProcessPlugin(
* id = "piwik_skip_row_if_not_set",
* handle_multiples = TRUE
* )
*/
class PiwikSkipRowIfNotSet extends ProcessPluginBase {
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
if (!isset($value[$this->configuration['module']][$this->configuration['key']])) {
throw new MigrateSkipRowException();
}
return $value[$this->configuration['module']][$this->configuration['key']];
}
}
@@ -0,0 +1,113 @@
<?php
namespace Drupal\piwik\Plugin\migrate\process;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\MigrateSkipRowException;
use Drupal\migrate\Plugin\MigrateProcessInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Prefixes paths with a slash.
*
* @MigrateProcessPlugin(
* id = "piwik_visibility_pages"
* )
*/
class PiwikVisibilityPages extends ProcessPluginBase implements ContainerFactoryPluginInterface {
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The migration process plugin, configured for lookups in the d6_user_role
* and d7_user_role migrations.
*
* @var \Drupal\migrate\Plugin\MigrateProcessInterface
*/
protected $migrationPlugin;
/**
* Whether or not to skip Piwik that use PHP for visibility. Only applies if
* the PHP module is not enabled.
*
* @var bool
*/
protected $skipPHP = FALSE;
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, ModuleHandlerInterface $module_handler, MigrateProcessInterface $migration_plugin) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->moduleHandler = $module_handler;
$this->migrationPlugin = $migration_plugin;
if (isset($configuration['skip_php'])) {
$this->skipPHP = $configuration['skip_php'];
}
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration = NULL) {
$migration_configuration = [
'migration' => [
'd6_user_role',
'd7_user_role',
],
];
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('module_handler'),
$container->get('plugin.manager.migrate.process')->createInstance('migration', $migration_configuration, $migration)
);
}
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
list($old_visibility, $pages) = $value;
$request_path_pages = '';
if ($pages) {
// 2 == BLOCK_VISIBILITY_PHP in Drupal 6 and 7.
if ($old_visibility == 2) {
// If the PHP module is present, migrate the visibility code unaltered.
if ($this->moduleHandler->moduleExists('php')) {
$request_path_pages = $pages;
}
// Skip the row if we're configured to. If not, we don't need to do
// anything else -- the block will simply have no PHP or request_path
// visibility configuration.
elseif ($this->skipPHP) {
throw new MigrateSkipRowException();
}
}
else {
$paths = preg_split("(\r\n?|\n)", $pages);
foreach ($paths as $key => $path) {
$paths[$key] = $path === '<front>' ? $path : '/' . ltrim($path, '/');
}
$request_path_pages = implode("\n", $paths);
}
}
return $request_path_pages;
}
}
@@ -0,0 +1,87 @@
<?php
namespace Drupal\piwik\Plugin\migrate\process;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\Plugin\MigrateProcessInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Converts D7 role ids to D8 role names.
*
* @MigrateProcessPlugin(
* id = "piwik_visibility_roles"
* )
*/
class PiwikVisibilityRoles extends ProcessPluginBase implements ContainerFactoryPluginInterface {
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The migration process plugin, configured for lookups in the d6_user_role
* and d7_user_role migrations.
*
* @var \Drupal\migrate\Plugin\MigrateProcessInterface
*/
protected $migrationPlugin;
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, ModuleHandlerInterface $module_handler, MigrateProcessInterface $migration_plugin) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->moduleHandler = $module_handler;
$this->migrationPlugin = $migration_plugin;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration = NULL) {
$migration_configuration = [
'migration' => [
'd6_user_role',
'd7_user_role',
],
];
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('module_handler'),
$container->get('plugin.manager.migrate.process')->createInstance('migration', $migration_configuration, $migration)
);
}
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
list($roles) = $value;
// Remove role IDs disabled in D6/D7.
$roles = array_filter($roles);
$user_role_roles = [];
if ($roles) {
foreach ($roles as $key => $role_id) {
$roles[$key] = $this->migrationPlugin->transform($role_id, $migrate_executable, $row, $destination_property);
}
$user_role_roles = array_combine($roles, $roles);
}
return $user_role_roles;
}
}
@@ -0,0 +1,221 @@
<?php
namespace Drupal\piwik\Tests;
use Drupal\Core\Session\AccountInterface;
use Drupal\simpletest\WebTestBase;
/**
* Test basic functionality of Piwik module.
*
* @group Piwik
*/
class PiwikBasicTest extends WebTestBase {
/**
* User without permissions to use snippets.
*
* @var \Drupal\user\UserInterface
*/
protected $noSnippetUser;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['piwik'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$permissions = [
'access administration pages',
'administer piwik',
];
// User to set up piwik.
$this->noSnippetUser = $this->drupalCreateUser($permissions);
$permissions[] = 'add JS snippets for piwik';
$this->admin_user = $this->drupalCreateUser($permissions);
$this->drupalLogin($this->admin_user);
}
/**
* Tests if configuration is possible.
*/
public function testPiwikConfiguration() {
// Check for setting page's presence.
$this->drupalGet('admin/config/system/piwik');
$this->assertRaw(t('Piwik site ID'), '[testPiwikConfiguration]: Settings page displayed.');
// Check for account code validation.
$edit['piwik_site_id'] = $this->randomMachineName(2);
$edit['piwik_url_http'] = 'http://www.example.com/piwik/';
$this->drupalPostForm('admin/config/system/piwik', $edit, 'Save configuration');
$this->assertRaw(t('A valid Piwik site ID is an integer only.'), '[testPiwikConfiguration]: Invalid Piwik site ID number validated.');
// Verify that invalid URLs throw a form error.
$edit = [];
$edit['piwik_site_id'] = 1;
$edit['piwik_url_http'] = 'http://www.example.com/piwik/';
$edit['piwik_url_https'] = 'https://www.example.com/piwik/';
$this->drupalPostForm('admin/config/system/piwik', $edit, t('Save configuration'));
$this->assertRaw('The validation of "http://www.example.com/piwik/piwik.php" failed with an exception', '[testPiwikConfiguration]: HTTP URL exception shown.');
$this->assertRaw('The validation of "https://www.example.com/piwik/piwik.php" failed with an exception', '[testPiwikConfiguration]: HTTPS URL exception shown.');
// User should have access to code snippets.
$this->assertFieldByName('piwik_codesnippet_before');
$this->assertFieldByName('piwik_codesnippet_after');
$this->assertNoFieldByXPath("//textarea[@name='piwik_codesnippet_before' and @disabled='disabled']", NULL, '"Code snippet (before)" is enabled.');
$this->assertNoFieldByXPath("//textarea[@name='piwik_codesnippet_after' and @disabled='disabled']", NULL, '"Code snippet (after)" is enabled.');
// Login as user without JS permissions.
$this->drupalLogin($this->noSnippetUser);
$this->drupalGet('admin/config/system/piwik');
// User should *not* have access to snippets, but create fields.
$this->assertFieldByName('piwik_codesnippet_before');
$this->assertFieldByName('piwik_codesnippet_after');
$this->assertFieldByXPath("//textarea[@name='piwik_codesnippet_before' and @disabled='disabled']", NULL, '"Code snippet (before)" is disabled.');
$this->assertFieldByXPath("//textarea[@name='piwik_codesnippet_after' and @disabled='disabled']", NULL, '"Code snippet (after)" is disabled.');
}
/**
* Tests if page visibility works.
*/
public function testPiwikPageVisibility() {
$site_id = '1';
$this->config('piwik.settings')->set('site_id', $site_id)->save();
$this->config('piwik.settings')->set('url_http', 'http://www.example.com/piwik/')->save();
$this->config('piwik.settings')->set('url_https', 'https://www.example.com/piwik/')->save();
// Show tracking on "every page except the listed pages".
$this->config('piwik.settings')->set('visibility.request_path_mode', 0)->save();
// Disable tracking one "admin*" pages only.
$this->config('piwik.settings')->set('visibility.request_path_pages', "/admin\n/admin/*")->save();
// Enable tracking only for authenticated users only.
$this->config('piwik.settings')->set('visibility.user_role_roles', [AccountInterface::AUTHENTICATED_ROLE => AccountInterface::AUTHENTICATED_ROLE])->save();
// Check tracking code visibility.
$this->drupalGet('');
$this->assertRaw('u+"piwik.php"', '[testPiwikPageVisibility]: Tracking code is displayed for authenticated users.');
// Test whether tracking code is not included on pages to omit.
$this->drupalGet('admin');
$this->assertNoRaw('u+"piwik.php"', '[testPiwikPageVisibility]: Tracking code is not displayed on admin page.');
$this->drupalGet('admin/config/system/piwik');
// Checking for tracking URI here, as $site_id is displayed in the form.
$this->assertNoRaw('u+"piwik.php"', '[testPiwikPageVisibility]: Tracking code is not displayed on admin subpage.');
// Test whether tracking code display is properly flipped.
$this->config('piwik.settings')->set('visibility.request_path_mode', 1)->save();
$this->drupalGet('admin');
$this->assertRaw('u+"piwik.php"', '[testPiwikPageVisibility]: Tracking code is displayed on admin page.');
$this->drupalGet('admin/config/system/piwik');
// Checking for tracking URI here, as $site_id is displayed in the form.
$this->assertRaw('u+"piwik.php"', '[testPiwikPageVisibility]: Tracking code is displayed on admin subpage.');
$this->drupalGet('');
$this->assertNoRaw('u+"piwik.php"', '[testPiwikPageVisibility]: Tracking code is NOT displayed on front page.');
// Test whether tracking code is not display for anonymous.
$this->drupalLogout();
$this->drupalGet('');
$this->assertNoRaw('u+"piwik.php"', '[testPiwikPageVisibility]: Tracking code is NOT displayed for anonymous.');
// Switch back to every page except the listed pages.
$this->config('piwik.settings')->set('visibility.request_path_mode', 0)->save();
// Enable tracking code for all user roles.
$this->config('piwik.settings')->set('visibility.user_role_roles', [])->save();
// Test whether 403 forbidden tracking code is shown if user has no access.
$this->drupalGet('admin');
$this->assertRaw('"403/URL = "', '[testPiwikPageVisibility]: 403 Forbidden tracking code shown if user has no access.');
// Test whether 404 not found tracking code is shown on non-existent pages.
$this->drupalGet($this->randomMachineName(64));
$this->assertRaw('"404/URL = "', '[testPiwikPageVisibility]: 404 Not Found tracking code shown on non-existent page.');
}
/**
* Tests if tracking code is properly added to the page.
*/
public function testPiwikTrackingCode() {
$site_id = '2';
$this->config('piwik.settings')->set('site_id', $site_id)->save();
$this->config('piwik.settings')->set('url_http', 'http://www.example.com/piwik/')->save();
$this->config('piwik.settings')->set('url_https', 'https://www.example.com/piwik/')->save();
// Show tracking code on every page except the listed pages.
$this->config('piwik.settings')->set('visibility.request_path_mode', 0)->save();
// Enable tracking code for all user roles.
$this->config('piwik.settings')->set('visibility.user_role_roles', [])->save();
/* Sample JS code as added to page:
<script type="text/javascript">
var _paq = _paq || [];
(function(){
var u=(("https:" == document.location.protocol) ? "https://{$PIWIK_URL}" : "http://{$PIWIK_URL}");
_paq.push(['setSiteId', {$IDSITE}]);
_paq.push(['setTrackerUrl', u+'piwik.php']);
_paq.push(['trackPageView']);
var d=document,
g=d.createElement('script'),
s=d.getElementsByTagName('script')[0];
g.type='text/javascript';
g.defer=true;
g.async=true;
g.src=u+'piwik.js';
s.parentNode.insertBefore(g,s);
})();
</script>
*/
// Test whether tracking code uses latest JS.
$this->config('piwik.settings')->set('cache', 0)->save();
$this->drupalGet('');
$this->assertRaw('u+"piwik.php"', '[testPiwikTrackingCode]: Latest tracking code used.');
// Test if tracking of User ID is enabled.
$this->config('piwik.settings')->set('track.userid', 1)->save();
$this->drupalGet('');
$this->assertRaw('_paq.push(["setUserId", ', '[testPiwikTrackingCode]: Tracking code for User ID is enabled.');
// Test if tracking of User ID is disabled.
$this->config('piwik.settings')->set('track.userid', 0)->save();
$this->drupalGet('');
$this->assertNoRaw('_paq.push(["setUserId", ', '[testPiwikTrackingCode]: Tracking code for User ID is disabled.');
// Test whether single domain tracking is active.
$this->drupalGet('');
$this->assertNoRaw('_paq.push(["setCookieDomain"', '[testPiwikTrackingCode]: Single domain tracking is active.');
// Enable "One domain with multiple subdomains".
$this->config('piwik.settings')->set('domain_mode', 1)->save();
$this->drupalGet('');
// Test may run on localhost, an ipaddress or real domain name.
// TODO: Workaround to run tests successfully. This feature cannot tested
// reliable.
global $cookie_domain;
if (count(explode('.', $cookie_domain)) > 2 && !is_numeric(str_replace('.', '', $cookie_domain))) {
$this->assertRaw('_paq.push(["setCookieDomain"', '[testPiwikTrackingCode]: One domain with multiple subdomains is active on real host.');
}
else {
// Special cases, Localhost and IP addresses don't show 'setCookieDomain'.
$this->assertNoRaw('_paq.push(["setCookieDomain"', '[testPiwikTrackingCode]: One domain with multiple subdomains may be active on localhost (test result is not reliable).');
}
// Test whether the BEFORE and AFTER code is added to the tracker.
$this->config('piwik.settings')->set('codesnippet.before', '_paq.push(["setLinkTrackingTimer", 250]);')->save();
$this->config('piwik.settings')->set('codesnippet.after', '_paq.push(["t2.setSiteId", 2]);if(1 == 1 && 2 < 3 && 2 > 1){console.log("Piwik: Custom condition works.");}_gaq.push(["t2.trackPageView"]);')->save();
$this->drupalGet('');
$this->assertRaw('setLinkTrackingTimer', '[testPiwikTrackingCode]: Before codesnippet has been found with "setLinkTrackingTimer" set.');
$this->assertRaw('t2.trackPageView', '[testPiwikTrackingCode]: After codesnippet with "t2" tracker has been found.');
$this->assertRaw('if(1 == 1 && 2 < 3 && 2 > 1){console.log("Piwik: Custom condition works.");}', '[testPiwikTrackingCode]: JavaScript code is not HTML escaped.');
}
}
@@ -0,0 +1,59 @@
<?php
namespace Drupal\piwik\Tests;
use Drupal\simpletest\WebTestBase;
use Drupal\Component\Serialization\Json;
/**
* Test custom url functionality of Piwik module.
*
* @group Piwik
*/
class PiwikCustomUrls extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['piwik'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$permissions = [
'access administration pages',
'administer piwik',
'administer modules',
'administer site configuration',
];
// User to set up piwik.
$this->admin_user = $this->drupalCreateUser($permissions);
}
/**
* Tests if user password page urls are overridden.
*/
public function testPiwikUserPasswordPage() {
$base_path = base_path();
$site_id = '1';
$this->config('piwik.settings')->set('site_id', $site_id)->save();
$this->config('piwik.settings')->set('url_http', 'http://www.example.com/piwik/')->save();
$this->config('piwik.settings')->set('url_https', 'https://www.example.com/piwik/')->save();
$this->drupalGet('user/password', ['query' => ['name' => 'foo']]);
$this->assertRaw('_paq.push(["setCustomUrl", ' . Json::encode($base_path . 'user/password') . ']);');
$this->drupalGet('user/password', ['query' => ['name' => 'foo@example.com']]);
$this->assertRaw('_paq.push(["setCustomUrl", ' . Json::encode($base_path . 'user/password') . ']);');
$this->drupalGet('user/password');
$this->assertNoRaw('_paq.push(["setCustomUrl", "', '[testPiwikCustomUrls]: Custom url not set.');
}
}
@@ -0,0 +1,133 @@
<?php
namespace Drupal\piwik\Tests;
use Drupal\Component\Serialization\Json;
use Drupal\simpletest\WebTestBase;
/**
* Test custom variables functionality of Piwik module.
*
* @group Piwik
*/
class PiwikCustomVariablesTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['piwik', 'token'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$permissions = [
'access administration pages',
'administer piwik',
];
// User to set up piwik.
$this->admin_user = $this->drupalCreateUser($permissions);
}
/**
* Tests if custom variables are properly added to the page.
*/
public function testPiwikCustomVariables() {
$site_id = '3';
$this->config('piwik.settings')->set('site_id', $site_id)->save();
$this->config('piwik.settings')->set('url_http', 'http://www.example.com/piwik/')->save();
$this->config('piwik.settings')->set('url_https', 'https://www.example.com/piwik/')->save();
// Basic test if the feature works.
$custom_vars = [
1 => [
'slot' => 1,
'name' => 'Foo 1',
'value' => 'Bar 1',
'scope' => 'visit',
],
2 => [
'slot' => 2,
'name' => 'Foo 2',
'value' => 'Bar 2',
'scope' => 'page',
],
3 => [
'slot' => 3,
'name' => 'Foo 3',
'value' => 'Bar 3',
'scope' => 'page',
],
4 => [
'slot' => 4,
'name' => 'Foo 4',
'value' => 'Bar 4',
'scope' => 'visit',
],
5 => [
'slot' => 5,
'name' => 'Foo 5',
'value' => 'Bar 5',
'scope' => 'visit',
],
];
$this->config('piwik.settings')->set('custom.variable', $custom_vars)->save();
$this->drupalGet('');
foreach ($custom_vars as $slot) {
$this->assertRaw('_paq.push(["setCustomVariable", ' . Json::encode($slot['slot']) . ', ' . Json::encode($slot['name']) . ', ' . Json::encode($slot['value']) . ', ' . Json::encode($slot['scope']) . ']);', '[testPiwikCustomVariables]: setCustomVariable ' . $slot['slot'] . ' is shown.');
}
// Test whether tokens are replaced in custom variable names.
$site_slogan = $this->randomMachineName(16);
$this->config('system.site')->set('slogan', $site_slogan)->save();
$custom_vars = [
1 => [
'slot' => 1,
'name' => 'Name: [site:slogan]',
'value' => 'Value: [site:slogan]',
'scope' => 'visit',
],
2 => [
'slot' => 2,
'name' => '',
'value' => $this->randomMachineName(16),
'scope' => 'page',
],
3 => [
'slot' => 3,
'name' => $this->randomMachineName(16),
'value' => '',
'scope' => 'visit',
],
4 => [
'slot' => 4,
'name' => '',
'value' => '',
'scope' => 'page',
],
5 => [
'slot' => 5,
'name' => '',
'value' => '',
'scope' => 'visit',
],
];
$this->config('piwik.settings')->set('custom.variable', $custom_vars)->save();
$this->verbose('<pre>' . print_r($custom_vars, TRUE) . '</pre>');
$this->drupalGet('');
$this->assertRaw('_paq.push(["setCustomVariable", 1, ' . Json::encode("Name: $site_slogan") . ', ' . Json::encode("Value: $site_slogan") . ', "visit"]', '[testPiwikCustomVariables]: Tokens have been replaced in custom variable.');
$this->assertNoRaw('_paq.push(["setCustomVariable", 2,', '[testPiwikCustomVariables]: Value with empty name is not shown.');
$this->assertNoRaw('_paq.push(["setCustomVariable", 3,', '[testPiwikCustomVariables]: Name with empty value is not shown.');
$this->assertNoRaw('_paq.push(["setCustomVariable", 4,', '[testPiwikCustomVariables]: Empty name and value is not shown.');
$this->assertNoRaw('_paq.push(["setCustomVariable", 5,', '[testPiwikCustomVariables]: Empty name and value is not shown.');
}
}
@@ -0,0 +1,107 @@
<?php
namespace Drupal\piwik\Tests;
use Drupal\Component\Utility\Html;
use Drupal\simpletest\WebTestBase;
/**
* Test php filter functionality of Piwik module.
*
* @group Piwik
*/
class PiwikPhpFilterTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['piwik', 'php'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
// Administrator with all permissions.
$permissions_admin_user = [
'access administration pages',
'administer piwik',
'use PHP for piwik tracking visibility',
];
$this->admin_user = $this->drupalCreateUser($permissions_admin_user);
// Administrator who cannot configure tracking visibility with PHP.
$permissions_delegated_admin_user = [
'access administration pages',
'administer piwik',
];
$this->delegated_admin_user = $this->drupalCreateUser($permissions_delegated_admin_user);
}
/**
* Tests if PHP module integration works.
*/
public function testPiwikPhpFilter() {
$site_id = '1';
$this->drupalLogin($this->admin_user);
$edit = [];
$edit['piwik_site_id'] = $site_id;
$edit['piwik_url_http'] = 'http://www.example.com/piwik/';
$edit['piwik_url_https'] = 'https://www.example.com/piwik/';
// Skip url check errors in automated tests.
$edit['piwik_url_skiperror'] = TRUE;
$edit['piwik_visibility_request_path_mode'] = 2;
$edit['piwik_visibility_request_path_pages'] = '<?php return 0; ?>';
$this->drupalPostForm('admin/config/system/piwik', $edit, t('Save configuration'));
// Compare saved setting with posted setting.
$piwik_visibility_request_path_pages = \Drupal::config('piwik.settings')->get('visibility.request_path_pages');
$this->assertEqual('<?php return 0; ?>', $piwik_visibility_request_path_pages, '[testPiwikPhpFilter]: PHP code snippet is intact.');
// Check tracking code visibility.
$this->config('piwik.settings')->set('visibility.request_path_pages', '<?php return TRUE; ?>')->save();
$this->drupalGet('');
$this->assertRaw('u+"piwik.php"', '[testPiwikPhpFilter]: Tracking is displayed on frontpage page.');
$this->drupalGet('admin');
$this->assertRaw('u+"piwik.php"', '[testPiwikPhpFilter]: Tracking is displayed on admin page.');
$this->config('piwik.settings')->set('visibility.request_path_pages', '<?php return FALSE; ?>')->save();
$this->drupalGet('');
$this->assertNoRaw('u+"piwik.php"', '[testPiwikPhpFilter]: Tracking is not displayed on frontpage page.');
// Test administration form.
$this->config('piwik.settings')->set('visibility.request_path_pages', '<?php return TRUE; ?>')->save();
$this->drupalGet('admin/config/system/piwik');
$this->assertRaw(t('Pages on which this PHP code returns <code>TRUE</code> (experts only)'), '[testPiwikPhpFilter]: Permission to administer PHP for tracking visibility.');
$this->assertRaw(Html::escape('<?php return TRUE; ?>'), '[testPiwikPhpFilter]: PHP code snippted is displayed.');
// Login the delegated user and check if fields are visible.
$this->drupalLogin($this->delegated_admin_user);
$this->drupalGet('admin/config/system/piwik');
$this->assertNoRaw(t('Pages on which this PHP code returns <code>TRUE</code> (experts only)'), '[testPiwikPhpFilter]: No permission to administer PHP for tracking visibility.');
$this->assertRaw(Html::escape('<?php return TRUE; ?>'), '[testPiwikPhpFilter]: No permission to view PHP code snippted.');
// Set a different value and verify that this is still the same after the
// post.
$this->config('piwik.settings')->set('visibility.request_path_pages', '<?php return 0; ?>')->save();
$edit = [];
$edit['piwik_site_id'] = $site_id;
$edit['piwik_url_http'] = 'http://www.example.com/piwik/';
$edit['piwik_url_https'] = 'https://www.example.com/piwik/';
// Required for testing only.
$edit['piwik_url_skiperror'] = TRUE;
$this->drupalPostForm('admin/config/system/piwik', $edit, t('Save configuration'));
// Compare saved setting with posted setting.
$piwik_visibility_request_path_mode = $this->config('piwik.settings')->get('visibility.request_path_mode');
$piwik_visibility_request_path_pages = $this->config('piwik.settings')->get('visibility.request_path_pages');
$this->assertEqual(2, $piwik_visibility_request_path_mode, '[testPiwikPhpFilter]: Pages on which this PHP code returns TRUE is selected.');
$this->assertEqual('<?php return 0; ?>', $piwik_visibility_request_path_pages, '[testPiwikPhpFilter]: PHP code snippet is intact.');
}
}
@@ -0,0 +1,107 @@
<?php
namespace Drupal\piwik\Tests;
use Drupal\Core\Session\AccountInterface;
use Drupal\simpletest\WebTestBase;
/**
* Test roles functionality of Piwik module.
*
* @group Piwik
*/
class PiwikRolesTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['piwik'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$permissions = [
'access administration pages',
'administer piwik',
];
// User to set up piwik.
$this->admin_user = $this->drupalCreateUser($permissions);
}
/**
* Tests if roles based tracking works.
*/
public function testPiwikRolesTracking() {
$site_id = '1';
$this->config('piwik.settings')->set('site_id', $site_id)->save();
$this->config('piwik.settings')->set('url_http', 'http://www.example.com/piwik/')->save();
$this->config('piwik.settings')->set('url_https', 'https://www.example.com/piwik/')->save();
// Test if the default settings are working as expected.
// Add to the selected roles only.
$this->config('piwik.settings')->set('visibility.user_role_mode', 0)->save();
// Enable tracking for all users.
$this->config('piwik.settings')->set('visibility.user_role_roles', [])->save();
// Check tracking code visibility.
$this->drupalGet('');
$this->assertRaw('u+"piwik.php"', '[testPiwikRoleVisibility]: Tracking code is displayed for anonymous users on frontpage with default settings.');
$this->drupalGet('admin');
$this->assertRaw('"403/URL = "', '[testPiwikRoleVisibility]: 403 Forbidden tracking code is displayed for anonymous users in admin section with default settings.');
$this->drupalLogin($this->admin_user);
$this->drupalGet('');
$this->assertRaw('u+"piwik.php"', '[testPiwikRoleVisibility]: Tracking code is displayed for authenticated users on frontpage with default settings.');
$this->drupalGet('admin');
$this->assertNoRaw('u+"piwik.php"', '[testPiwikRoleVisibility]: Tracking code is NOT displayed for authenticated users in admin section with default settings.');
// Test if the non-default settings are working as expected.
// Enable tracking only for authenticated users.
$this->config('piwik.settings')->set('visibility.user_role_roles', [AccountInterface::AUTHENTICATED_ROLE => AccountInterface::AUTHENTICATED_ROLE])->save();
$this->drupalGet('');
$this->assertRaw('u+"piwik.php"', '[testPiwikRoleVisibility]: Tracking code is displayed for authenticated users only on frontpage.');
$this->drupalLogout();
$this->drupalGet('');
$this->assertNoRaw('u+"piwik.php"', '[testPiwikRoleVisibility]: Tracking code is NOT displayed for anonymous users on frontpage.');
// Add to every role except the selected ones.
$this->config('piwik.settings')->set('visibility.user_role_mode', 1)->save();
// Enable tracking for all users.
$this->config('piwik.settings')->set('visibility.user_role_roles', [])->save();
// Check tracking code visibility.
$this->drupalGet('');
$this->assertRaw('u+"piwik.php"', '[testPiwikRoleVisibility]: Tracking code is added to every role and displayed for anonymous users.');
$this->drupalGet('admin');
$this->assertRaw('"403/URL = "', '[testPiwikRoleVisibility]: 403 Forbidden tracking code is shown for anonymous users if every role except the selected ones is selected.');
$this->drupalLogin($this->admin_user);
$this->drupalGet('');
$this->assertRaw('u+"piwik.php"', '[testPiwikRoleVisibility]: Tracking code is added to every role and displayed on frontpage for authenticated users.');
$this->drupalGet('admin');
$this->assertNoRaw('u+"piwik.php"', '[testPiwikRoleVisibility]: Tracking code is added to every role and NOT displayed in admin section for authenticated users.');
// Disable tracking for authenticated users.
$this->config('piwik.settings')->set('visibility.user_role_roles', [AccountInterface::AUTHENTICATED_ROLE => AccountInterface::AUTHENTICATED_ROLE])->save();
$this->drupalGet('');
$this->assertNoRaw('u+"piwik.php"', '[testPiwikRoleVisibility]: Tracking code is NOT displayed on frontpage for excluded authenticated users.');
$this->drupalGet('admin');
$this->assertNoRaw('u+"piwik.php"', '[testPiwikRoleVisibility]: Tracking code is NOT displayed in admin section for excluded authenticated users.');
$this->drupalLogout();
$this->drupalGet('');
$this->assertRaw('u+"piwik.php"', '[testPiwikRoleVisibility]: Tracking code is displayed on frontpage for included anonymous users.');
}
}
@@ -0,0 +1,98 @@
<?php
namespace Drupal\piwik\Tests;
use Drupal\simpletest\WebTestBase;
/**
* Test search functionality of Piwik module.
*
* @group Piwik
*/
class PiwikSearchTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['piwik', 'search', 'node'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']);
$permissions = [
'access administration pages',
'administer piwik',
'search content',
'create page content',
'edit own page content',
];
// User to set up piwik.
$this->admin_user = $this->drupalCreateUser($permissions);
$this->drupalLogin($this->admin_user);
}
/**
* Tests if search tracking is properly added to the page.
*/
public function testPiwikSearchTracking() {
$site_id = '1';
$this->config('piwik.settings')->set('site_id', $site_id)->save();
$this->config('piwik.settings')->set('url_http', 'http://www.example.com/piwik/')->save();
$this->config('piwik.settings')->set('url_https', 'https://www.example.com/piwik/')->save();
// Check tracking code visibility.
$this->drupalGet('');
$this->assertRaw($site_id, '[testPiwikSearch]: Tracking code is displayed for authenticated users.');
$this->drupalGet('search/node');
$this->assertNoRaw('_paq.push(["trackSiteSearch", ', '[testPiwikSearch]: Search tracker not added to page.');
// Enable site search support.
$this->config('piwik.settings')->set('track.site_search', 1)->save();
// Search for random string.
$search = [];
$search['keys'] = $this->randomMachineName(8);
// Create a node to search for.
// Create a node.
$edit = [];
$edit['title[0][value]'] = 'This is a test title';
$edit['body[0][value]'] = 'This test content contains ' . $search['keys'] . ' string.';
// Fire a search, it's expected to get 0 results.
$this->drupalPostForm('search/node', $search, t('Search'));
$this->assertRaw('_paq.push(["trackSiteSearch", ', '[testPiwikSearch]: Search results tracker is displayed.');
$this->assertRaw('window.piwik_search_results = 0;', '[testPiwikSearch]: Search yielded no results.');
// Save the node.
$this->drupalPostForm('node/add/page', $edit, t('Save'));
$this->assertText(t('@type @title has been created.', ['@type' => 'Basic page', '@title' => $edit['title[0][value]']]), 'Basic page created.');
// Index the node or it cannot found.
$this->cronRun();
$this->drupalPostForm('search/node', $search, t('Search'));
$this->assertRaw('_paq.push(["trackSiteSearch", ', '[testPiwikSearch]: Search results tracker is displayed.');
$this->assertRaw('window.piwik_search_results = 1;', '[testPiwikSearch]: One search result found.');
$this->drupalPostForm('node/add/page', $edit, t('Save'));
$this->assertText(t('@type @title has been created.', ['@type' => 'Basic page', '@title' => $edit['title[0][value]']]), 'Basic page created.');
// Index the node or it cannot found.
$this->cronRun();
$this->drupalPostForm('search/node', $search, t('Search'));
$this->assertRaw('_paq.push(["trackSiteSearch", ', '[testPiwikSearch]: Search results tracker is displayed.');
$this->assertRaw('window.piwik_search_results = 2;', '[testPiwikSearch]: Two search results found.');
}
}
@@ -0,0 +1,69 @@
<?php
namespace Drupal\piwik\Tests;
use Drupal\simpletest\WebTestBase;
/**
* Test status messages functionality of Piwik module.
*
* @group Piwik
*/
class PiwikStatusMessagesTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['piwik', 'piwik_test'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$permissions = [
'access administration pages',
'administer piwik',
];
// User to set up piwik.
$this->admin_user = $this->drupalCreateUser($permissions);
}
/**
* Tests if status messages tracking is properly added to the page.
*/
public function testPiwikStatusMessages() {
$site_id = '1';
$this->config('piwik.settings')->set('site_id', $site_id)->save();
$this->config('piwik.settings')->set('url_http', 'http://www.example.com/piwik/')->save();
$this->config('piwik.settings')->set('url_https', 'https://www.example.com/piwik/')->save();
// Enable logging of errors only.
$this->config('piwik.settings')->set('track.messages', ['error' => 'error'])->save();
$this->drupalPostForm('user/login', [], t('Log in'));
$this->assertRaw('_paq.push(["trackEvent", "Messages", "Error message", "Username field is required."]);', '[testPiwikStatusMessages]: trackEvent "Username field is required." is shown.');
$this->assertRaw('_paq.push(["trackEvent", "Messages", "Error message", "Password field is required."]);', '[testPiwikStatusMessages]: trackEvent "Password field is required." is shown.');
// Testing this drupal_set_message() requires an extra test module.
$this->drupalGet('piwik-test/drupal-set-message');
$this->assertNoRaw('_paq.push(["trackEvent", "Messages", "Status message", "Example status message."]);', '[testPiwikStatusMessages]: Example status message is not enabled for tracking.');
$this->assertNoRaw('_paq.push(["trackEvent", "Messages", "Warning message", "Example warning message."]);', '[testPiwikStatusMessages]: Example warning message is not enabled for tracking.');
$this->assertRaw('_paq.push(["trackEvent", "Messages", "Error message", "Example error message."]);', '[testPiwikStatusMessages]: Example error message is shown.');
$this->assertRaw('_paq.push(["trackEvent", "Messages", "Error message", "Example error message with html tags and link."]);', '[testPiwikStatusMessages]: HTML has been stripped successful from Example error message with html tags and link.');
// Enable logging of status, warnings and errors.
$this->config('piwik.settings')->set('track.messages', ['status' => 'status', 'warning' => 'warning', 'error' => 'error'])->save();
$this->drupalGet('piwik-test/drupal-set-message');
$this->assertRaw('_paq.push(["trackEvent", "Messages", "Status message", "Example status message."]);', '[testPiwikStatusMessages]: Example status message is enabled for tracking.');
$this->assertRaw('_paq.push(["trackEvent", "Messages", "Warning message", "Example warning message."]);', '[testPiwikStatusMessages]: Example warning message is enabled for tracking.');
$this->assertRaw('_paq.push(["trackEvent", "Messages", "Error message", "Example error message."]);', '[testPiwikStatusMessages]: Example error message is shown.');
$this->assertRaw('_paq.push(["trackEvent", "Messages", "Error message", "Example error message with html tags and link."]);', '[testPiwikStatusMessages]: HTML has been stripped successful from Example error message with html tags and link.');
}
}
@@ -0,0 +1,78 @@
<?php
namespace Drupal\piwik\Tests;
use Drupal\simpletest\WebTestBase;
/**
* Test uninstall functionality of Piwik module.
*
* @group Piwik
*/
class PiwikUninstallTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['piwik'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$permissions = [
'access administration pages',
'administer piwik',
'administer modules',
];
// User to set up piwik.
$this->admin_user = $this->drupalCreateUser($permissions);
$this->drupalLogin($this->admin_user);
}
/**
* Tests if the module cleans up the disk on uninstall.
*/
public function testPiwikUninstall() {
$cache_path = 'public://piwik';
$site_id = '1';
$this->config('piwik.settings')->set('site_id', $site_id)->save();
$this->config('piwik.settings')->set('url_http', 'http://www.example.com/piwik/')->save();
$this->config('piwik.settings')->set('url_https', 'https://www.example.com/piwik/')->save();
// Enable local caching of piwik.js.
$this->config('piwik.settings')->set('cache', 1)->save();
// Load front page to get the piwik.js downloaded into local cache. But
// loading the piwik.js is not possible as "url_http" is a test dummy only.
// Create a dummy file to complete the rest of the tests.
file_prepare_directory($cache_path, FILE_CREATE_DIRECTORY);
$data = $this->randomMachineName(128);
$file_destination = $cache_path . '/piwik.js';
file_unmanaged_save_data($data, $file_destination);
file_unmanaged_save_data(gzencode($data, 9, FORCE_GZIP), $file_destination . '.gz', FILE_EXISTS_REPLACE);
// Test if the directory and piwik.js exists.
$this->assertTrue(file_prepare_directory($cache_path), 'Cache directory "public://piwik" has been found.');
$this->assertTrue(file_exists($cache_path . '/piwik.js'), 'Cached piwik.js tracking file has been found.');
$this->assertTrue(file_exists($cache_path . '/piwik.js.gz'), 'Cached piwik.js.gz tracking file has been found.');
// Uninstall the module.
$edit = [];
$edit['uninstall[piwik]'] = TRUE;
$this->drupalPostForm('admin/modules/uninstall', $edit, t('Uninstall'));
$this->assertNoText(\Drupal::translation()->translate('Configuration deletions'), 'No configuration deletions listed on the module install confirmation page.');
$this->drupalPostForm(NULL, NULL, t('Uninstall'));
$this->assertText(t('The selected modules have been uninstalled.'), 'Modules status has been updated.');
// Test if the directory and all files have been removed.
$this->assertFalse(file_scan_directory($cache_path, '/.*/'), 'Cached JavaScript files have been removed.');
$this->assertFalse(file_prepare_directory($cache_path), 'Cache directory "public://piwik" has been removed.');
}
}
@@ -0,0 +1,71 @@
<?php
namespace Drupal\piwik\Tests;
use Drupal\simpletest\WebTestBase;
/**
* Test user fields functionality of Piwik module.
*
* @group Piwik
*/
class PiwikUserFieldsTestTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['piwik', 'field_ui'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$permissions = [
'access administration pages',
'administer user form display',
'opt-in or out of piwik tracking',
];
// User to set up piwik.
$this->admin_user = $this->drupalCreateUser($permissions);
$this->drupalLogin($this->admin_user);
}
/**
* Tests if "allow users to customize tracking on their account page" works.
*/
public function testPiwikUserFields() {
$ua_code = 'UA-123456-1';
$this->config('piwik.settings')->set('account', $ua_code)->save();
// Check if the pseudo field is shown on account forms.
$this->drupalGet('admin/config/people/accounts/form-display');
$this->assertResponse(200);
$this->assertRaw(t('Piwik settings'), '[testPiwikUserFields]: Piwik settings field exists on Manage form display.');
// No customization allowed.
$this->config('piwik.settings')->set('visibility.user_account_mode', 0)->save();
$this->drupalGet('user/' . $this->admin_user->id() . '/edit');
$this->assertResponse(200);
$this->assertNoRaw(t('Piwik settings'), '[testPiwikUserFields]: Piwik settings field does not exist on user edit page.');
// Tracking on by default, users with opt-in or out of tracking permission
// can opt out.
$this->config('piwik.settings')->set('visibility.user_account_mode', 1)->save();
$this->drupalGet('user/' . $this->admin_user->id() . '/edit');
$this->assertResponse(200);
$this->assertRaw(t('Users are tracked by default, but you are able to opt out.'), '[testPiwikUserFields]: Piwik settings field exists on on user edit page');
// Tracking off by default, users with opt-in or out of tracking permission
// can opt in.
$this->config('piwik.settings')->set('visibility.user_account_mode', 2)->save();
$this->drupalGet('user/' . $this->admin_user->id() . '/edit');
$this->assertResponse(200);
$this->assertRaw(t('Users are <em>not</em> tracked by default, but you are able to opt in.'), '[testPiwikUserFields]: Piwik settings field exists on on user edit page.');
}
}
@@ -0,0 +1,11 @@
name: 'Piwik test'
type: module
description: 'Support module for Piwik testing.'
package: Testing
# core: 8.x
# Information added by Drupal.org packaging script on 2017-11-11
version: '8.x-1.2'
core: '8.x'
project: 'piwik'
datestamp: 1510404815
@@ -0,0 +1,7 @@
piwik_test.drupal_set_message:
path: '/piwik-test/drupal-set-message'
defaults:
_title: 'Set messages with drupal_set_message()'
_controller: '\Drupal\piwik_test\Controller\PiwikTestController::drupalSetMessageTest'
requirements:
_access: 'TRUE'
@@ -0,0 +1,28 @@
<?php
namespace Drupal\piwik_test\Controller;
use Drupal\Core\Controller\ControllerBase;
/**
* Controller routines for system_test routes.
*/
class PiwikTestController extends ControllerBase {
/**
* Tests setting messages and removing one before it is displayed.
*
* @return string
* Empty string, we just test the setting of messages.
*/
public function drupalSetMessageTest() {
// Set some messages.
drupal_set_message('Example status message.', 'status');
drupal_set_message('Example warning message.', 'warning');
drupal_set_message('Example error message.', 'error');
drupal_set_message('Example error <em>message</em> with html tags and <a href="http://example.com/">link</a>.', 'error');
return [];
}
}
@@ -0,0 +1,339 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.
@@ -0,0 +1,3 @@
This is the new module home for a unified redirection API (also replaces
path_redirect and globalredirect).
@@ -0,0 +1,10 @@
{
"name": "drupal/redirect",
"description": "Allows users to redirect from old URLs to new URLs.",
"type": "drupal-module",
"license": "GPL-2.0+",
"minimum-stability": "dev",
"require": {
"drupal/core": "~8"
}
}
@@ -0,0 +1,7 @@
auto_redirect: true
default_status_code: 301
passthrough_querystring: true
warning: false
ignore_admin_path: false
access_check: false
route_normalizer_enabled: true
@@ -0,0 +1,11 @@
langcode: en
status: true
dependencies:
enforced:
module:
- redirect
id: redirect_delete_action
label: 'Delete redirect'
type: redirect
plugin: redirect_delete_action
configuration: { }
@@ -0,0 +1,593 @@
langcode: en
status: true
dependencies:
module:
- link
- redirect
- user
id: redirect
label: Redirect
module: views
description: 'List of redirects'
tag: ''
base_table: redirect
base_field: rid
core: 8.x
display:
default:
display_plugin: default
id: default
display_title: Master
position: 0
display_options:
access:
type: perm
options:
perm: 'administer redirects'
cache:
type: tag
options: { }
query:
type: views_query
options:
disable_sql_rewrite: false
distinct: false
replica: false
query_comment: ''
query_tags: { }
exposed_form:
type: basic
options:
submit_button: Apply
reset_button: false
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
pager:
type: full
options:
items_per_page: 50
offset: 0
id: 0
total_pages: null
tags:
previous: ' previous'
next: 'next '
first: '« first'
last: 'last »'
expose:
items_per_page: false
items_per_page_label: 'Items per page'
items_per_page_options: '5, 10, 25, 50'
items_per_page_options_all: false
items_per_page_options_all_label: '- All -'
offset: false
offset_label: Offset
quantity: 9
style:
type: table
options:
grouping: { }
row_class: ''
default_row_class: true
override: true
sticky: false
caption: ''
summary: ''
description: ''
columns:
redirect_source__path: redirect_source__path
redirect_redirect__uri: redirect_redirect__uri
status_code: status_code
language: language
created: created
operations: operations
info:
redirect_source__path:
sortable: true
default_sort_order: asc
align: ''
separator: ''
empty_column: false
responsive: ''
redirect_redirect__uri:
sortable: true
default_sort_order: asc
align: ''
separator: ''
empty_column: false
responsive: ''
status_code:
sortable: true
default_sort_order: asc
align: ''
separator: ''
empty_column: false
responsive: ''
language:
sortable: true
default_sort_order: asc
align: ''
separator: ''
empty_column: false
responsive: ''
created:
sortable: true
default_sort_order: asc
align: ''
separator: ''
empty_column: false
responsive: ''
operations:
sortable: false
default_sort_order: asc
align: ''
separator: ''
empty_column: false
responsive: ''
default: created
empty_table: false
row:
type: fields
fields:
redirect_bulk_form:
id: redirect_bulk_form
table: redirect
field: redirect_bulk_form
relationship: none
group_type: group
admin_label: ''
label: ''
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: false
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
action_title: 'With selection'
include_exclude: exclude
selected_actions: { }
entity_type: redirect
plugin_id: redirect_bulk_form
redirect_source__path:
id: redirect_source__path
table: redirect
field: redirect_source__path
relationship: none
group_type: group
admin_label: ''
label: From
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: path
type: redirect_source
settings: { }
group_column: ''
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
entity_type: redirect
entity_field: redirect_source
plugin_id: field
redirect_redirect__uri:
id: redirect_redirect__uri
table: redirect
field: redirect_redirect__uri
entity_type: redirect
entity_field: redirect_redirect
plugin_id: field
status_code:
id: status_code
table: redirect
field: status_code
entity_type: redirect
entity_field: status_code
plugin_id: field
language:
id: language
table: redirect
field: language
entity_type: redirect
entity_field: language
plugin_id: field
created:
id: created
table: redirect
field: created
relationship: none
group_type: group
admin_label: ''
label: Created
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
date_format: fallback
custom_date_format: ''
timezone: ''
entity_type: redirect
entity_field: created
plugin_id: date
operations:
id: operations
table: redirect
field: operations
entity_type: redirect
plugin_id: entity_operations
filters:
redirect_source__path:
id: redirect_source__path
table: redirect
field: redirect_source__path
relationship: none
group_type: group
admin_label: ''
operator: contains
value: ''
group: 1
exposed: true
expose:
operator_id: redirect_source__path_op
label: From
description: ''
use_operator: false
operator: redirect_source__path_op
identifier: redirect_source__path
required: false
remember: false
multiple: false
remember_roles:
authenticated: authenticated
anonymous: '0'
administrator: '0'
is_grouped: false
group_info:
label: ''
description: ''
identifier: ''
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items: { }
entity_type: redirect
entity_field: redirect_source
plugin_id: string
redirect_redirect__uri:
id: redirect_redirect__uri
table: redirect
field: redirect_redirect__uri
relationship: none
group_type: group
admin_label: ''
operator: contains
value: ''
group: 1
exposed: true
expose:
operator_id: redirect_redirect__uri_op
label: To
description: ''
use_operator: false
operator: redirect_redirect__uri_op
identifier: redirect_redirect__uri
required: false
remember: false
multiple: false
remember_roles:
authenticated: authenticated
anonymous: '0'
administrator: '0'
is_grouped: false
group_info:
label: ''
description: ''
identifier: ''
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items: { }
entity_type: redirect
entity_field: redirect_redirect
plugin_id: string
status_code:
id: status_code
table: redirect
field: status_code
relationship: none
group_type: group
admin_label: ''
operator: '='
value:
min: ''
max: ''
value: ''
group: 1
exposed: true
expose:
operator_id: status_code_op
label: 'Status code'
description: ''
use_operator: false
operator: status_code_op
identifier: status_code
required: false
remember: false
multiple: false
remember_roles:
authenticated: authenticated
anonymous: '0'
administrator: '0'
is_grouped: true
group_info:
label: 'Status code'
description: ''
identifier: status_code
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items:
1:
title: '300 Multiple Choices'
operator: '='
value:
value: '300'
min: ''
max: ''
2:
title: '301 Moved Permanently'
operator: '='
value:
value: '301'
min: ''
max: ''
3:
title: '302 Found'
operator: '='
value:
value: '302'
min: ''
max: ''
4:
title: '303 See Other'
operator: '='
value:
value: '303'
min: ''
max: ''
5:
title: '304 Not Modified'
operator: '='
value:
value: '304'
min: ''
max: ''
6:
title: '305 Use Proxy'
operator: '='
value:
value: '305'
min: ''
max: ''
7:
title: '307 Temporary Redirect'
operator: '='
value:
value: '307'
min: ''
max: ''
entity_type: redirect
entity_field: status_code
plugin_id: numeric
language:
id: language
table: redirect
field: language
relationship: none
group_type: group
admin_label: ''
operator: in
value: { }
group: 1
exposed: true
expose:
operator_id: language_op
label: 'Original language'
description: ''
use_operator: false
operator: language_op
identifier: language
required: false
remember: false
multiple: false
remember_roles:
authenticated: authenticated
anonymous: '0'
administrator: '0'
reduce: false
is_grouped: false
group_info:
label: ''
description: ''
identifier: ''
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items: { }
entity_type: redirect
entity_field: language
plugin_id: language
sorts: { }
title: Redirect
header: { }
footer: { }
empty:
area_text_custom:
id: area_text_custom
table: views
field: area_text_custom
relationship: none
group_type: group
admin_label: ''
empty: true
tokenize: false
content: 'There is no redirect yet.'
plugin_id: text_custom
relationships: { }
arguments: { }
display_extenders: { }
filter_groups:
operator: AND
groups:
1: AND
cache_metadata:
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url
- url.query_args
- user.permissions
cacheable: false
page_1:
display_plugin: page
id: page_1
display_title: Page
position: 1
display_options:
display_extenders: { }
path: admin/config/search/redirect
cache_metadata:
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url
- url.query_args
- user.permissions
cacheable: false
@@ -0,0 +1,7 @@
status: true
dependencies: { }
id: redirect.redirect
target_entity_type_id: redirect
target_bundle: redirect
default_langcode: und
language_alterable: true
@@ -0,0 +1,31 @@
# Schema for the configuration files of the Redirect module.
redirect.settings:
type: config_entity
label: 'Redirect settings'
mapping:
auto_redirect:
type: boolean
label: 'Automatically create redirects when URL aliases are changed.'
default_status_code:
type: integer
label: 'Default redirect status'
passthrough_querystring:
type: boolean
label: 'Retain query string through redirect.'
warning:
type: boolean
label: 'Display a warning message to users when they are redirected.'
ignore_admin_path:
type: boolean
label: 'Allow redirections on admin paths.'
access_check:
type: boolean
label: 'Menu Access Checking'
route_normalizer_enabled:
type: boolean
label: 'Enforce clean and canonical URLs.'
action.configuration.redirect_delete_action:
type: action_configuration_default
label: 'Delete redirect configuration'
@@ -0,0 +1,19 @@
# Schema for the views plugins of the Redirect module.
views.field.redirect_type:
type: views.field.redirect
label: 'Redirect type'
mapping:
machine_name:
type: string
label: 'Output machine name'
# Schema for the formatter of Redirect Source
field.formatter.settings.redirect_source:
type: mapping
label: 'Redirect Source formatter'
mapping: { }
views.field.redirect_bulk_form:
type: views_field_bulk_form
label: 'Redirect bulk form'
@@ -0,0 +1,14 @@
.redirect-table {
table-layout: fixed;
}
.redirect-table__operations {
width: 30%;
}
.redirect-table__path {
width: 70%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
@@ -0,0 +1,21 @@
id: d6_path_redirect
label: Path Redirect
migration_tags:
- Drupal 6
source:
plugin: d6_path_redirect
process:
rid: rid
redirect_source: source
redirect_redirect:
plugin: d6_path_redirect
source:
- redirect
- query
language:
plugin: default_value
source: language
default_value: und
status_code: type
destination:
plugin: entity:redirect
@@ -0,0 +1,25 @@
id: d7_path_redirect
label: Path Redirect
migration_tags:
- Drupal 7
source:
plugin: d7_path_redirect
process:
rid: rid
uid: uid
redirect_source/path: source
redirect_source/query:
plugin: d7_redirect_source_query
source: source_options
redirect_redirect/uri:
plugin: d7_path_redirect
source:
- redirect
- redirect_options
language:
plugin: default_value
source: language
default_value: und
status_code: status_code
destination:
plugin: entity:redirect
@@ -0,0 +1,3 @@
row_limit: 10000
pages: ''
suppress_404: FALSE
@@ -0,0 +1,523 @@
langcode: en
status: true
dependencies:
module:
- redirect_404
- user
id: redirect_404
label: 'Redirect 404'
module: views
description: ''
tag: ''
base_table: redirect_404
base_field: ''
core: 8.x
display:
default:
display_plugin: default
id: default
display_title: Master
position: 0
display_options:
access:
type: perm
options:
perm: 'administer redirects'
cache:
type: none
options: { }
query:
type: views_query
options:
disable_sql_rewrite: false
distinct: false
replica: false
query_comment: ''
query_tags: { }
exposed_form:
type: basic
options:
submit_button: Filter
reset_button: true
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
pager:
type: mini
options:
items_per_page: 10
offset: 0
id: 0
total_pages: null
expose:
items_per_page: false
items_per_page_label: 'Items per page'
items_per_page_options: '5, 10, 25, 50'
items_per_page_options_all: false
items_per_page_options_all_label: '- All -'
offset: false
offset_label: Offset
tags:
previous: ‹‹
next: ››
style:
type: table
options:
grouping: { }
row_class: ''
default_row_class: true
override: true
sticky: false
caption: ''
summary: ''
description: ''
columns:
path: path
count: count
timestamp: timestamp
info:
path:
sortable: true
default_sort_order: desc
align: ''
separator: ''
empty_column: false
responsive: ''
count:
sortable: true
default_sort_order: desc
align: ''
separator: ''
empty_column: false
responsive: ''
timestamp:
sortable: true
default_sort_order: desc
align: ''
separator: ''
empty_column: false
responsive: ''
default: count
empty_table: false
row:
type: fields
fields:
path:
table: redirect_404
field: path
id: path
entity_type: null
entity_field: null
plugin_id: standard
relationship: none
group_type: group
admin_label: ''
label: Path
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
count:
id: count
table: redirect_404
field: count
relationship: none
group_type: group
admin_label: ''
label: Count
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
format: unserialized
key: ''
plugin_id: serialized
timestamp:
id: timestamp
table: redirect_404
field: timestamp
relationship: none
group_type: group
admin_label: ''
label: 'Last accessed'
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
date_format: short
custom_date_format: ''
timezone: ''
plugin_id: date
langcode:
id: langcode
table: redirect_404
field: langcode
relationship: none
group_type: group
admin_label: ''
label: Language
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
plugin_id: standard
redirect_404_operations:
id: redirect_404_operations
table: redirect_404
field: redirect_404_operations
relationship: none
group_type: group
admin_label: ''
label: Operations
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
plugin_id: redirect_404_operations
filters:
path:
id: path
table: redirect_404
field: path
relationship: none
group_type: group
admin_label: ''
operator: contains
value: ''
group: 1
exposed: true
expose:
operator_id: path_op
label: Path
description: ''
use_operator: false
operator: path_op
identifier: path
required: false
remember: false
multiple: false
remember_roles:
authenticated: authenticated
anonymous: '0'
administrator: '0'
is_grouped: false
group_info:
label: ''
description: ''
identifier: ''
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items: { }
plugin_id: string
langcode:
id: langcode
table: redirect_404
field: langcode
relationship: none
group_type: group
admin_label: ''
operator: in
value: { }
group: 1
exposed: true
expose:
operator_id: langcode_op
label: Language
description: ''
use_operator: false
operator: langcode_op
identifier: langcode
required: false
remember: false
multiple: false
remember_roles:
authenticated: authenticated
anonymous: '0'
administrator: '0'
reduce: false
is_grouped: false
group_info:
label: ''
description: ''
identifier: ''
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items: { }
plugin_id: in_operator
resolved:
id: resolved
table: redirect_404
field: resolved
relationship: none
group_type: group
admin_label: ''
operator: '='
value: '0'
group: 1
exposed: false
expose:
operator_id: ''
label: Resolved
description: ''
use_operator: false
operator: resolved_op
identifier: resolved
required: false
remember: false
multiple: false
remember_roles:
authenticated: authenticated
anonymous: '0'
administrator: '0'
is_grouped: false
group_info:
label: ''
description: ''
identifier: ''
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items: { }
plugin_id: boolean
sorts: { }
title: 'Fix 404 pages'
header: { }
footer: { }
empty:
area_text_custom:
id: area_text_custom
table: views
field: area_text_custom
relationship: none
group_type: group
admin_label: ''
empty: true
tokenize: false
content: 'There are no 404 errors to fix.'
plugin_id: text_custom
relationships: { }
arguments: { }
display_extenders: { }
filter_groups:
operator: AND
groups:
1: AND
cache_metadata:
max-age: 0
contexts:
- 'languages:language_interface'
- url
- url.query_args
- user.permissions
tags: { }
page_1:
display_plugin: page
id: page_1
display_title: Page
position: 1
display_options:
display_extenders: { }
path: admin/config/search/redirect/404
display_description: 'Lists 404 error paths with no redirect assigned yet.'
enabled: true
cache_metadata:
max-age: 0
contexts:
- 'languages:language_interface'
- url
- url.query_args
- user.permissions
tags: { }
@@ -0,0 +1,14 @@
# Schema for the configuration files of the redirect_404 module.
redirect_404.settings:
type: config_object
label: '404 error database logging settings.'
mapping:
row_limit:
type: integer
label: '404 error database logs to keep.'
pages:
type: string
suppress_404:
type: boolean
label: "Whether to log 'page not found' messages to the standard log or not."
@@ -0,0 +1,7 @@
views.field.redirect_404_operations:
type: views_field
label: 'Redirect 404 operations'
mapping:
text:
type: label
label: 'Redirect 404 operations'
@@ -0,0 +1,14 @@
name: 'Redirect 404'
type: module
description: 'Logs 404 errors and allows users to create redirects for often requested but missing pages.'
# core: 8.x
dependencies:
- redirect
- views
# Information added by Drupal.org packaging script on 2018-03-12
version: '8.x-1.1'
core: '8.x'
project: 'redirect'
datestamp: 1520897884
@@ -0,0 +1,62 @@
<?php
/**
* @file
* Update hooks for the redirect_404 module.
*/
use Drupal\Core\Language\LanguageInterface;
use Drupal\redirect_404\SqlRedirectNotFoundStorage;
/**
* Implements hook_schema().
*/
function redirect_404_schema() {
$schema['redirect_404'] = [
'description' => 'Stores 404 requests.',
'fields' => [
'path' => [
'description' => 'The path of the request.',
'type' => 'varchar',
'length' => SqlRedirectNotFoundStorage::MAX_PATH_LENGTH,
'not null' => TRUE,
],
'langcode' => [
'description' => 'The language of this request.',
'type' => 'varchar_ascii',
'length' => 12,
'not null' => TRUE,
'default' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
],
'count' => [
'description' => 'The number of requests with that path and language.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
],
'timestamp' => [
'description' => 'The timestamp of the last request with that path and language.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
],
'resolved' => [
'description' => 'Boolean indicating whether or not this path has a redirect assigned.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
],
],
'primary key' => ['path', 'langcode'],
];
return $schema;
}
/**
* Remove relevancy field from the redirect_404 table.
*/
function redirect_404_update_8101() {
\Drupal::database()->schema()->dropField('redirect_404', 'relevancy');
}
@@ -0,0 +1,5 @@
redirect_404.goto_fix_404:
route_name: redirect_404.fix_404
title: 'Fix 404 pages with URL redirects'
appears_on:
- 'dblog.page_not_found'
@@ -0,0 +1,6 @@
redirect_404.fix_404:
title: 'Fix 404 pages'
parent: redirect.list
route_name: redirect_404.fix_404
description: 'Add redirects for 404 pages.'
menu_name: admin
@@ -0,0 +1,5 @@
redirect_404.fix_404:
route_name: redirect_404.fix_404
base_route: redirect.list
title: Fix 404 pages
weight: 30
@@ -0,0 +1,96 @@
<?php
/**
* @file
* Module file for redirect_404.
*/
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Drupal\redirect\Entity\Redirect;
/**
* Implements hook_cron().
*
* Adds clean up job to drop the irrelevant rows from the redirect_404 table.
*/
function redirect_404_cron() {
/** @var \Drupal\redirect_404\SqlRedirectNotFoundStorage $redirect_storage */
$redirect_storage = \Drupal::service('redirect.not_found_storage');
$redirect_storage->purgeOldRequests();
}
/**
* Implements hook_form_FORM_ID_alter() for system_logging_settings().
*/
function redirect_404_form_redirect_settings_form_alter(&$form, FormStateInterface $form_state, $form_id) {
$config = \Drupal::configFactory()->getEditable('redirect_404.settings');
$row_limits = [100, 1000, 10000, 100000, 1000000];
$form['row_limit'] = [
'#type' => 'select',
'#title' => t('404 error database logs to keep'),
'#default_value' => $config->get('row_limit'),
'#options' => [0 => t('All')] + array_combine($row_limits, $row_limits),
'#description' => t('The maximum number of 404 error logs to keep in the database log. Requires a <a href=":cron">cron maintenance task</a>.', [':cron' => Url::fromRoute('system.status')->toString()])
];
$ignored_pages = $config->get('pages');
// Add a new path to be ignored, if there is an ignore argument in the query.
if ($path_to_ignore = \Drupal::request()->query->get('ignore')) {
$ignored_pages .= "\n" . $path_to_ignore;
}
$form['ignore_pages'] = [
'#type' => 'textarea',
'#title' => t('Pages to ignore'),
'#default_value' => $ignored_pages,
'#description' => t("Specify pages by using their paths. Enter one path per line. The '*' character is a wildcard. An example path is %user-wildcard for every user page. %front is the front page.", [
'%user-wildcard' => '/user/*',
'%front' => '<front>',
]),
];
$form['suppress_404'] = [
'#type' => 'checkbox',
'#title' => t("Suppress 'page not found' log messages"),
'#default_value' => $config->get('suppress_404'),
'#description' => t("Prevents logging 'page not found' events. Can be safely enabled when redirect_404 module is used, which stores them separately, nothing else relies on those messages."),
];
$form['#submit'][] = 'redirect_404_logging_settings_submit';
}
/**
* Form submission handler for system_logging_settings().
*
* @see redirect_404_form_redirect_settings_form_alter()
*/
function redirect_404_logging_settings_submit($form, FormStateInterface $form_state) {
// Make sure to store the 'pages to ignore' with the leading slash.
$ignore_pages = explode(PHP_EOL, $form_state->getValue('ignore_pages'));
$pages = '';
foreach ($ignore_pages as $page) {
if (!empty($page)) {
$pages .= '/' . ltrim($page, '/') . "\n";
}
}
\Drupal::configFactory()
->getEditable('redirect_404.settings')
->set('row_limit', $form_state->getValue('row_limit'))
->set('pages', $pages)
->set('suppress_404', $form_state->getValue('suppress_404'))
->save();
}
/**
* Implements hook_ENTITY_TYPE_presave() for redirect entities.
*/
function redirect_404_redirect_presave(Redirect $redirect) {
$path = $redirect->getSourcePathWithQuery();
$langcode = $redirect->get('language')->value;
// Mark a potentially existing log entry for this path as resolved.
\Drupal::service('redirect.not_found_storage')->resolveLogRequest($path, $langcode);
}
@@ -0,0 +1,15 @@
redirect_404.fix_404:
path: '/admin/config/search/redirect/404'
defaults:
_title: 'Fix 404 pages'
_form: '\Drupal\redirect_404\Form\RedirectFix404Form'
requirements:
_permission: 'administer redirects'
redirect_404.ignore_404:
path: '/admin/config/search/redirect/404/ignore'
defaults:
_controller: '\Drupal\redirect_404\Controller\Fix404IgnoreController::ignorePath'
requirements:
_permission: 'administer redirect settings'
_csrf_token: 'TRUE'
@@ -0,0 +1,16 @@
services:
redirect.404_subscriber:
class: Drupal\redirect_404\EventSubscriber\Redirect404Subscriber
arguments: ['@path.current', '@path.matcher', '@request_stack', '@language_manager', '@redirect.not_found_storage', '@config.factory']
tags:
- { name: event_subscriber }
redirect.not_found_storage:
class: Drupal\redirect_404\SqlRedirectNotFoundStorage
arguments: ['@database', '@config.factory']
tags:
- { name: backend_overridable }
logger.redirect_404:
public: false
class: \Drupal\redirect_404\Render\Redirect404LogSuppressor
decorates: logger.factory
arguments: ['@logger.redirect_404.inner', '@config.factory']
@@ -0,0 +1,99 @@
<?php
/**
* @file
* Provide views data for redirect_404.module.
*/
use Drupal\redirect_404\SqlRedirectNotFoundStorage;
/**
* Implements hook_views_data().
*/
function redirect_404_views_data() {
$data = [];
// Only define views data if the service uses our specific implementation.
if (!\Drupal::service('redirect.not_found_storage') instanceof SqlRedirectNotFoundStorage) {
return $data;
}
$data['redirect_404']['table']['group'] = t('Redirect 404');
$data['redirect_404']['table']['base'] = [
'field' => '',
'title' => t('Fix 404 pages'),
'help' => t('Overview for 404 error paths with no redirect assigned yet.'),
];
$data['redirect_404']['path'] = [
'title' => t('Path'),
'help' => t('The path of the request.'),
'field' => [
'id' => 'standard',
],
'filter' => [
'id' => 'string',
],
];
$data['redirect_404']['langcode'] = [
'title' => t('Language'),
'help' => t('The language of this request.'),
'field' => [
'id' => 'redirect_404_langcode',
],
'filter' => [
'id' => 'language',
],
];
$data['redirect_404']['count'] = [
'title' => t('Count'),
'help' => t('The number of requests with that path and language.'),
'field' => [
'id' => 'numeric',
'click sortable' => TRUE,
],
'filter' => [
'id' => 'numeric',
],
];
$data['redirect_404']['timestamp'] = [
'title' => t('Timestamp'),
'help' => t('The timestamp of the last request with that path and language.'),
'field' => [
'id' => 'date',
'click sortable' => TRUE,
],
'filter' => [
'id' => 'date',
],
];
$data['redirect_404']['resolved'] = [
'title' => t('Resolved'),
'help' => t('Whether or not this path has a redirect assigned.'),
'field' => [
'id' => 'boolean',
],
'filter' => [
'id' => 'boolean',
'label' => t('Resolved'),
'use_equal' => TRUE,
],
];
$data['redirect_404']['redirect_404_operations'] = [
'title' => t('Operations'),
'help' => t('Provide operation buttons to handle the 404 path.'),
'field' => [
'id' => 'redirect_404_operations',
'additional fields' => ['path', 'langcode'],
'real field' => 'path',
],
];
return $data;
}
@@ -0,0 +1,85 @@
<?php
namespace Drupal\redirect_404\Controller;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Url;
use Drupal\redirect_404\RedirectNotFoundStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Controller to ignore a path from the 'Fix 404 pages' page.
*/
class Fix404IgnoreController extends ControllerBase {
/**
* The config factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configuration;
/**
* The redirect storage.
*
* @var \Drupal\redirect_404\RedirectNotFoundStorageInterface
*/
protected $redirectStorage;
/**
* Constructs a Fix404Ignore object.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
* @param \Drupal\redirect_404\RedirectNotFoundStorageInterface $redirect_storage
* A redirect storage.
*/
public function __construct(ConfigFactoryInterface $config_factory, RedirectNotFoundStorageInterface $redirect_storage) {
$this->configuration = $config_factory;
$this->redirectStorage = $redirect_storage;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('config.factory'),
$container->get('redirect.not_found_storage')
);
}
/**
* Adds path into the ignored list.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The HttpRequest object representing the current request.
*
* @return \Symfony\Component\HttpFoundation\RedirectResponse
*/
public function ignorePath(Request $request) {
$ignored_paths = $this->config('redirect_404.settings')->get('pages');
$path = $request->query->get('path');
$langcode = $request->query->get('langcode');
if (empty($ignored_paths) || !strpos($path, $ignored_paths)) {
$this->redirectStorage->resolveLogRequest($path, $langcode);
drupal_set_message($this->t('Resolved the path %path in the database. Please check the ignored list and save the settings.', [
'%path' => $path,
]));
}
$options = [
'query' => [
'ignore' => $path,
'destination' => Url::fromRoute('redirect_404.fix_404')->getInternalPath(),
],
];
return $this->redirect('redirect.settings', [], $options);
}
}
@@ -0,0 +1,130 @@
<?php
namespace Drupal\redirect_404\EventSubscriber;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Path\PathMatcherInterface;
use Drupal\redirect_404\RedirectNotFoundStorageInterface;
use Drupal\Core\Path\CurrentPathStack;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* An EventSubscriber that listens to redirect 404 errors.
*/
class Redirect404Subscriber implements EventSubscriberInterface {
/**
* The current path.
*
* @var \Drupal\Core\Path\CurrentPathStack
*/
protected $currentPath;
/**
* The path matcher.
*
* @var \Drupal\Core\Path\PathMatcherInterface
*/
protected $pathMatcher;
/**
* The request stack (get the URL argument(s) and combined it with the path).
*
* @var \Symfony\Component\HttpFoundation\RequestStack
*/
protected $requestStack;
/**
* The language manager.
*
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected $languageManager;
/**
* The redirect storage.
*
* @var \Drupal\redirect_404\RedirectNotFoundStorageInterface
*/
protected $redirectStorage;
/**
* The configuration factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $config;
/**
* Constructs a new Redirect404Subscriber.
*
* @param \Drupal\Core\Path\CurrentPathStack $current_path
* The current path.
* @param \Drupal\Core\Path\PathMatcherInterface $path_matcher
* The path matcher service.
* @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
* The request stack.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
* @param \Drupal\redirect_404\RedirectNotFoundStorageInterface $redirect_storage
* A redirect storage.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config
* The configuration factory.
*/
public function __construct(CurrentPathStack $current_path, PathMatcherInterface $path_matcher, RequestStack $request_stack, LanguageManagerInterface $language_manager, RedirectNotFoundStorageInterface $redirect_storage, ConfigFactoryInterface $config) {
$this->currentPath = $current_path;
$this->pathMatcher = $path_matcher;
$this->requestStack = $request_stack;
$this->languageManager = $language_manager;
$this->redirectStorage = $redirect_storage;
$this->config = $config->get('redirect_404.settings');
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
$events[KernelEvents::EXCEPTION][] = 'onKernelException';
return $events;
}
/**
* Logs an exception of 404 Redirect errors.
*
* @param GetResponseForExceptionEvent $event
* Is given by the event dispatcher.
*/
public function onKernelException(GetResponseForExceptionEvent $event) {
// Only log page not found (404) errors.
if ($event->getException() instanceof NotFoundHttpException) {
$path = $this->currentPath->getPath();
// Ignore paths specified in the redirect settings.
if ($pages = Unicode::strtolower($this->config->get('pages'))) {
// Do not trim a trailing slash if that is the complete path.
$path_to_match = $path === '/' ? $path : rtrim($path, '/');
if ($this->pathMatcher->matchPath(Unicode::strtolower($path_to_match), $pages)) {
return;
}
}
// Allow to store paths with arguments.
if ($query_string = $this->requestStack->getCurrentRequest()->getQueryString()) {
$query_string = '?' . $query_string;
}
$path .= $query_string;
$langcode = $this->languageManager->getCurrentLanguage()->getId();
// Write record.
$this->redirectStorage->logRequest($path, $langcode);
}
}
}
@@ -0,0 +1,193 @@
<?php
namespace Drupal\redirect_404\Form;
use Drupal\Core\Datetime\DateFormatterInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Url;
use Drupal\redirect_404\SqlRedirectNotFoundStorage;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a form that lists all 404 error paths and no redirect assigned yet.
*
* This is a fallback for the provided default view.
*/
class RedirectFix404Form extends FormBase {
/**
* The language manager.
*
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected $languageManager;
/**
* The redirect storage.
*
* @var \Drupal\redirect_404\SqlRedirectNotFoundStorage
*/
protected $redirectStorage;
/**
* The date formatter service.
*
* @var \Drupal\Core\Datetime\DateFormatterInterface
*/
protected $dateFormatter;
/**
* The entity manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Constructs a RedirectFix404Form.
*
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
* @param \Drupal\redirect_404\SqlRedirectNotFoundStorage $redirect_storage
* The redirect storage.
* @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
* The date Formatter service.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity manager.
*/
public function __construct(LanguageManagerInterface $language_manager, SqlRedirectNotFoundStorage $redirect_storage, DateFormatterInterface $date_formatter, EntityTypeManagerInterface $entity_type_manager) {
$this->languageManager = $language_manager;
$this->redirectStorage = $redirect_storage;
$this->dateFormatter = $date_formatter;
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('language_manager'),
$container->get('redirect.not_found_storage'),
$container->get('date.formatter'),
$container->get('entity_type.manager')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'redirect_fix_404_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$destination = $this->getDestinationArray();
$search = $this->getRequest()->get('search');
$form['#attributes'] = ['class' => ['search-form']];
$form['basic'] = [
'#type' => 'fieldset',
'#title' => $this->t('Filter 404s'),
'#attributes' => ['class' => ['container-inline']],
];
$form['basic']['filter'] = [
'#type' => 'textfield',
'#title' => '',
'#default_value' => $search,
'#maxlength' => 128,
'#size' => 25,
];
$form['basic']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Filter'),
'#action' => 'filter',
];
if ($search) {
$form['basic']['reset'] = [
'#type' => 'submit',
'#value' => $this->t('Reset'),
'#action' => 'reset',
];
}
$languages = $this->languageManager->getLanguages(LanguageInterface::STATE_ALL);
$multilingual = $this->languageManager->isMultilingual();
$header = [
['data' => $this->t('Path'), 'field' => 'source'],
['data' => $this->t('Count'), 'field' => 'count', 'sort' => 'desc'],
['data' => $this->t('Last accessed'), 'field' => 'timestamp'],
];
if ($multilingual) {
$header[] = ['data' => $this->t('Language'), 'field' => 'language'];
}
$header[] = ['data' => $this->t('Operations')];
$rows = [];
$results = $this->redirectStorage->listRequests($header, $search);
foreach ($results as $result) {
$path = ltrim($result->path, '/');
$row = [];
$row['source'] = $path;
$row['count'] = $result->count;
$row['timestamp'] = $this->dateFormatter->format($result->timestamp, 'short');
if ($multilingual) {
if (isset($languages[$result->langcode])) {
$row['language'] = $languages[$result->langcode]->getName();
}
else {
$row['language'] = $this->t('Undefined @langcode', ['@langcode' => $result->langcode]);
}
}
$operations = [];
if ($this->entityTypeManager->getAccessControlHandler('redirect')->createAccess()) {
$operations['add'] = [
'title' => $this->t('Add redirect'),
'url' => Url::fromRoute('redirect.add', [], ['query' => ['source' => $path, 'language' => $result->langcode] + $destination]),
];
}
$row['operations'] = [
'data' => [
'#type' => 'operations',
'#links' => $operations,
],
];
$rows[] = $row;
}
$form['redirect_404_table'] = [
'#theme' => 'table',
'#header' => $header,
'#rows' => $rows,
'#empty' => $this->t('There are no 404 errors to fix.'),
];
$form['redirect_404_pager'] = ['#type' => 'pager'];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
if ($form_state->getTriggeringElement()['#action'] == 'filter') {
$form_state->setRedirect('redirect_404.fix_404', [], ['query' => ['search' => trim($form_state->getValue('filter'))]]);
}
else {
$form_state->setRedirect('redirect_404.fix_404');
}
}
}
@@ -0,0 +1,63 @@
<?php
namespace Drupal\redirect_404\Plugin\views\field;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\views\Plugin\views\field\LanguageField;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a views field for the 404 error language.
*
* @ingroup views_field_handlers
*
* @ViewsField("redirect_404_langcode")
*/
class Language extends LanguageField {
/**
* The language manager.
*
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected $languageManager;
/**
* Constructs a Language object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, LanguageManagerInterface $language_manager) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->languageManager = $language_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('language_manager')
);
}
/**
* {@inheritdoc}
*/
public function access(AccountInterface $account) {
return $this->languageManager->isMultilingual();
}
}
@@ -0,0 +1,123 @@
<?php
namespace Drupal\redirect_404\Plugin\views\field;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Url;
use Drupal\views\Plugin\views\field\FieldPluginBase;
use Drupal\views\ResultRow;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a views field for the redirect operation buttons.
*
* @ingroup views_field_handlers
*
* @ViewsField("redirect_404_operations")
*/
class Redirect404Operations extends FieldPluginBase {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The renderer service.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* Constructor for the redirect operations view field.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer service.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current user.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, RendererInterface $renderer, AccountInterface $current_user) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->entityTypeManager = $entity_type_manager;
$this->renderer = $renderer;
$this->currentUser = $current_user;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity_type.manager'),
$container->get('renderer'),
$container->get('current_user')
);
}
/**
* {@inheritdoc}
*/
public function clickSortable() {
return FALSE;
}
/**
* {@inheritdoc}
*/
public function render(ResultRow $values) {
$links = [];
$query = [
'query' => [
'source' => ltrim($this->getValue($values, 'path'), '/'),
'language' => $this->getValue($values, 'langcode'),
'destination' => $this->view->getPath(),
],
];
$links['add'] = [
'title' => $this->t('Add redirect'),
'url' => Url::fromRoute('redirect.add', [], $query),
];
if ($this->currentUser->hasPermission('administer redirect settings')) {
$links['ignore'] = [
'title' => $this->t('Ignore'),
'url' => Url::fromRoute('redirect_404.ignore_404', [
'path' => $this->getValue($values, 'path'),
'langcode' => $this->getValue($values, 'langcode'),
]),
];
}
$operations['data'] = [
'#type' => 'operations',
'#links' => $links,
];
return $this->renderer->render($operations);
}
/**
* {@inheritdoc}
*/
public function access(AccountInterface $account) {
return $this->entityTypeManager->getAccessControlHandler('redirect')->createAccess();
}
}
@@ -0,0 +1,53 @@
<?php
namespace Drupal\redirect_404;
/**
* Interface for redirect 404 services.
*/
interface RedirectNotFoundStorageInterface {
/**
* Merges a 404 request log in the database.
*
* @param string $path
* The path of the current request.
* @param string $langcode
* The ID of the language code.
*/
public function logRequest($path, $langcode);
/**
* Marks a 404 request log as resolved.
*
* @param string $path
* The path of the current request.
* @param string $langcode
* The ID of the language code.
*/
public function resolveLogRequest($path, $langcode);
/**
* Returns the 404 request data.
*
* @param array $header
* An array containing arrays of the redirect_404 fields data.
* @param string $search
* The search text. It is possible to have multiple '*' as a wildcard.
*
* @return array
* A list of objects with the properties:
* - path
* - count
* - timestamp
* - langcode
* - resolved
*/
public function listRequests(array $header = [], $search = NULL);
/**
* Cleans the irrelevant 404 request logs.
*/
public function purgeOldRequests();
}
@@ -0,0 +1,65 @@
<?php
namespace Drupal\redirect_404\Render;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
use Drupal\Core\Logger\LoggerChannelFactory;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
/**
* Allows 'page not found' events to be suppressed by returning a NullLogger.
*/
class Redirect404LogSuppressor implements LoggerChannelFactoryInterface {
use DependencySerializationTrait;
/**
* The logger channel factory.
*
* @var \Drupal\Core\Logger\LoggerChannelFactoryInterface
*/
protected $loggerChannelFactory;
/**
* The configuration factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* Constructs a Redirect404LogSuppressor object.
*
* @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_channel_factory
* The logger channel factory.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The configuration factory.
*/
public function __construct(LoggerChannelFactoryInterface $logger_channel_factory, ConfigFactoryInterface $config_factory) {
$this->loggerChannelFactory = $logger_channel_factory;
$this->configFactory = $config_factory;
}
/**
* {@inheritdoc}
*/
public function get($channel) {
if ($channel == 'page not found' && $this->configFactory->get('redirect_404.settings')->get('suppress_404')) {
// Do not log if a 404 error is detected and the suppress_404 is enabled.
return new NullLogger();
}
// Call LoggerChannelFactory to let the default logger workflow proceed.
return $this->loggerChannelFactory->get($channel);
}
/**
* {@inheritdoc}
*/
public function addLogger(LoggerInterface $logger, $priority = 0) {
$this->loggerChannelFactory->addLogger($logger, $priority);
}
}
@@ -0,0 +1,162 @@
<?php
namespace Drupal\redirect_404;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Database\Connection;
/**
* Provides an SQL implementation for redirect not found storage.
*
* To keep a limited amount of relevant records, we compute a relevancy based
* on the amount of visits for each row, deleting the less visited record and
* sorted by timestamp.
*/
class SqlRedirectNotFoundStorage implements RedirectNotFoundStorageInterface {
/**
* Maximum column length for invalid paths.
*/
const MAX_PATH_LENGTH = 191;
/**
* Active database connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected $database;
/**
* The configuration factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* Constructs a new SqlRedirectNotFoundStorage.
*
* @param \Drupal\Core\Database\Connection $database
* A Database connection to use for reading and writing database data.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The configuration factory.
*/
public function __construct(Connection $database, ConfigFactoryInterface $config_factory) {
$this->database = $database;
$this->configFactory = $config_factory;
}
/**
* {@inheritdoc}
*/
public function logRequest($path, $langcode) {
if (Unicode::strlen($path) > static::MAX_PATH_LENGTH) {
// Don't attempt to log paths that would result in an exception. There is
// no point in logging truncated paths, as they cannot be used to build a
// new redirect.
return;
}
// Ignore invalid UTF-8, which can't be logged.
if (!Unicode::validateUtf8($path)) {
return;
}
// If the request is not new, update its count and timestamp.
$this->database->merge('redirect_404')
->key('path', $path)
->key('langcode', $langcode)
->expression('count', 'count + 1')
->fields([
'timestamp' => REQUEST_TIME,
'count' => 1,
'resolved' => 0,
])
->execute();
}
/**
* {@inheritdoc}
*/
public function resolveLogRequest($path, $langcode) {
$this->database->update('redirect_404')
->fields(['resolved' => 1])
->condition('path', $path)
->condition('langcode', $langcode)
->execute();
}
/**
* {@inheritdoc}
*/
public function purgeOldRequests() {
$row_limit = $this->configFactory->get('redirect_404.settings')->get('row_limit');
// In admin form 0 used as value for 'All' label.
if ($row_limit == 0) {
return;
}
$query = $this->database->select('redirect_404', 'r404');
$query->fields('r404', ['timestamp']);
// On databases known to support log(), use it to calculate a logarithmic
// scale of the count, to delete records with count of 1-9 first, then
// 10-99 and so on.
if ($this->database->driver() == 'mysql' || $this->database->driver() == 'pgsql') {
$query->addExpression('floor(log(10, count))', 'count_log');
$query->orderBy('count_log', 'DESC');
}
$query->orderBy('timestamp', 'DESC');
$cutoff = $query
->range($row_limit, 1)
->execute()
->fetchAssoc();
if (!empty($cutoff)) {
// Delete records having older timestamp and less visits (on a logarithmic
// scale) than cutoff.
$delete_query = $this->database->delete('redirect_404');
if ($this->database->driver() == 'mysql' || $this->database->driver() == 'pgsql') {
// Delete rows with same count_log AND older timestamp than cutoff.
$and_condition = $delete_query->andConditionGroup()
->where('floor(log(10, count)) = :count_log2', [':count_log2' => $cutoff['count_log']])
->condition('timestamp', $cutoff['timestamp'], '<=');
// And delete all the rows with count_log less than the cutoff.
$condition = $delete_query->orConditionGroup()
->where('floor(log(10, count)) < :count_log1', [':count_log1' => $cutoff['count_log']])
->condition($and_condition);
$delete_query->condition($condition);
}
else {
$delete_query->condition('timestamp', $cutoff['timestamp'], '<=');
}
$delete_query->execute();
}
}
/**
* {@inheritdoc}
*/
public function listRequests(array $header = [], $search = NULL) {
$query = $this->database
->select('redirect_404', 'r404')
->extend('Drupal\Core\Database\Query\TableSortExtender')
->orderByHeader($header)
->extend('Drupal\Core\Database\Query\PagerSelectExtender')
->limit(25)
->fields('r404');
if ($search) {
// Replace wildcards with PDO wildcards.
// @todo Find a way to write a nicer pattern.
$wildcard = '%' . trim(preg_replace('!\*+!', '%', $this->database->escapeLike($search)), '%') . '%';
$query->condition('path', $wildcard, 'LIKE');
}
$results = $query->condition('resolved', 0, '=')->execute()->fetchAll();
return $results;
}
}
@@ -0,0 +1,202 @@
<?php
namespace Drupal\redirect_404\Tests;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Url;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\redirect\Tests\AssertRedirectTrait;
/**
* UI tests for redirect_404 module with language and content translation.
*
* This runs the exact same tests as Fix404RedirectUITest, but with both
* language and content translation modules enabled.
*
* @group redirect_404
*/
class Fix404RedirectUILanguageTest extends Redirect404TestBase {
use AssertRedirectTrait;
/**
* Additional modules to enable.
*
* @var array
*/
public static $modules = ['language'];
/**
* Admin user's permissions for this test.
*
* @var array
*/
protected $adminPermissions = [
'administer redirects',
'administer redirect settings',
'access content',
'bypass node access',
'create url aliases',
'administer url aliases',
'administer languages',
];
/**
* {@inheritdoc}
*/
public function setUp() {
parent::setUp();
// Enable some languages for this test.
$language = ConfigurableLanguage::createFromLangcode('de');
$language->save();
$language = ConfigurableLanguage::createFromLangcode('es');
$language->save();
$language = ConfigurableLanguage::createFromLangcode('fr');
$language->save();
}
/**
* Tests the fix 404 pages workflow with language and content translation.
*/
public function testFix404RedirectList() {
// Visit a non existing page to have the 404 redirect_error entry.
$this->drupalGet('fr/testing');
$redirect = \Drupal::database()->select('redirect_404')
->fields('redirect_404')
->condition('path', '/testing')
->execute()
->fetchAll();
if (count($redirect) == 0) {
$this->fail('No record was added');
}
// Go to the "fix 404" page and check the listing.
$this->drupalGet('admin/config/search/redirect/404');
$this->assertText('testing');
$this->assertLanguageInTableBody('French');
// Check the Language view filter uses the default language filter.
$this->assertOption('edit-langcode', 'All');
$this->assertOption('edit-langcode', 'en');
$this->assertOption('edit-langcode', 'de');
$this->assertOption('edit-langcode', 'es');
$this->assertOption('edit-langcode', 'fr');
$this->assertOption('edit-langcode', LanguageInterface::LANGCODE_NOT_SPECIFIED);
$this->clickLink(t('Add redirect'));
// Check if we generate correct Add redirect url and if the form is
// pre-filled.
$destination = Url::fromRoute('redirect_404.fix_404')->getInternalPath();
$options = [
'query' => [
'source' => 'testing',
'language' => 'fr',
'destination' => $destination,
]
];
$this->assertUrl('admin/config/search/redirect/add', $options);
$this->assertFieldByName('redirect_source[0][path]', 'testing');
$this->assertOptionSelected('edit-language-0-value', 'fr');
// Save the redirect.
$edit = ['redirect_redirect[0][uri]' => '/node'];
$this->drupalPostForm(NULL, $edit, t('Save'));
$this->assertUrl('admin/config/search/redirect/404');
$this->assertText('There are no 404 errors to fix.');
// Check if the redirect works as expected.
$this->assertRedirect('fr/testing', 'fr/node', 'HTTP/1.1 301 Moved Permanently');
// Test removing a redirect assignment, visit again the non existing page.
$this->drupalGet('admin/config/search/redirect');
$this->assertText('testing');
$this->assertLanguageInTableBody('French');
$this->clickLink('Delete', 0);
$this->drupalPostForm(NULL, [], 'Delete');
$this->assertUrl('admin/config/search/redirect');
$this->assertText('There is no redirect yet.');
$this->drupalGet('admin/config/search/redirect/404');
$this->assertText('There are no 404 errors to fix.');
// Should be listed again in the 404 overview.
$this->drupalGet('fr/testing');
$this->drupalGet('admin/config/search/redirect/404');
$this->assertLanguageInTableBody('French');
// Check the error path visit count.
$this->assertFieldByXPath('//table/tbody/tr/td[2]', 2);
$this->clickLink('Add redirect');
// Save the redirect with a different langcode.
$this->assertFieldByName('redirect_source[0][path]', 'testing');
$this->assertOptionSelected('edit-language-0-value', 'fr');
$edit['language[0][value]'] = 'es';
$this->drupalPostForm(NULL, $edit, 'Save');
$this->assertUrl('admin/config/search/redirect/404');
// Should still be listed, redirecting to another language does not resolve
// the path.
$this->assertLanguageInTableBody('French');
$this->drupalGet('admin/config/search/redirect');
$this->assertLanguageInTableBody('Spanish');
// Check if the redirect works as expected.
$this->assertRedirect('es/testing', 'es/node', 'HTTP/1.1 301 Moved Permanently');
// Visit multiple non existing pages to test the Redirect 404 View.
$this->drupalGet('testing1');
$this->drupalGet('de/testing2');
$this->drupalGet('de/testing2?test=1');
$this->drupalGet('de/testing2?test=2');
$this->drupalGet('admin/config/search/redirect/404');
$this->assertLanguageInTableBody('French');
$this->assertLanguageInTableBody('English');
$this->assertLanguageInTableBody('German');
$this->assertText('testing1');
$this->assertText('testing2');
$this->assertText('testing2?test=1');
$this->assertText('testing2?test=2');
// Test the Language view filter.
$this->drupalGet('admin/config/search/redirect/404', ['query' => ['langcode' => 'de']]);
$this->assertText('English');
$this->assertNoLanguageInTableBody('English');
$this->assertLanguageInTableBody('German');
$this->assertNoText('testing1');
$this->assertText('testing2');
$this->assertText('testing2?test=1');
$this->assertText('testing2?test=2');
$this->drupalGet('admin/config/search/redirect/404');
$this->assertLanguageInTableBody('English');
$this->assertLanguageInTableBody('German');
$this->assertText('testing1');
$this->assertText('testing2');
$this->assertText('testing2?test=1');
$this->assertText('testing2?test=2');
$this->drupalGet('admin/config/search/redirect/404', ['query' => ['langcode' => 'en']]);
$this->assertLanguageInTableBody('English');
$this->assertNoLanguageInTableBody('German');
$this->assertText('testing1');
$this->assertNoText('testing2');
$this->assertNoText('testing2?test=1');
$this->assertNoText('testing2?test=2');
// Assign a redirect to 'testing1'.
$this->clickLink('Add redirect');
$options = [
'query' => [
'source' => 'testing1',
'language' => 'en',
'destination' => $destination,
]
];
$this->assertUrl('admin/config/search/redirect/add', $options);
$this->assertFieldByName('redirect_source[0][path]', 'testing1');
$this->assertOptionSelected('edit-language-0-value', 'en');
$edit = ['redirect_redirect[0][uri]' => '/node'];
$this->drupalPostForm(NULL, $edit, t('Save'));
$this->assertUrl('admin/config/search/redirect/404');
$this->assertNoLanguageInTableBody('English');
$this->assertLanguageInTableBody('German');
$this->drupalGet('admin/config/search/redirect');
$this->assertLanguageInTableBody('Spanish');
$this->assertLanguageInTableBody('English');
// Check if the redirect works as expected.
$this->assertRedirect('/testing1', '/node', 'HTTP/1.1 301 Moved Permanently');
}
}
@@ -0,0 +1,185 @@
<?php
namespace Drupal\redirect_404\Tests;
use Drupal\Core\Url;
/**
* UI tests for redirect_404 module.
*
* @group redirect_404
*/
class Fix404RedirectUITest extends Redirect404TestBase {
/**
* Tests the fix 404 pages workflow.
*/
public function testFix404Pages() {
// Visit a non existing page to have the 404 redirect_error entry.
$this->drupalGet('non-existing0');
// Go to the "fix 404" page and check the listing.
$this->drupalGet('admin/config/search/redirect/404');
$this->assertText('non-existing0');
$this->clickLink(t('Add redirect'));
// Check if we generate correct Add redirect url and if the form is
// pre-filled.
$destination = Url::fromRoute('redirect_404.fix_404')->getInternalPath();
$options = [
'query' => [
'source' => 'non-existing0',
'language' => 'en',
'destination' => $destination,
]
];
$this->assertUrl('admin/config/search/redirect/add', $options);
$this->assertFieldByName('redirect_source[0][path]', 'non-existing0');
// Save the redirect.
$edit = ['redirect_redirect[0][uri]' => '/node'];
$this->drupalPostForm(NULL, $edit, t('Save'));
$this->assertUrl('admin/config/search/redirect/404');
$this->assertText('There are no 404 errors to fix.');
// Check if the redirect works as expected.
$this->drupalGet('non-existing0');
$this->assertUrl('node');
// Test removing a redirect assignment, visit again the non existing page.
$this->drupalGet('admin/config/search/redirect');
$this->assertText('non-existing0');
$this->clickLink('Delete', 0);
$this->drupalPostForm(NULL, [], 'Delete');
$this->assertUrl('admin/config/search/redirect');
$this->assertText('There is no redirect yet.');
$this->drupalGet('admin/config/search/redirect/404');
$this->assertText('There are no 404 errors to fix.');
// Should be listed again in the 404 overview.
$this->drupalGet('non-existing0');
$this->drupalGet('admin/config/search/redirect/404');
$this->assertText('non-existing0');
// Visit multiple non existing pages to test the Redirect 404 View.
$this->drupalGet('non-existing0?test=1');
$this->drupalGet('non-existing0?test=2');
$this->drupalGet('non-existing1');
$this->drupalGet('non-existing2');
$this->drupalGet('admin/config/search/redirect/404');
$this->assertText('non-existing0?test=1');
$this->assertText('non-existing0?test=2');
$this->assertText('non-existing0');
$this->assertText('non-existing1');
$this->assertText('non-existing2');
// Test the Path view filter.
$this->drupalGet('admin/config/search/redirect/404', ['query' => ['path' => 'test=']]);
$this->assertText('non-existing0?test=1');
$this->assertText('non-existing0?test=2');
$this->assertNoText('non-existing1');
$this->assertNoText('non-existing2');
$this->drupalGet('admin/config/search/redirect/404', ['query' => ['path' => 'existing1']]);
$this->assertNoText('non-existing0?test=1');
$this->assertNoText('non-existing0?test=2');
$this->assertNoText('non-existing0');
$this->assertText('non-existing1');
$this->assertNoText('non-existing2');
$this->drupalGet('admin/config/search/redirect/404');
$this->assertText('non-existing0?test=1');
$this->assertText('non-existing0?test=2');
$this->assertText('non-existing0');
$this->assertText('non-existing1');
$this->assertText('non-existing2');
$this->drupalGet('admin/config/search/redirect/404', ['query' => ['path' => 'g2']]);
$this->assertNoText('non-existing0?test=1');
$this->assertNoText('non-existing0?test=2');
$this->assertNoText('non-existing0');
$this->assertNoText('non-existing1');
$this->assertText('non-existing2');
// Assign a redirect to 'non-existing2'.
$this->clickLink('Add redirect');
$options = [
'query' => [
'source' => 'non-existing2',
'language' => 'en',
'destination' => $destination,
]
];
$this->assertUrl('admin/config/search/redirect/add', $options);
$this->assertFieldByName('redirect_source[0][path]', 'non-existing2');
$this->drupalPostForm(NULL, $edit, t('Save'));
$this->assertUrl('admin/config/search/redirect/404');
$this->assertText('non-existing0?test=1');
$this->assertText('non-existing0?test=2');
$this->assertText('non-existing0');
$this->assertText('non-existing1');
$this->assertNoText('non-existing2');
// Check if the redirect works as expected.
$this->drupalGet('admin/config/search/redirect');
$this->assertText('non-existing2');
}
/**
* Tests the redirect ignore pages.
*/
public function testIgnorePages() {
// Create two nodes.
$node1 = $this->drupalCreateNode(['type' => 'page']);
$node2 = $this->drupalCreateNode(['type' => 'page']);
// Set some pages to be ignored just for the test.
$node_to_ignore = '/node/' . $node1->id() . '/test';
$terms_to_ignore = '/term/*';
$pages = $node_to_ignore . "\r\n" . $terms_to_ignore;
\Drupal::configFactory()
->getEditable('redirect_404.settings')
->set('pages', $pages)
->save();
// Visit ignored or non existing pages.
$this->drupalGet('node/' . $node1->id() . '/test');
$this->drupalGet('term/foo');
$this->drupalGet('term/1');
// Go to the "fix 404" page and check there are no 404 entries.
$this->drupalGet('admin/config/search/redirect/404');
$this->assertNoText('node/' . $node1->id() . '/test');
$this->assertNoText('term/foo');
$this->assertNoText('term/1');
// Visit non existing but 'unignored' page.
$this->drupalGet('node/' . $node2->id() . '/test');
// Go to the "fix 404" page and check there is a 404 entry.
$this->drupalGet('admin/config/search/redirect/404');
$this->assertText('node/' . $node2->id() . '/test');
// Add this 404 entry to the 'ignore path' list, assert it works properly.
$path_to_ignore = '/node/' . $node2->id() . '/test';
$destination = '&destination=admin/config/search/redirect/404';
$this->clickLink('Ignore');
$this->assertUrl('admin/config/search/redirect/settings?ignore=' . $path_to_ignore . $destination);
$this->assertText('Resolved the path ' . $path_to_ignore . ' in the database. Please check the ignored list and save the settings.');
$xpath = $this->xpath('//*[@id="edit-ignore-pages"]')[0]->asXML();
$this->assertTrue(strpos($xpath, $node_to_ignore), $node_to_ignore . " in 'Path to ignore' found");
$this->assertTrue(strpos($xpath, $terms_to_ignore), $terms_to_ignore . " in 'Path to ignore' found");
$this->assertTrue(strpos($xpath, $path_to_ignore), $path_to_ignore . " in 'Path to ignore' found");
// Save the path with wildcard, but omitting the leading slash.
$nodes_to_ignore = 'node/*';
$edit = ['ignore_pages' => $nodes_to_ignore . "\r\n" . $terms_to_ignore];
$this->drupalPostForm(NULL, $edit, 'Save configuration');
// Should redirect to 'Fix 404'. Check the 404 entry is not shown anymore.
$this->assertUrl('admin/config/search/redirect/404');
$this->assertText('Configuration was saved.');
$this->assertNoText('node/' . $node2->id() . '/test');
$this->assertText('There are no 404 errors to fix.');
// Go back to the settings to check the 'Path to ignore' configurations.
$this->drupalGet('admin/config/search/redirect/settings');
$xpath = $this->xpath('//*[@id="edit-ignore-pages"]')[0]->asXML();
// Check that the new page to ignore has been saved with leading slash.
$this->assertTrue(strpos($xpath, '/' . $nodes_to_ignore), '/' . $nodes_to_ignore . " in 'Path to ignore' found");
$this->assertTrue(strpos($xpath, $terms_to_ignore), $terms_to_ignore . " in 'Path to ignore' found");
$this->assertFalse(strpos($xpath, $node_to_ignore), $node_to_ignore . " in 'Path to ignore' found");
$this->assertFalse(strpos($xpath, $path_to_ignore), $path_to_ignore . " in 'Path to ignore' found");
}
}
@@ -0,0 +1,80 @@
<?php
namespace Drupal\redirect_404\Tests;
/**
* Tests suppressing 404 logs if the suppress_404 setting is enabled.
*
* @group redirect_404
*/
class Redirect404LogSuppressorTest extends Redirect404TestBase {
/**
* Additional modules to enable.
*
* @var array
*/
public static $modules = ['dblog'];
/**
* A user with some relevant administrative permissions.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* A user without any permissions.
*
* @var \Drupal\user\UserInterface
*/
protected $webUser;
/**
* {@inheritdoc}
*/
public function setUp() {
parent::setUp();
// Create users with specific permissions.
$this->adminUser = $this->drupalCreateUser([
'administer redirect settings',
'administer redirects',
]);
$this->webUser = $this->drupalCreateUser([]);
}
/**
* Tests the suppress_404 service.
*/
public function testSuppress404Events() {
// Cause a page not found and an access denied event.
$this->drupalGet('page-not-found');
$this->assertResponse(404);
$this->drupalLogin($this->webUser);
$this->drupalGet('admin/reports/dblog');
$this->assertResponse(403);
// Assert the events are logged in the dblog reports.
$this->assertEqual(db_query("SELECT COUNT(*) FROM {watchdog} WHERE type = 'page not found'")->fetchField(), 1);
$this->assertEqual(db_query("SELECT COUNT(*) FROM {watchdog} WHERE type = 'access denied'")->fetchField(), 1);
// Login as admin and enable suppress_404 to avoid logging the 404 event.
$this->drupalLogin($this->adminUser);
$edit = ['suppress_404' => TRUE];
$this->drupalPostForm('admin/config/search/redirect/settings', $edit, 'Save configuration');
// Cause again a page not found and an access denied event.
$this->drupalGet('page-not-found');
$this->assertResponse(404);
$this->drupalLogin($this->webUser);
$this->drupalGet('admin/reports/dblog');
$this->assertResponse(403);
// Assert only the new access denied event is logged now.
$this->drupalLogin($this->adminUser);
$this->assertEqual(db_query("SELECT COUNT(*) FROM {watchdog} WHERE type = 'page not found'")->fetchField(), 1);
$this->assertEqual(db_query("SELECT COUNT(*) FROM {watchdog} WHERE type = 'access denied'")->fetchField(), 2);
}
}
@@ -0,0 +1,144 @@
<?php
namespace Drupal\redirect_404\Tests;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\simpletest\WebTestBase;
/**
* This class provides methods specifically for testing redirect 404 paths.
*/
abstract class Redirect404TestBase extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = [
'redirect_404',
'node',
'path',
];
/**
* Permissions for the admin user.
*
* @var array
*/
protected $adminPermissions = [
'administer redirects',
'administer redirect settings',
'access content',
'bypass node access',
'create url aliases',
'administer url aliases',
];
/**
* A user with administrative permissions.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* {@inheritdoc}
*/
public function setUp() {
parent::setUp();
// Create an admin user.
$this->adminUser = $this->drupalCreateUser($this->adminPermissions);
$this->drupalLogin($this->adminUser);
$this->drupalCreateContentType(['type' => 'page', 'name' => 'Page']);
}
/**
* Passes if the language of the 404 path IS found on the loaded page.
*
* Because assertText() checks also in the Language select options, this
* specific assertion in the redirect 404 table body is needed.
*
* @param string $language
* The language to assert in the redirect 404 table body.
* @param string $body
* (optional) The table body xpath where to assert the language. Defaults
* to '//table/tbody'.
* @param string $message
* (optional) A message to display with the assertion. Do not translate
* messages: use \Drupal\Component\Utility\SafeMarkup::format() to embed
* variables in the message text, not t(). If left blank, a default message
* will be displayed.
*
* @return bool
* TRUE on pass, FALSE on fail.
*/
protected function assertLanguageInTableBody($language, $body = '//table/tbody', $message = '') {
return $this->assertLanguageInTableBodyHelper($language, $body, $message, FALSE);
}
/**
* Passes if the language of the 404 path is NOT found on the loaded page.
*
* Because assertText() checks also in the Language select options, this
* specific assertion in the redirect 404 table body is needed.
*
* @param string $language
* The language to assert in the redirect 404 table body.
* @param string $body
* (optional) The table body xpath where to assert the language. Defaults
* to '//table/tbody'.
* @param string $message
* (optional) A message to display with the assertion. Do not translate
* messages: use \Drupal\Component\Utility\SafeMarkup::format() to embed
* variables in the message text, not t(). If left blank, a default message
* will be displayed.
*
* @return bool
* TRUE on pass, FALSE on fail.
*/
protected function assertNoLanguageInTableBody($language, $body = '//table/tbody', $message = '') {
return $this->assertLanguageInTableBodyHelper($language, $body, $message, TRUE);
}
/**
* Helper for assertLanguageInTableBody and assertNoLanguageInTableBody.
*
* @param array $language
* The language to assert in the redirect 404 table body.
* @param string $body
* (optional) The table body xpath where to assert the language. Defaults
* to '//table/tbody'.
* @param string $message
* (optional) A message to display with the assertion. Do not translate
* messages: use \Drupal\Component\Utility\SafeMarkup::format() to embed
* variables in the message text, not t(). If left blank, a default message
* will be displayed.
* @param bool $not_exists
* (optional) TRUE if this language should not exist, FALSE if it should.
* Defaults to TRUE.
*
* @return bool
* TRUE on pass, FALSE on fail.
*/
protected function assertLanguageInTableBodyHelper($language, $body = '//table/tbody', $message = '', $not_exists = TRUE) {
if (!$message) {
if (!$not_exists) {
$message = new FormattableMarkup('Language "@language" found in 404 table.', ['@language' => $language]);
}
else {
$message = new FormattableMarkup('Language "@language" not found in 404 table.', ['@language' => $language]);
}
}
if ($not_exists) {
return $this->assertFalse(strpos($this->xpath($body)[0]->asXML(), $language), $message);
}
else {
return $this->assertTrue(strpos($this->xpath($body)[0]->asXML(), $language), $message);
}
}
}
@@ -0,0 +1,192 @@
<?php
namespace Drupal\Tests\redirect_404\Kernel;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests the clean up cron job for redirect_404.
*
* @group redirect_404
*/
class Fix404RedirectCronJobTest extends KernelTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['redirect_404'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installSchema('redirect_404', 'redirect_404');
// Insert some records in the test table with a given count and timestamp.
$this->insert404Row('/test1', 12, strtotime('now'));
$this->insert404Row('/test2', 5, strtotime('-1 hour'));
$this->insert404Row('/test3', 315, strtotime('-1 week'));
$this->insert404Row('/test4', 300, strtotime('-1 month'));
$this->insert404Row('/test5', 1557, strtotime('-1 week'));
$this->insert404Row('/test6', 1, strtotime('-1 day'));
}
/**
* Tests adding and deleting rows from redirect_404 table.
*/
function testRedirect404CronJob() {
// Set the limit to 3 just for the test.
\Drupal::configFactory()
->getEditable('redirect_404.settings')
->set('row_limit', 3)
->save();
// Check that there are 6 rows in the redirect_404 table.
$result = db_query("SELECT COUNT(*) FROM {redirect_404}")->fetchField();
$this->assertEquals(6, $result);
// Run cron to drop 3 rows from the redirect_404 test table.
redirect_404_cron();
$result = db_query("SELECT COUNT(*) FROM {redirect_404}")->fetchField();
$this->assertEquals(3, $result);
// Check there are only 3 rows with more count in the redirect_404 table.
if (\Drupal::database()->driver() == 'mysql' || \Drupal::database()->driver() == 'pgsql') {
$this->assertNo404Row('/test1');
$this->assertNo404Row('/test2');
$this->assert404Row('/test3');
$this->assert404Row('/test4');
$this->assert404Row('/test5');
$this->assertNo404Row('/test6');
}
else {
// In SQLite is the opposite: the 3 rows kept are the newest ones.
$this->assert404Row('/test1');
$this->assert404Row('/test2');
$this->assertNo404Row('/test3');
$this->assertNo404Row('/test4');
$this->assertNo404Row('/test5');
$this->assert404Row('/test6');
}
}
/**
* Tests adding rows and deleting one row from redirect_404 table.
*/
function testRedirect404CronJobKeepAllButOne() {
// Set the limit to 5 just for the test.
\Drupal::configFactory()
->getEditable('redirect_404.settings')
->set('row_limit', 5)
->save();
// Check that there are 6 rows in the redirect_404 table.
$result = db_query("SELECT COUNT(*) FROM {redirect_404}")->fetchField();
$this->assertEquals(6, $result);
// Run cron to drop just 1 row from the redirect_404 test table.
redirect_404_cron();
$result = db_query("SELECT COUNT(*) FROM {redirect_404}")->fetchField();
$this->assertEquals(5, $result);
// Check only the row with least count has been removed from the table.
if (\Drupal::database()->driver() == 'mysql' || \Drupal::database()->driver() == 'pgsql') {
$this->assert404Row('/test1');
$this->assert404Row('/test2');
$this->assert404Row('/test3');
$this->assert404Row('/test4');
$this->assert404Row('/test5');
$this->assertNo404Row('/test6');
}
else {
// In SQlite, only the oldest row is deleted.
$this->assert404Row('/test1');
$this->assert404Row('/test2');
$this->assert404Row('/test3');
$this->assertNo404Row('/test4');
$this->assert404Row('/test5');
$this->assert404Row('/test6');
}
}
/**
* Inserts a 404 request log in the redirect_404 test table.
*
* @param string $path
* The path of the request.
* @param int $count
* (optional) The visits count of the request.
* @param int $timestamp
* (optional) The timestamp of the last visited request.
* @param string $langcode
* (optional) The langcode of the request.
*/
protected function insert404Row($path, $count = 1, $timestamp = 0, $langcode = 'en') {
db_insert('redirect_404')
->fields([
'path' => $path,
'langcode' => $langcode,
'count' => $count,
'timestamp' => $timestamp,
'resolved' => 0,
])
->execute();
}
/**
* Passes if the row with the given parameters is in the redirect_404 table.
*
* @param string $path
* The path of the request.
* @param string $langcode
* (optional) The langcode of the request.
*/
protected function assert404Row($path, $langcode = 'en') {
$this->assert404RowHelper($path, $langcode, FALSE);
}
/**
* Passes if the row with the given parameters is NOT in the redirect_404 table.
*
* @param string $path
* The path of the request.
* @param string $langcode
* (optional) The langcode of the request.
*/
protected function assertNo404Row($path, $langcode = 'en') {
$this->assert404RowHelper($path, $langcode, TRUE);
}
/**
* Passes if the row with the given parameters is in the redirect_404 table.
*
* @param string $path
* The path of the request.
* @param string $langcode
* (optional) The langcode of the request.
* @param bool $not_exists
* (optional) TRUE if this 404 row should not exist in the redirect_404
* table, FALSE if it should. Defaults to TRUE.
*/
protected function assert404RowHelper($path, $langcode = 'en', $not_exists = TRUE) {
$result = db_select('redirect_404', 'r404')
->fields('r404', ['path'])
->condition('path', $path)
->condition('langcode', $langcode)
->condition('resolved', 0)
->execute()
->fetchField();
if ($not_exists) {
$this->assertNotEquals($path, $result);
}
else {
$this->assertEquals($path, $result);
}
}
}
@@ -0,0 +1,78 @@
<?php
namespace Drupal\Tests\redirect_404\Unit;
use Drupal\Core\Database\Connection;
use Drupal\Core\Language\LanguageInterface;
use Drupal\redirect_404\SqlRedirectNotFoundStorage;
use Drupal\Tests\UnitTestCase;
/**
* Tests that overly long paths aren't logged.
*
* @group redirect_404
*/
class SqlRedirectNotFoundStorageTest extends UnitTestCase {
/**
* Mock database connection.
*
* @var \Drupal\Core\Database\Connection|\PHPUnit_Framework_MockObject_MockObject
*/
protected $database;
/**
* Mock config factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $configFactory;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->database = $this->getMockBuilder(Connection::class)
->disableOriginalConstructor()
->getMock();
}
/**
* Tests that long paths aren't stored in the database.
*/
public function testLongPath() {
$this->database->expects($this->never())
->method('merge');
$storage = new SqlRedirectNotFoundStorage($this->database, $this->getConfigFactoryStub());
$storage->logRequest($this->randomMachineName(SqlRedirectNotFoundStorage::MAX_PATH_LENGTH + 1), LanguageInterface::LANGCODE_DEFAULT);
}
/**
* Tests that invalid UTF-8 paths are not stored in the database.
*/
public function testInvalidUtf8Path() {
$this->database->expects($this->never())
->method('merge');
$storage = new SqlRedirectNotFoundStorage($this->database, $this->getConfigFactoryStub());
$storage->logRequest("Caf\xc3", LanguageInterface::LANGCODE_DEFAULT);
}
/**
* Tests that all logs are kept if row limit config is "All".
*/
public function testPurgeOldRequests() {
$this->configFactory = $this->getConfigFactoryStub(
[
'redirect_404.settings' => [
'row_limit' => 0,
],
]
);
$storage = new SqlRedirectNotFoundStorage($this->database, $this->configFactory);
$storage->purgeOldRequests();
$this->database->expects($this->never())
->method('select');
}
}
@@ -0,0 +1,18 @@
redirect_domain.domains:
type: config_object
label: 'Redirect domains'
mapping:
domain_redirects:
type: sequence
label: 'Domain redirects'
sequence:
type: sequence
sequence:
type: mapping
mapping:
sub_path:
type: string
label: 'Sub path'
destination:
type: string
label: 'Destination'
@@ -0,0 +1,13 @@
name: 'Redirect Domain'
type: module
description: 'Allows users to redirect between domains.'
# core: 8.x
dependencies:
- redirect
# Information added by Drupal.org packaging script on 2018-03-12
version: '8.x-1.1'
core: '8.x'
project: 'redirect'
datestamp: 1520897884
@@ -0,0 +1,5 @@
redirect_domain.domain_list:
title: 'Domain redirects'
base_route: redirect.list
route_name: redirect_domain.domain_list
weight: 60
@@ -0,0 +1,33 @@
<?php
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Url;
/**
* Implements hook_help().
*/
function redirect_domain_help($route_name, RouteMatchInterface $route_match) {
$output = '';
switch ($route_name) {
case 'help.page.redirect_domain':
$output = '<h3>' . t('About') . '</h3>';
$output .= '<p>' . t('The Redirect domain module allows users to redirect between domains.') . '</p>';
$output .= '<h3>' . t('Uses') . '</h3>';
$output .= '<dt>' . t('Manage domain redirects') . '</dt>';
$output .= '<dd>' . t('The domain redirect is accessed through <a href=":domainlist">Domain Redirects</a>. The user can add the domain redirects through the domain redirect table which consists of the domain from which it needs to be redirected, the sub path and the complete url destination to which it needs to be redirected. The module also supports the usage of a wildcard redirecting, thus many requests can be handled with one instance of domain redirect.', [':domainlist' => Url::fromRoute('redirect_domain.domain_list')->toString()]) . '</dd>';
return $output;
case 'redirect_domain.domain_list':
$output = '<p>' . t('The domain redirect table consists of the domain from which it needs to be redirected, the sub path and the complete url destination to which it needs to be redirected.') . '</p>';
$output .= '<h5>' . t('Example Configuration') . '</h5>';
$output .= '<ul>';
$output .= '<li> example.com/redirect => redirected.com/example-path </li>';
$output .= '<li> foo.com/* => bar.com </li>';
$output .= '</ul>';
$output .= '<h5>' . t('Example Redirects') . '</h5>';
$output .= '<ul>';
$output .= '<li>' . t('Request: example.com/redirect => Response: redirected.com/example-path') . '</li>';
$output .= '<li>' . t('Request: foo.com/any-path => Response: bar.com') . '</li>';
$output .= '</ul>';
return $output;
}
}
@@ -0,0 +1,7 @@
redirect_domain.domain_list:
path: '/admin/config/search/redirect/domain'
defaults:
_title: 'Domain redirects'
_form: '\Drupal\redirect_domain\Form\RedirectDomainForm'
requirements:
_permission: 'administer redirects'
@@ -0,0 +1,6 @@
services:
redirect_domain.request_subscriber:
class: Drupal\redirect_domain\EventSubscriber\DomainRedirectRequestSubscriber
arguments: ['@config.factory', '@redirect.checker', '@path.matcher']
tags:
- { name: event_subscriber }
@@ -0,0 +1,133 @@
<?php
namespace Drupal\redirect_domain\EventSubscriber;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Path\PathMatcherInterface;
use Drupal\Core\Routing\TrustedRedirectResponse;
use Drupal\Core\Url;
use Drupal\redirect\RedirectChecker;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Redirect subscriber for controller requests.
*/
class DomainRedirectRequestSubscriber implements EventSubscriberInterface {
/**
* @var \Drupal\redirect\RedirectChecker
*/
protected $redirectChecker;
/**
* Domain redirect configuration.
*
* @var \Drupal\Core\Config\Config
*/
protected $domainConfig;
/**
* The path matcher.
*
* @var \Drupal\Core\Path\PathMatcherInterface
*/
protected $pathMatcher;
/**
* Redirect configuration.
*
* @var \Drupal\Core\Config\Config
*/
protected $redirectConfig;
/**
* Constructs a \Drupal\redirect\EventSubscriber\RedirectRequestSubscriber object.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
* @param \Drupal\redirect\RedirectChecker $redirect_checker
* The redirect checker service.
* @param \Drupal\Core\Path\PathMatcherInterface $path_matcher
* The path matcher.
*/
public function __construct(ConfigFactoryInterface $config_factory, RedirectChecker $redirect_checker, PathMatcherInterface $path_matcher) {
$this->domainConfig = $config_factory->get('redirect_domain.domains');
$this->redirectConfig = $config_factory->get('redirect.settings');
$this->redirectChecker = $redirect_checker;
$this->pathMatcher = $path_matcher;
}
/**
* Handles the domain redirect if any found.
*
* @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event
* The event to process.
*/
public function onKernelRequestCheckDomainRedirect(GetResponseEvent $event) {
$request = clone $event->getRequest();
if (!$this->redirectChecker->canRedirect($request)) {
return;
}
// Redirect between domains configuration.
$domains = $this->domainConfig->get('domain_redirects');
if (!empty($domains)) {
$host = $request->getHost();
$path = $request->getPathInfo();
$protocol = $request->getScheme() . '://';
$destination = NULL;
// Checks if there is a redirect domain in the configuration.
if (isset($domains[str_replace('.', ':', $host)])) {
foreach ($domains[str_replace('.', ':', $host)] as $item) {
if ($this->pathMatcher->matchPath($path, $item['sub_path'])) {
$destination = $item['destination'];
break;
}
}
if ($destination) {
// Use the default status code from Redirect.
$response = new TrustedRedirectResponse(
$protocol . $destination,
$this->redirectConfig->get('default_status_code')
);
$event->setResponse($response);
return;
}
}
}
}
/**
* Prior to set the response it check if we can redirect.
*
* @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event
* The event object.
* @param \Drupal\Core\Url $url
* The Url where we want to redirect.
*/
protected function setResponse(GetResponseEvent $event, Url $url) {
$request = $event->getRequest();
parse_str($request->getQueryString(), $query);
$url->setOption('query', $query);
$url->setAbsolute(TRUE);
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
// This needs to run before RouterListener::onKernelRequest(), which has
// a priority of 32 and
// RedirectRequestSubscriber::onKernelRequestCheckRedirect(), which has
// a priority of 33. Otherwise, that aborts the request if no matching
// route is found.
$events[KernelEvents::REQUEST][] = ['onKernelRequestCheckDomainRedirect', 34];
return $events;
}
}
@@ -0,0 +1,165 @@
<?php
namespace Drupal\redirect_domain\Form;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Provides a redirect domain configuration form.
*/
class RedirectDomainForm extends ConfigFormBase {
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'form_redirect_domain_form';
}
/**
* {@inheritdoc}
*/
protected function getEditableConfigNames() {
return [
'redirect_domain.domains',
];
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
if (!$form_state->has('maximum_domains')) {
$form_state->set('maximum_domains', 1);
}
$form['redirects'] = [
'#type' => 'table',
'#tree' => TRUE,
'#header' => [
$this->t('From domain'),
$this->t('Sub path'),
$this->t('Destination')
],
'#prefix' => '<div id="redirect-domain-wrapper">',
'#suffix' => '</div>',
];
$rows = [];
// Obtain domain redirects from configuration.
if ($domain_redirects = $this->config('redirect_domain.domains')->get('domain_redirects')) {
foreach ($domain_redirects as $key => $value) {
foreach ($value as $item) {
$form['redirects'][] = [
'from' => [
'#type' => 'textfield',
'#value' => str_replace(':','.',$key),
],
'sub_path' => [
'#type' => 'textfield',
'#value' => $item['sub_path'],
],
'destination' => [
'#type' => 'textfield',
'#value' => $item['destination'],
],
];
}
}
}
// Fields for the new domain redirects.
for ($i = 0; $i < $form_state->get('maximum_domains'); $i++) {
$form['redirects'][] = [
'from' => [
'#type' => 'textfield',
],
'sub_path' => [
'#type' => 'textfield',
'#value' => '/',
],
'destination' => [
'#type' => 'textfield',
],
];
}
$form['add'] = [
'#type' => 'submit',
'#value' => $this->t('Add another'),
'#submit' => ['::addAnotherSubmit'],
'#ajax' => [
'callback' => '::ajaxAddAnother',
'wrapper' => 'redirect-domain-wrapper',
],
];
$form['submit'] = [
'#type' => 'submit',
'#button_type' => 'primary',
'#value' => $this->t('Save'),
];
return $form;
}
/**
* Ajax callback for adding another domain redirect.
*
* @param array $form
* The form structure.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*
* @return array
* The new domain redirect form part.
*/
public function ajaxAddAnother(array $form, FormStateInterface $form_state) {
return $form['redirects'];
}
/**
* Submit callback for adding a new domain field.
*/
public function addAnotherSubmit(array $form, FormStateInterface $form_state) {
$form_state->set('maximum_domains', $form_state->get('maximum_domains') + 1);
$form_state->setRebuild(TRUE);
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
parent::validateForm($form, $form_state);
if ($redirects = $form_state->getValue('redirects')) {
foreach ($redirects as $redirect) {
if (strpos($redirect['from'], '://') !== FALSE) {
$form_state->setErrorByName('redirects', t('No protocol should be included in the redirect domain.'));
}
}
}
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$domain_redirects = [];
$domain_config = $this->config('redirect_domain.domains');
if ($redirects = $form_state->getValue('redirects')) {
foreach ($redirects as $redirect) {
if (!empty($redirect['from']) && !empty($redirect['destination'])) {
// Replace '.' with ':' for an eligible key.
$redirect['from'] = str_replace('.', ':', $redirect['from']);
$domain_redirects[$redirect['from']][] = [
'sub_path' => '/' . ltrim($redirect['sub_path'], '/'),
'destination' => $redirect['destination']
];
}
}
}
$domain_config->set('domain_redirects', $domain_redirects);
$domain_config->save();
drupal_set_message(t('The domain redirects have been saved.'));
}
}
@@ -0,0 +1,64 @@
<?php
namespace Drupal\redirect_domain\Tests;
use Drupal\simpletest\WebTestBase;
/**
* Tests the UI for domain redirect.
*
* @group redirect_domain
*/
class RedirectDomainUITest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = [
'redirect_domain',
];
/**
* Tests domain redirect.
*/
public function testDomainRedirect() {
$user = $this->drupalCreateUser([
'administer site configuration',
'access administration pages',
'administer redirects'
]);
$this->drupalLogin($user);
$this->drupalGet('/admin/config/search/redirect/domain');
// Assert that there are 2 domain redirect fields.
$this->assertFieldByName('redirects[0][from]');
$this->assertFieldByName('redirects[0][sub_path]');
$this->assertFieldByName('redirects[0][destination]');
// Add another field for new domain redirect.
$this->drupalPostAjaxForm(NULL, [], ['op' => t('Add another')]);
// Add two new domain redirects.
$edit = [
'redirects[0][from]' => 'foo.example.org',
'redirects[0][sub_path]' => '//sub-path',
'redirects[0][destination]' => 'www.example.org/foo',
'redirects[1][from]' => 'bar.example.org',
'redirects[1][sub_path]' => '',
'redirects[1][destination]' => 'www.example.org/bar',
];
$this->drupalPostForm(NULL, $edit, t('Save'));
// Check the new domain redirects.
$this->assertFieldByName('redirects[0][from]', 'foo.example.org');
$this->assertFieldByName('redirects[0][destination]', 'www.example.org/foo');
$this->assertFieldByName('redirects[1][from]', 'bar.example.org');
$this->assertFieldByName('redirects[1][destination]', 'www.example.org/bar');
// Ensure that the sub paths are correct.
$this->assertFieldByName('redirects[0][sub_path]', '/sub-path');
$this->assertFieldByName('redirects[1][sub_path]', '/');
}
}
@@ -0,0 +1,144 @@
<?php
namespace Drupal\Tests\redirect_domain\Unit;
use Drupal\Core\Path\PathMatcher;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\redirect\RedirectChecker;
use Drupal\redirect_domain\EventSubscriber\DomainRedirectRequestSubscriber;
use Drupal\Tests\UnitTestCase;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
/**
* Tests the redirect logic.
*
* @group redirect_domain
*
* @coversDefaultClass Drupal\redirect_domain\EventSubscriber\DomainRedirectRequestSubscriber
*/
class DomainRedirectRequestSubscriberTest extends UnitTestCase {
/**
* Tests redirect between domains.
*
* @dataProvider providerDomains
*/
public function testDomainRedirect($request_url, $response_url) {
$data = [
'redirect_domain.domains' => [
'domain_redirects' => [
'foo:com' => [
[
'sub_path' => '/fixedredirect',
'destination' => 'bar.com/fixedredirect',
],
[
'sub_path' => '/*',
'destination' => 'bar.com/example',
],
],
'example:com' => [
[
'sub_path' => '/foo/*/bar',
'destination' => 'example.com/bar/foo',
],
],
'simpleexample:com' => [
[
'sub_path' => '/redirect',
'destination' => 'redirected.com/redirect',
],
],
],
],
'redirect.settings' => [
'default_status_code' => 301,
],
'system.site' => [
'page.front' => '/',
],
];
// Create a mock redirect checker.
$checker = $this->getMockBuilder(RedirectChecker::class)
->disableOriginalConstructor()
->getMock();
$checker->expects($this->any())
->method('canRedirect')
->will($this->returnValue(TRUE));
// Set up the configuration for the requested domain.
$config_factory = $this->getConfigFactoryStub($data);
// Create a mock path matcher.
$route_match = $this->getMock(RouteMatchInterface::class);
$path_matcher = new PathMatcher($config_factory, $route_match);
$subscriber = new DomainRedirectRequestSubscriber(
$config_factory,
$checker,
$path_matcher
);
// Make a request to the urls from the data provider and get the response.
$event = $this->getGetResponseEventStub($request_url, http_build_query([]));
// Run the main redirect method.
$subscriber->onKernelRequestCheckDomainRedirect($event);
// Assert the expected response from the data provider.
if ($response_url) {
$this->assertTrue($event->getResponse() instanceof RedirectResponse);
$response = $event->getResponse();
// Make sure that the response is properly redirected.
$this->assertEquals($response_url, $response->getTargetUrl());
$this->assertEquals(
$config_factory->get('redirect.settings')->get('default_status_code'),
$response->getStatusCode()
);
}
else {
$this->assertNull($event->getResponse());
}
}
/**
* Gets response event object.
*
* @param $path_info
* The path info.
* @param $query_string
* The query string in the url.
*
* @return GetResponseEvent
* The response for the request.
*/
protected function getGetResponseEventStub($path_info, $query_string) {
$request = Request::create($path_info . '?' . $query_string, 'GET', [], [], [], ['SCRIPT_NAME' => 'index.php']);
$http_kernel = $this->getMockBuilder(HttpKernelInterface::class)
->getMock();
return new GetResponseEvent($http_kernel, $request, 'test');
}
/**
* Data provider for the domain redirects.
*
* @return array
* An array of requests and expected responses for the redirect domains.
*/
public function providerDomains() {
$datasets = [];
$datasets[] = ['http://foo.com/example', 'http://bar.com/example'];
$datasets[] = ['http://example.com/foo/test/bar', 'http://example.com/bar/foo'];
$datasets[] = ['http://simpleexample.com/redirect', 'http://redirected.com/redirect'];
$datasets[] = ['http://nonexisting.com', NULL];
$datasets[] = ['http://simpleexample.com/wrongpath', NULL];
$datasets[] = ['http://foo.com/fixedredirect', 'http://bar.com/fixedredirect'];
return $datasets;
}
}
@@ -0,0 +1,142 @@
<?php
/**
* @file
* Hooks provided by the Redirect module.
*/
/**
* @defgroup redirect_api_hooks Redirect API Hooks
* @{
* During redirect operations (create, update, view, delete, etc.), there are
* several sets of hooks that get invoked to allow modules to modify the
* redirect operation:
* - All-module hooks: Generic hooks for "redirect" operations. These are
* always invoked on all modules.
* - Entity hooks: Generic hooks for "entity" operations. These are always
* invoked on all modules.
*
* Here is a list of the redirect and entity hooks that are invoked, and other
* steps that take place during redirect operations:
* - Creating a new redirect (calling redirect_save() on a new redirect):
* - hook_redirect_presave() (all)
* - Redirect written to the database
* - hook_redirect_insert() (all)
* - hook_entity_insert() (all)
* - Updating an existing redirect (calling redirect_save() on an existing redirect):
* - hook_redirect_presave() (all)
* - Redirect written to the database
* - hook_redirect_update() (all)
* - hook_entity_update() (all)
* - Loading a redirect (calling redirect_load(), redirect_load_multiple(), or
* entity_load() with $entity_type of 'redirect'):
* - Redirect information is read from database.
* - hook_entity_load() (all)
* - hook_redirect_load() (all)
* - Deleting a redirect (calling redirect_delete() or redirect_delete_multiple()):
* - Redirect is loaded (see Loading section above)
* - Redirect information is deleted from database
* - hook_redirect_delete() (all)
* - hook_entity_delete() (all)
* - Preparing a redirect for editing (note that if it's
* an existing redirect, it will already be loaded; see the Loading section
* above):
* - hook_redirect_prepare() (all)
* - Validating a redirect during editing form submit (calling
* redirect_form_validate()):
* - hook_redirect_validate() (all)
* @}
*/
/**
* @addtogroup hooks
* @{
*/
/**
* Act on redirects being loaded from the database.
*
* This hook is invoked during redirect loading, which is handled by
* entity_load(), via classes RedirectController and
* DrupalDefaultEntityController. After the redirect information is read from
* the database or the entity cache, hook_entity_load() is invoked on all
* implementing modules, and then hook_redirect_load() is invoked on all
* implementing modules.
*
* This hook should only be used to add information that is not in the redirect
* table, not to replace information that is in that table (which could
* interfere with the entity cache). For performance reasons, information for
* all available redirects should be loaded in a single query where possible.
*
* The $types parameter allows for your module to have an early return (for
* efficiency) if your module only supports certain redirect types.
*
* @param $redirects
* An array of the redirects being loaded, keyed by rid.
* @param $types
* An array containing the types of the redirects.
*
* @ingroup redirect_api_hooks
*/
function hook_redirect_load(array &$redirects, $types) {
}
/**
* Alter the list of redirects matching a certain source.
*
* @param $redirects
* An array of redirect objects.
* @param $source
* The source request path.
* @param $context
* An array with the following key/value pairs:
* - language: The language code of the source request.
* - query: An array of the source request query string.
*
* @see redirect_load_by_source()
* @ingroup redirect_api_hooks
*/
function hook_redirect_load_by_source_alter(array &$redirects, $source, array $context) {
foreach ($redirects as $rid => $redirect) {
if ($redirect->source !== $source) {
// If the redirects to do not exactly match $source (e.g. case
// insensitive matches), then remove them from the results.
unset($redirects[$rid]);
}
}
}
/**
* Act on a redirect object about to be shown on the add/edit form.
*
* This hook is invoked from redirect_create().
*
* @param $redirect
* The redirect that is about to be shown on the add/edit form.
*
* @ingroup redirect_api_hooks
*/
function hook_redirect_prepare($redirect) {
}
/**
* Act on a redirect being redirected.
*
* This hook is invoked from redirect_redirect() before the redirect callback
* is invoked.
*
* @param $redirect
* The redirect that is being used for the redirect.
*
* @see redirect_redirect()
* @see drupal_page_is_cacheable()
* @ingroup redirect_api_hooks
*/
function hook_redirect_alter($redirect) {
}
/**
* @} End of "addtogroup hooks".
*/
@@ -0,0 +1,72 @@
<?php
/**
* @file
* Drush integration for the redirect module.
*/
/**
* Implements hook_drush_command().
*/
function redirect_drush_command() {
$items['generate-redirects'] = array(
'description' => 'Create redirects.',
'drupal dependencies' => array('devel_generate'),
'arguments' => array(
'count' => 'Number of redirects to generate.',
),
'options' => array(
'delete' => 'Delete all redirects before generating new ones.',
),
);
return $items;
}
/**
* Command callback. Generate a number of redirects.
*/
function drush_redirect_generate_redirects($count = NULL) {
if (drush_generate_is_number($count) == FALSE) {
return drush_set_error('DEVEL_GENERATE_INVALID_INPUT', t('Invalid number of redirects.'));
}
module_load_include('inc', 'redirect', 'redirect.generate');
drush_generate_include_devel();
redirect_run_unprogressive_batch('redirect_generate_redirects_batch_info', $count, drush_get_option('delete'));
}
/**
* Perform an unprogressive batch process for CLI.
*/
function redirect_run_unprogressive_batch() {
$batch = batch_get();
if (!empty($batch)) {
// If there is already something in the batch, don't run.
return FALSE;
}
$args = func_get_args();
$batch_callback = array_shift($args);
if (!lock_acquire($batch_callback)) {
return FALSE;
}
// Attempt to increase the execution time.
drupal_set_time_limit(240);
// Build the batch array.
$batch = call_user_func_array($batch_callback, $args);
batch_set($batch);
// We need to manually set the progressive variable again.
// @todo Remove when http://drupal.org/node/638712 is fixed.
$batch =& batch_get();
$batch['progressive'] = FALSE;
// Run the batch process.
batch_process();
lock_release($batch_callback);
return TRUE;
}
@@ -0,0 +1,194 @@
<?php
/**
* @file
* Generate callbacks for the redirect module.
*/
use Drupal\Component\Utility\Random;
use Drupal\devel_generate\DevelGenerateBase;
use Drupal\redirect\Entity\Redirect;
/**
* @file
* Devel generate integration for the redirect module.
*/
function redirect_generate_form() {
$form['count'] = array(
'#type' => 'textfield',
'#title' => t('How many URL redirects would you like to generate?'),
'#default_value' => 50,
'#size' => 4,
);
$form['delete'] = array(
'#type' => 'checkbox',
'#title' => t('Delete all URL redirects before generating new URL redirects.'),
'#default_value' => FALSE,
);
$form['submit'] = array(
'#type' => 'submit',
'#value' => t('Generate'),
);
return $form;
}
function redirect_generate_form_submit(&$form, &$form_state) {
// Run the batch.
$batch = redirect_generate_redirects_batch_info($form_state['values']['count'], $form_state['values']['delete']);
batch_set($batch);
}
function redirect_generate_redirects_batch_info($count, $delete = FALSE) {
if ($delete) {
$operations[] = array('redirect_generate_batch_delete', array());
}
$operations[] = array('redirect_generate_batch_generate', array($count));
return array(
'operations' => $operations,
'finished' => 'redirect_generate_batch_finished',
'file' => drupal_get_path('module', 'redirect') . '/redirect.generate.inc',
);
}
function redirect_generate_batch_delete(array &$context) {
if (empty($context['sandbox'])) {
$context['sandbox'] = array();
$context['sandbox']['progress'] = 0;
$context['sandbox']['current_rid'] = 0;
$context['sandbox']['max'] = db_query('SELECT COUNT(DISTINCT rid) FROM {redirect}')->fetchField();
}
$limit = 20;
$rids = db_query_range("SELECT rid FROM {redirect} WHERE rid > :rid ORDER BY rid", 0, $limit, array(':rid' => $context['sandbox']['current_rid']))->fetchCol();
foreach (redirect_repository()->loadMultiple($rids) as $redirect) {
$redirect->delete();
}
// Update our progress information.
$context['sandbox']['progress'] += count($rids);
$context['sandbox']['current_rid'] = end($rids);
$context['message'] = t('Deleted URL redirect @rid.', array('@rid' => end($rids)));
// Inform the batch engine that we are not finished,
// and provide an estimation of the completion level we reached.
if ($context['sandbox']['progress'] != $context['sandbox']['max']) {
$context['finished'] = ($context['sandbox']['progress'] >= $context['sandbox']['max']);
}
}
function redirect_generate_batch_generate($num, array &$context) {
if (empty($context['sandbox'])) {
$context['sandbox'] = array();
$context['sandbox']['progress'] = 0;
$context['sandbox']['max'] = $num;
$query = \Drupal::database()->select('node', 'n');
$query->addField('n', 'nid');
$query->condition('n.status', NODE_PUBLISHED);
$query->addTag('node_access');
$context['sandbox']['nids'] = $query->execute()->fetchAllKeyed(0, 0);
}
module_load_include('inc', 'devel_generate');
$limit = 20;
$types = array_keys(redirect_status_code_options());
$languages = \Drupal::moduleHandler()->moduleExists('locale') ? array_keys(\Drupal::languageManager()->getLanguages()) : array();
for ($i = 0; $i < min($limit, $context['sandbox']['max'] - $context['sandbox']['progress']); $i++) {
$rand = mt_rand(0, 100);
$redirect = Redirect::create();
$source = _redirect_generate_url();
$source_options = array();
$redirect_options = array();
if ($context['sandbox']['nids'] && $rand >= 40) {
$redirect_target = 'node/' . array_rand($context['sandbox']['nids']);
}
else {
$redirect_target = _redirect_generate_url(TRUE);
if ($rand <= 20) {
$redirect_options['query'] = _redirect_generate_querystring();
}
if ($rand <= 5) {
$redirect_options['fragment'] = DevelGenerateBase::generateWord(mt_rand(4, 8));
}
}
if ($rand <= 20) {
$redirect->setStatusCode($types[array_rand($types)]);
}
if ($languages && $rand <= 20) {
$redirect->setLanguage($languages[array_rand($languages)]);
}
$query = array();
if ($rand <= 30) {
$query = _redirect_generate_querystring();
}
$redirect->setSource($source, $query);
$redirect->setRedirect($redirect_target);
$redirect->save();
if (mt_rand(0, 1)) {
$query = \Drupal::database();
$query->update('redirect')
->fields(array(
'count' => mt_rand(1, 500),
'access' => mt_rand(REQUEST_TIME - 31536000, REQUEST_TIME),
))
->condition('rid', $redirect->id())
->execute();
}
$context['results'][] = $redirect->id();
}
// Update our progress information.
$context['sandbox']['progress'] += $limit;
//$context['message'] = t('Deleted URL redirect @rid.', array('@rid' => end($rids)));
// Inform the batch engine that we are not finished,
// and provide an estimation of the completion level we reached.
if ($context['sandbox']['progress'] != $context['sandbox']['max']) {
$context['finished'] = ($context['sandbox']['progress'] >= $context['sandbox']['max']);
}
}
function redirect_generate_batch_finished($success, $results, $operations) {
if ($success) {
drupal_set_message(\Drupal::translation()->formatPlural(count($results), 'One URL redirect created.', '@count URL redirects created.'));
}
else {
// An error occurred.
// $operations contains the operations that remained unprocessed.
$error_operation = reset($operations);
drupal_set_message(t('An error occurred while processing @operation with arguments : @args', array('@operation' => $error_operation[0], '@args' => print_r($error_operation[0], TRUE))));
}
}
function _redirect_generate_url($external = FALSE, $max_levels = 2) {
$url = array();
if ($external) {
$tlds = array('com', 'net', 'org');
$url[] = 'http://www.example.'. $tlds[array_rand($tlds)];
}
$max_levels = mt_rand($external ? 0 : 1, $max_levels);
for ($i = 1; $i <= $max_levels; $i++) {
$url[] = DevelGenerateBase::generateWord(mt_rand(6 / $i, 8));
}
return implode('/', $url);
}
function _redirect_generate_querystring() {
$query = array(DevelGenerateBase::generateWord(mt_rand(1, 3)) => DevelGenerateBase::generateWord(mt_rand(2, 4)));
return $query;
}
@@ -0,0 +1,15 @@
name: Redirect
type: module
description: Allows users to redirect from old URLs to new URLs.
# core: 8.x
configure: redirect.settings
dependencies:
- drupal:link
- drupal:views
# Information added by Drupal.org packaging script on 2018-03-12
version: '8.x-1.1'
core: '8.x'
project: 'redirect'
datestamp: 1520897884
@@ -0,0 +1,224 @@
<?php
/**
* @file
* Update hooks for the Redirect module.
*/
use Drupal\redirect\Entity\Redirect;
use Drupal\Core\Database\Database;
use Drupal\system\Entity\Action;
use Drupal\views\Entity\View;
use Symfony\Component\Yaml\Yaml;
use Drupal\Core\Config\InstallStorage;
use Drupal\Core\Config\FileStorage;
use Drupal\user\Entity\Role;
/**
* Rehash redirects to account for case insensitivity.
*/
function redirect_update_8100(&$sandbox) {
// Loop through 100 redirects at a time.
if (!isset($sandbox['progress'])) {
$sandbox['progress'] = 0;
$sandbox['current_rid'] = 0;
// Note, because MySQL can treat `foo = LOWER(foo)`, all records must be checked.
$sandbox['max'] = db_query('SELECT COUNT(1) FROM {redirect}')->fetchField();
}
$result = \Drupal::database()->select('redirect', 'r')
->fields('r', ['rid', 'redirect_source__path', 'redirect_source__query', 'language', 'hash'])
->condition('rid', $sandbox['current_rid'], '>')
->range(0, 100)
->orderBy('rid', 'ASC')
->execute();
foreach ($result as $row) {
$query = !empty($row->redirect_source__query) ? unserialize($row->redirect_source__query): [];
$new_hash = Redirect::generateHash($row->redirect_source__path, (array) $query, $row->language);
if ($row->hash != $new_hash) {
// Do a direct query to speed things up.
$query = \Drupal::database();
$query->update('redirect')
->fields(['hash' => $new_hash])
->condition('rid', $row->rid)
->execute();
}
$sandbox['progress']++;
$sandbox['current_rid'] = $row->rid;
}
// Reset caches.
$sandbox['#finished'] = empty($sandbox['max']) ? 1 : ($sandbox['progress'] / $sandbox['max']);
}
/**
* Update the {redirect} table.
*/
function redirect_update_8101() {
// Get the current schema, change the length.
$key_value_store_schema = \Drupal::keyValue('entity.storage_schema.sql');
$schema = $key_value_store_schema->get('redirect.field_schema_data.hash');
$schema['redirect']['fields']['hash']['length'] = 64;
// Set the max_length of the hash column to 64 characters.
Database::getConnection()
->schema()
->changeField('redirect', 'hash', 'hash', $schema['redirect']['fields']['hash']);
// Update the last installed field definition and field schema.
/** @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface $key_value_store */
\Drupal::entityManager()->clearCachedFieldDefinitions();
$key_value_store_definition = \Drupal::keyValue('entity.definitions.installed');
$storage_definitions = $key_value_store_definition->get('redirect.field_storage_definitions');
$storage_definitions['hash'] = $storage_definition = \Drupal::entityManager()
->getFieldStorageDefinitions('redirect')['hash'];
$key_value_store_definition->set('redirect.field_storage_definitions', $storage_definitions);
// Update the stored schema.
$key_value_store_schema->set('redirect.field_schema_data.hash', $schema);
}
/**
* Update settings based on existing settings and Globalredirect settings.
*/
function redirect_update_8102() {
// Load default configuration.
$redirect_settings = \Drupal::config('redirect.settings');
$globalredirect_settings = \Drupal::config('globalredirect.settings');
$config_factory = \Drupal::configFactory();
$redirect = $config_factory->getEditable('redirect.settings');
$nonclean_to_clean = $redirect_settings->get('global_clean');
$admin_path = $redirect_settings->get('global_admin_paths');
$frontpage_redirect = $redirect_settings->get('global_home');
$deslash = $redirect_settings->get('global_deslash');
$message = NULL;
// If Globalredirect configuration exists, use those settings.
if ((!$globalredirect_settings->isNew())) {
$access_check = $globalredirect_settings->get('access_check');
$normalize_aliases = $globalredirect_settings->get('normalize_aliases');
$content_location_header = $globalredirect_settings->get('content_location_header');
$term_path_handler = $globalredirect_settings->get('term_path_handler');
$deslash = $globalredirect_settings->get('deslash');
$frontpage_redirect = $globalredirect_settings->get('frontpage_redirect');
$nonclean_to_clean = $globalredirect_settings->get('nonclean_to_clean');
$redirect->set('access_check', $access_check);
$redirect->set('normalize_aliases', $normalize_aliases);
$redirect->set('content_location_header', $content_location_header);
$redirect->set('term_path_handler', $term_path_handler);
$message = 'The Globalredirect module functionality has been merged into redirect, it should be uninstalled now.';
}
else {
$redirect->set('access_check', FALSE);
$redirect->set('normalize_aliases', TRUE);
$redirect->set('content_location_header', FALSE);
$redirect->set('term_path_handler', TRUE);
}
// Update new redirect settings names.
$redirect->set('nonclean_to_clean', $nonclean_to_clean);
$redirect->set('admin_path', $admin_path);
$redirect->set('frontpage_redirect', $frontpage_redirect);
$redirect->set('deslash', $deslash);
// Remove old names of redirect settings.
$redirect->clear('global_clean');
$redirect->clear('global_admin_paths');
$redirect->clear('global_home');
$redirect->clear('global_deslash');
$redirect->save();
return $message;
}
/**
* Creates the new default redirect view.
*/
function redirect_update_8103() {
$message = NULL;
// Only create if the redirect view doesn't exist and views is enabled.
if (!View::load('redirect') && \Drupal::moduleHandler()->moduleExists('views')) {
$config_path = drupal_get_path('module', 'redirect') . '/config/install/views.view.redirect.yml';
$data = Yaml::parse($config_path);
\Drupal::configFactory()->getEditable('views.view.redirect')->setData($data)->save(TRUE);
$message = 'The new redirect view has been created.';
}
else {
$message = 'Not creating a redirect view since it already exists.';
}
return $message;
}
/**
* Save the bulk delete action to config.
*/
function redirect_update_8104() {
if (!Action::load('redirect_delete_action')) {
$entity_type_manager = \Drupal::entityTypeManager();
$module_handler = \Drupal::moduleHandler();
// Save the bulk delete action to config.
$config_install_path = $module_handler->getModule('redirect')->getPath() . '/' . InstallStorage::CONFIG_INSTALL_DIRECTORY;
$storage = new FileStorage($config_install_path);
$entity_type_manager
->getStorage('action')
->create($storage->read('system.action.redirect_delete_action'))
->trustData()
->save();
}
}
/**
* Ensure to use the redirect_404 submodule.
*/
function redirect_update_8105() {
\Drupal::service('module_installer')->install(['redirect_404']);
}
/**
* Removes unnecessary settings from storage.
*/
function redirect_update_8106() {
$config = \Drupal::configFactory()->getEditable('redirect.settings');
$config->set('route_normalizer_enabled', $config->get('normalize_aliases'));
$config->clear('term_path_handler');
$config->clear('normalize_aliases');
$config->clear('deslash');
$config->clear('frontpage_redirect');
$config->clear('nonclean_to_clean');
$config->save();
}
/**
* Update permissions.
*
* When splitting the permission for settings from the permission to view
* the list of redirects, maintain the current permissions for sites which
* already have this module installed.
*/
function redirect_update_8107() {
if ($roles = Role::loadMultiple()) {
foreach ($roles as $role) {
if ($role->hasPermission('administer redirects')) {
$role->grantPermission('administer redirect settings');
$role->save();
}
}
}
}
/**
* Removes unnecessary settings.
*/
function redirect_update_8108() {
$config = \Drupal::configFactory()->getEditable('redirect.settings');
$config->clear('canonical');
$config->clear('content_location_header');
$config->save();
}
@@ -0,0 +1,5 @@
drupal.redirect.admin:
version: VERSION
css:
component:
css/redirect.admin.css: {}
@@ -0,0 +1,5 @@
redirect.add:
route_name: redirect.add
title: 'Add redirect'
appears_on:
- 'redirect.list'
@@ -0,0 +1,26 @@
redirect.list:
title: 'URL redirects'
parent: system.admin_config_search
description: 'Redirect users from one URL to another.'
route_name: redirect.list
menu_name: admin
redirect.add:
title: 'Add redirect'
parent: redirect.list
route_name: redirect.add
menu_name: admin
redirect.settings:
title: 'Settings'
parent: redirect.list
route_name: redirect.settings
description: 'Configure behavior for URL redirects.'
menu_name: admin
#redirect.devel_generate:
# title: 'Generate redirects'
# parent: dblog.page_not_found
# route_name: redirect.devel_generate
# description: 'Generate a given number of redirects. Optionally delete current redirects.'
# menu_name: admin
@@ -0,0 +1,10 @@
redirect.list:
route_name: redirect.list
base_route: redirect.list
title: URL Redirects
redirect.settings:
route_name: redirect.settings
base_route: redirect.list
title: Settings
weight: 50
@@ -0,0 +1,497 @@
<?php
/**
* @file
* The redirect module.
*/
/**
* @defgroup redirect_api Redirection API
* @{
* Functions related to URL redirects.
*
* @} End of "defgroup redirect_api".
*/
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Field\FieldItemList;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\Language;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Url;
use Drupal\Core\Site\Settings;
use Drupal\redirect\Entity\Redirect;
/**
* Implements hook_hook_info().
*/
function redirect_hook_info() {
$hooks = array(
'redirect_load',
'redirect_load_by_source_alter',
'redirect_access',
'redirect_prepare',
'redirect_validate',
'redirect_presave',
'redirect_insert',
'redirect_update',
'redirect_delete',
'redirect_alter',
);
return array_fill_keys($hooks, array('group' => 'redirect'));
}
/**
* Implements hook_help().
*/
function redirect_help($route_name, RouteMatchInterface $route_match) {
$output = '';
switch ($route_name) {
case 'help.page.redirect':
$output = '<h3>' . t('About') . '</h3>';
$output .= '<p>' . t('The Redirect module allows users to redirect from old URLs to new URLs. For more information, see the <a href=":online">online documentation for Redirect</a>.', [':online' => 'https://www.drupal.org/documentation/modules/path-redirect']) . '</p>';
$output .= '<dl>';
$output .= '<h3>' . t('Uses') . '</h3>';
$output .= '<dd>' . t('Redirect is accessed from three tabs that help you manage <a href=":list">URL Redirects</a>.', [':list' => Url::fromRoute('redirect.list')->toString()]) . '</dd>';
$output .= '<dt>' . t('Manage URL Redirects') . '</dt>';
$output .= '<dd>' . t('The <a href=":redirect">"URL Redirects"</a> page is used to setup and manage URL Redirects. New redirects are created here using the <a href=":add_form">Add redirect</a> button which presents a form to simplify the creation of redirects . The URL redirects page provides a list of all redirects on the site and allows you to edit them.', [':redirect' => Url::fromRoute('redirect.list')->toString(), ':add_form' => Url::fromRoute('redirect.add')->toString()]) . '</dd>';
if (\Drupal::moduleHandler()->moduleExists('redirect_404')) {
$output .= '<dt>' . t('Fix 404 pages') . '</dt>';
$output .= '<dd>' . t('<a href=":fix_404">"Fix 404 pages"</a> lists all paths that have resulted in 404 errors and do not yet have any redirects assigned to them. This 404 (or Not Found) error message is an HTTP standard response code indicating that the client was able to communicate with a given server, but the server could not find what was requested.', [':fix_404' => Url::fromRoute('redirect_404.fix_404')->toString()]) . '</dd>';
}
elseif (!\Drupal::moduleHandler()->moduleExists('redirect_404') && \Drupal::currentUser()->hasPermission('administer modules')) {
$output .= '<dt>' . t('Fix 404 pages') . '</dt>';
$output .= '<dd>' . t('404 (or Not Found) error message is an HTTP standard response code indicating that the client was able to communicate with a given server, but the server could not find what was requested. Please install the <a href=":extend">Redirect 404</a> submodule to be able to log all paths that have resulted in 404 errors.', [':extend' => Url::fromRoute('system.modules_list')->toString()]) . '</dd>';
}
$output .= '<dt>' . t('Configure Global Redirects') . '</dt>';
$output .= '<dd>' . t('The <a href=":settings">"Settings"</a> page presents you with a number of means to adjust redirect settings.', [':settings' => Url::fromRoute('redirect.settings')->toString()]) . '</dd>';
$output .= '</dl>';
return $output;
break;
}
}
/**
* Implements hook_entity_delete().
*
* Will delete redirects based on the entity URL.
*/
function redirect_entity_delete(EntityInterface $entity) {
if ($entity->getEntityType()->hasLinkTemplate('canonical')) {
redirect_delete_by_path('internal:/' . $entity->toUrl('canonical')->getInternalPath());
redirect_delete_by_path('entity:' . $entity->getEntityTypeId() . '/' . $entity->id());
}
}
/**
* Implements hook_path_update().
*
* Will create redirect from the old path alias to the new one.
*/
function redirect_path_update(array $path) {
if (!\Drupal::config('redirect.settings')->get('auto_redirect')) {
return;
}
$original_path = $path['original'];
// Delete all redirects having the same source as this alias.
redirect_delete_by_path($path['alias'], $path['langcode'], FALSE);
if ($original_path['alias'] != $path['alias']) {
if (!redirect_repository()->findMatchingRedirect($original_path['alias'], array(), $original_path['langcode'])) {
$redirect = Redirect::create();
$redirect->setSource($original_path['alias']);
$redirect->setRedirect($path['source']);
$redirect->setLanguage($original_path['langcode']);
$redirect->setStatusCode(\Drupal::config('redirect.settings')->get('default_status_code'));
$redirect->save();
}
}
}
/**
* Implements hook_path_insert().
*/
function redirect_path_insert(array $path) {
// Delete all redirects having the same source as this alias.
redirect_delete_by_path($path['alias'], $path['langcode'], FALSE);
}
/**
* Implements hook_path_delete().
*/
function redirect_path_delete($path) {
if (!\Drupal::config('redirect.settings')->get('auto_redirect')) {
return;
}
elseif (isset($path['redirect']) && !$path['redirect']) {
return;
}
elseif (empty($path)) {
// @todo Remove this condition and allow $path to use an array type hint
// when http://drupal.org/node/1025904 is fixed.
return;
}
// Redirect from a deleted alias to the system path.
//if (!redirect_load_by_source($path['alias'], $path['language'])) {
// $redirect = new stdClass();
// redirect_create($redirect);
// $redirect->source = $path['alias'];
// $redirect->redirect = $path['source'];
// $redirect->language = $path['language'];
// redirect_save($redirect);
//}
}
/**
* Implements hook_page_build().
*
* Adds an action on 404 pages to create a redirect.
*
* @todo hook_page_build() can no longer be used for this. Find a different way.
*/
function redirect_page_build(&$page) {
if (redirect_is_current_page_404() && \Drupal::currentUser()->hasPermission('administer redirects')) {
if (!isset($page['content']['system_main']['actions'])) {
$page['content']['system_main']['actions'] = array(
'#theme' => 'links',
'#links' => array(),
'#attributes' => array('class' => array('action-links')),
'#weight' => -100,
);
}
// We cannot simply use current_path() because if a 404 path is set, then
// that value overrides whatever is in $_GET['q']. The
// drupal_deliver_html_page() function thankfully puts the original current
// path into $_GET['destination'].
$destination = drupal_get_destination();
$page['content']['system_main']['actions']['#links']['add_redirect'] = array(
'title' => t('Add URL redirect from this page to another location'),
'href' => 'admin/config/search/redirect/add',
'query' => array('source' => $destination['destination']) + drupal_get_destination(),
);
}
}
/**
* Gets the redirect repository service.
*
* @return \Drupal\redirect\RedirectRepository
* The repository service.
*/
function redirect_repository() {
return \Drupal::service('redirect.repository');
}
/**
* Delete any redirects associated with a path or any of its sub-paths.
*
* Given a source like 'node/1' this function will delete any redirects that
* have that specific source or any sources that match 'node/1/%'.
*
* @param string $path
* An string with an internal Drupal path.
* @param string $langcode
* (optional) If specified, limits deletion to redirects for the given
* language. Defaults to all languages.
* @param bool $match_subpaths_and_redirect
* (optional) Whether redirects with a destination to the given path and
* sub-paths should also be deleted.
*
* @ingroup redirect_api
*/
function redirect_delete_by_path($path, $langcode = NULL, $match_subpaths_and_redirect = TRUE) {
$path = ltrim($path, '/');
$database = \Drupal::database();
$query = $database->select('redirect');
$query->addField('redirect', 'rid');
$query_or = db_or();
$query_or->condition('redirect_source__path', $database->escapeLike($path), 'LIKE');
if ($match_subpaths_and_redirect) {
$query_or->condition('redirect_source__path', $database->escapeLike($path . '/') . '%', 'LIKE');
$query_or->condition('redirect_redirect__uri', $database->escapeLike($path), 'LIKE');
$query_or->condition('redirect_redirect__uri', $database->escapeLike($path . '/') . '%', 'LIKE');
}
if ($langcode) {
$query->condition('language', $langcode);
}
$query->condition($query_or);
$rids = $query->execute()->fetchCol();
if ($rids) {
foreach (redirect_repository()->loadMultiple($rids) as $redirect) {
$redirect->delete();
}
}
}
/**
* Sort an array recusively.
*
* @param $array
* The array to sort, by reference.
* @param $callback
* The sorting callback to use (e.g. 'sort', 'ksort', 'asort').
*
* @return
* TRUE on success or FALSE on failure.
*/
function redirect_sort_recursive(&$array, $callback = 'sort') {
$result = $callback($array);
foreach ($array as $key => $value) {
if (is_array($value)) {
$result &= redirect_sort_recursive($array[$key], $callback);
}
}
return $result;
}
/**
* Build the URL of a redirect for display purposes only.
*/
function redirect_url($path, array $options = array(), $clean_url = NULL) {
// @todo - deal with removal of clean_url config. See
// https://drupal.org/node/1659580
if (!isset($clean_url)) {
//$clean_url = variable_get('clean_url', 0);
}
if ($path == '') {
$path = '<front>';
}
if (!isset($options['alter']) || !empty($options['alter'])) {
\Drupal::moduleHandler()->alter('redirect_url', $path, $options);
}
// The base_url might be rewritten from the language rewrite in domain mode.
if (!isset($options['base_url'])) {
// @todo - is this correct? See https://drupal.org/node/1798832.
if (isset($options['https']) && Settings::get('mixed_mode_sessions', FALSE)) {
if ($options['https'] === TRUE) {
$options['base_url'] = $GLOBALS['base_secure_url'];
$options['absolute'] = TRUE;
}
elseif ($options['https'] === FALSE) {
$options['base_url'] = $GLOBALS['base_insecure_url'];
$options['absolute'] = TRUE;
}
}
else {
$options['base_url'] = $GLOBALS['base_url'];
}
}
if (empty($options['absolute']) || url_is_external($path)) {
$url = $path;
}
else {
$url = $options['base_url'] . base_path() . $path;
}
if (isset($options['query'])) {
$url .= $clean_url ? '?' : '&';
$url .= UrlHelper::buildQuery($options['query']);
}
if (isset($options['fragment'])) {
$url .= '#' . $options['fragment'];
}
return $url;
}
function redirect_status_code_options($code = NULL) {
$codes = array(
300 => t('300 Multiple Choices'),
301 => t('301 Moved Permanently'),
302 => t('302 Found'),
303 => t('303 See Other'),
304 => t('304 Not Modified'),
305 => t('305 Use Proxy'),
307 => t('307 Temporary Redirect'),
);
return isset($codes[$code]) ? $codes[$code] : $codes;
}
/**
* Returns if the current page request is a page not found (404 status error).
*
* Why the fuck do we have to do this? Why is there not an easier way???
*
* @return
* TRUE if the current page is a 404, or FALSE otherwise.
*/
function redirect_is_current_page_404() {
return drupal_get_http_header('Status') == '404 Not Found';
}
/**
* uasort callback; Compare redirects based on language neutrality and rids.
*/
function _redirect_uasort($a, $b) {
$a_weight = isset($a->weight) ? $a->weight : 0;
$b_weight = isset($b->weight) ? $b->weight : 0;
if ($a_weight != $b_weight) {
// First sort by weight (case sensitivity).
return $a_weight > $b_weight;
}
elseif ($a->language != $b->language) {
// Then sort by language specific over language neutral.
return $a->language == Language::LANGCODE_NOT_SPECIFIED;
}
elseif (!empty($a->source_options['query']) != !empty($b->source_options['query'])) {
// Then sort by redirects that do not have query strings over ones that do.
return empty($a->source_options['query']);
}
else {
// Lastly sort by the highest redirect ID.
return $a->rid < $b->rid;
}
}
/**
* Implements hook_form_FORM_ID_alter() on behalf of locale.module.
*/
function locale_form_redirect_edit_form_alter(array &$form, FormStateInterface $form_state) {
$form['language'] = array(
'#type' => 'select',
'#title' => t('Language'),
'#options' => array(Language::LANGCODE_NOT_SPECIFIED => t('All languages')) + \Drupal::languageManager()->getLanguages(),
'#default_value' => $form['language']['#value'],
'#description' => t('A redirect set for a specific language will always be used when requesting this page in that language, and takes precedence over redirects set for <em>All languages</em>.'),
);
}
/**
* Fetch an array of redirect bulk operations.
*
* @see hook_redirect_operations()
* @see hook_redirect_operations_alter()
*/
function redirect_get_redirect_operations() {
$operations = &drupal_static(__FUNCTION__);
if (!isset($operations)) {
$operations = \Drupal::moduleHandler()->invokeAll('redirect_operations');
\Drupal::moduleHandler()->alter('redirect_operations', $operations);
}
return $operations;
}
/**
* Implements hook_redirect_operations().
*/
function redirect_redirect_operations() {
$operations['delete'] = array(
'action' => t('Delete'),
'action_past' => t('Deleted'),
'callback' => 'redirect_delete_multiple',
'confirm' => TRUE,
);
return $operations;
}
/**
* Ajax callback for the redirect link widget.
*/
function redirect_source_link_get_status_messages(array $form, FormStateInterface $form_state) {
return $form['redirect_source']['widget'][0]['status_box'];
}
/**
* Implements hook_entity_extra_field_info().
*/
function redirect_entity_extra_field_info() {
$extra = [];
if (\Drupal::service('module_handler')->moduleExists('node')) {
$node_types = \Drupal::entityTypeManager()
->getStorage('node_type')
->loadMultiple();
foreach ($node_types as $node_type) {
$extra['node'][$node_type->id()]['form']['url_redirects'] = [
'label' => t('URL redirects'),
'description' => t('Redirect module form elements'),
'weight' => 50,
];
}
}
return $extra;
}
/**
* Implements hook_form_node_form_alter().
*/
function redirect_form_node_form_alter(&$form, FormStateInterface $form_state, $form_id) {
/** @var \Drupal\node\NodeInterface $node */
$node = $form_state->getFormObject()->getEntity();
if (!$node->isNew() && \Drupal::currentUser()->hasPermission('administer redirects')) {
$nid = $node->id();
// Find redirects to this node.
$redirects = \Drupal::service('redirect.repository')
->findByDestinationUri(["internal:/node/$nid", "entity:node/$nid"]);
// Assemble the rows for the table.
$rows = [];
/** @var \Drupal\Core\Entity\EntityListBuilder $list_builder */
$list_builder = \Drupal::service('entity.manager')->getListBuilder('redirect');
/** @var \Drupal\redirect\Entity\Redirect[] $redirects */
foreach ($redirects as $redirect) {
$row = [];
$path = $redirect->getSourcePathWithQuery();
$row['path'] = [
'class' => ['redirect-table__path'],
'data' => ['#plain_text' => $path],
'title' => $path,
];
$row['operations'] = [
'data' => [
'#type' => 'operations',
'#links' => $list_builder->getOperations($redirect),
],
];
$rows[] = $row;
}
// Add the list to the vertical tabs section of the form.
$header = [
['class' => ['redirect-table__path'], 'data' => t('From')],
['class' => ['redirect-table__operations'], 'data' => t('Operations')],
];
$form['url_redirects'] = [
'#type' => 'details',
'#title' => t('URL redirects'),
'#group' => 'advanced',
'#open' => FALSE,
'table' => [
'#type' => 'table',
'#header' => $header,
'#rows' => $rows,
'#empty' => t('No URL redirects available.'),
'#attributes' => ['class' => ['redirect-table']],
],
'#attached' => [
'library' => [
'redirect/drupal.redirect.admin',
],
],
];
if (!empty($rows)) {
$form['url_redirects']['warning'] = [
'#markup' => t('Note: links open in the current window.'),
'#prefix' => '<p>',
'#suffix' => '</p>',
];
}
}
}
@@ -0,0 +1,4 @@
administer redirects:
title: 'Administer individual URL redirections'
administer redirect settings:
title: 'Administer global URL redirection settings'

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