Compare commits

...
20 Commits
Author SHA1 Message Date
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
243 changed files with 23201 additions and 4277 deletions
@@ -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'
@@ -0,0 +1,58 @@
redirect.list:
path: '/admin/config/search/redirect'
defaults:
_entity_list: 'redirect'
_title: 'URL redirects'
requirements:
_permission: 'administer redirects'
entity.redirect.canonical:
path: '/admin/config/search/redirect/edit/{redirect}'
defaults:
_entity_form: 'redirect.edit'
_title: 'Edit URL redirect'
requirements:
_entity_access: 'redirect.update'
redirect.add:
path: '/admin/config/search/redirect/add'
defaults:
_entity_form: 'redirect.default'
_title: 'Add URL redirect'
requirements:
_entity_create_access: 'redirect'
entity.redirect.edit_form:
path: '/admin/config/search/redirect/edit/{redirect}'
defaults:
_entity_form: 'redirect.edit'
_title: 'Edit URL redirect'
requirements:
_entity_access: 'redirect.update'
entity.redirect.delete_form:
path: '/admin/config/search/redirect/delete/{redirect}'
defaults:
_entity_form: 'redirect.delete'
_title: 'Delete URL redirect'
requirements:
_entity_access: 'redirect.delete'
entity.redirect.multiple_delete_confirm:
path: '/admin/config/search/redirect/delete'
defaults:
_form: '\Drupal\redirect\Form\RedirectDeleteMultipleForm'
requirements:
_permission: 'administer redirects'
redirect.settings:
path: '/admin/config/search/redirect/settings'
defaults:
_form: '\Drupal\redirect\Form\RedirectSettingsForm'
_title: 'Settings'
requirements:
_permission: 'administer redirect settings'
#redirect.devel_generate:
# requirements:
# _module_dependencies: 'devel'
@@ -0,0 +1,30 @@
parameters:
route_normalizer_enabled: true
services:
redirect.repository:
class: Drupal\redirect\RedirectRepository
arguments: ['@entity.manager', '@database', '@config.factory']
tags:
- { name: backend_overridable }
redirect.checker:
class: Drupal\redirect\RedirectChecker
arguments: ['@config.factory', '@state', '@access_manager', '@current_user', '@router.route_provider']
redirect.request_subscriber:
class: Drupal\redirect\EventSubscriber\RedirectRequestSubscriber
arguments: ['@redirect.repository', '@language_manager', '@config.factory', '@path.alias_manager', '@module_handler', '@entity.manager', '@redirect.checker', '@router.request_context', '@path_processor_manager']
tags:
- { name: event_subscriber }
redirect.settings_cache_tag:
class: Drupal\redirect\EventSubscriber\RedirectSettingsCacheTag
arguments: ['@cache_tags.invalidator']
tags:
- { name: event_subscriber }
redirect.route_normalizer_request_subscriber:
class: Drupal\redirect\EventSubscriber\RouteNormalizerRequestSubscriber
arguments: ['@url_generator', '@path.matcher', '@config.factory', '@redirect.checker']
tags:
- { name: event_subscriber }
redirect.route_subscriber:
class: Drupal\redirect\Routing\RouteSubscriber
tags:
- { name: event_subscriber }
@@ -0,0 +1,344 @@
<?php
namespace Drupal\redirect\Entity;
use Drupal\Component\Utility\Crypt;
use Drupal\Component\Utility\Unicode;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Entity\ContentEntityBase;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\link\LinkItemInterface;
/**
* The redirect entity class.
*
* @ContentEntityType(
* id = "redirect",
* label = @Translation("Redirect"),
* bundle_label = @Translation("Redirect type"),
* handlers = {
* "list_builder" = "Drupal\Core\Entity\EntityListBuilder",
* "form" = {
* "default" = "Drupal\redirect\Form\RedirectForm",
* "delete" = "Drupal\redirect\Form\RedirectDeleteForm",
* "edit" = "Drupal\redirect\Form\RedirectForm"
* },
* "views_data" = "Drupal\views\EntityViewsData",
* "storage_schema" = "\Drupal\redirect\RedirectStorageSchema"
* },
* base_table = "redirect",
* translatable = FALSE,
* admin_permission = "administer redirects",
* entity_keys = {
* "id" = "rid",
* "label" = "redirect_source",
* "uuid" = "uuid",
* "bundle" = "type",
* "langcode" = "language",
* },
* links = {
* "canonical" = "/admin/config/search/redirect/edit/{redirect}",
* "delete-form" = "/admin/config/search/redirect/delete/{redirect}",
* "edit-form" = "/admin/config/search/redirect/edit/{redirect}",
* }
* )
*/
class Redirect extends ContentEntityBase {
/**
* Generates a unique hash for identification purposes.
*
* @param string $source_path
* Source path of the redirect.
* @param array $source_query
* Source query as an array.
* @param string $language
* Redirect language.
*
* @return string
* Base 64 hash.
*/
public static function generateHash($source_path, array $source_query, $language) {
$hash = array(
'source' => Unicode::strtolower($source_path),
'language' => $language,
);
if (!empty($source_query)) {
$hash['source_query'] = $source_query;
}
redirect_sort_recursive($hash, 'ksort');
return Crypt::hashBase64(serialize($hash));
}
/**
* {@inheritdoc}
*/
public static function preCreate(EntityStorageInterface $storage_controller, array &$values) {
$values += array(
'type' => 'redirect',
);
}
/**
* {@inheritdoc}
*/
public function preSave(EntityStorageInterface $storage_controller) {
$this->set('hash', Redirect::generateHash($this->redirect_source->path, (array) $this->redirect_source->query, $this->language()->getId()));
}
/**
* Sets the redirect language.
*
* @param string $language
* Language code.
*/
public function setLanguage($language) {
$this->set('language', $language);
}
/**
* Sets the redirect status code.
*
* @param int $status_code
* The redirect status code.
*/
public function setStatusCode($status_code) {
$this->set('status_code', $status_code);
}
/**
* Gets the redirect status code.
*
* @return int
* The redirect status code.
*/
public function getStatusCode() {
return $this->get('status_code')->value;
}
/**
* Sets the redirect created datetime.
*
* @param int $datetime
* The redirect created datetime.
*/
public function setCreated($datetime) {
$this->set('created', $datetime);
}
/**
* Gets the redirect created datetime.
*
* @return int
* The redirect created datetime.
*/
public function getCreated() {
return $this->get('created')->value;
}
/**
* Sets the source URL data.
*
* @param string $path
* The base url of the source.
* @param array $query
* Query arguments.
*/
public function setSource($path, array $query = array()) {
$this->redirect_source->set(0, ['path' => ltrim($path, '/'), 'query' => $query]);
}
/**
* Gets the source URL data.
*
* @return array
*/
public function getSource() {
return $this->get('redirect_source')->get(0)->getValue();
}
/**
* Gets the source base URL.
*
* @return string
*/
public function getSourceUrl() {
return $this->get('redirect_source')->get(0)->getUrl()->toString();
}
/**
* Gets the source URL path with its query.
*
* @return string
* The source URL path, eventually with its query.
*/
public function getSourcePathWithQuery() {
$path = '/' . $this->get('redirect_source')->path;
if ($this->get('redirect_source')->query) {
$path .= '?' . UrlHelper::buildQuery($this->get('redirect_source')->query);
}
return $path;
}
/**
* Gets the redirect URL data.
*
* @return array
* The redirect URL data.
*/
public function getRedirect() {
return $this->get('redirect_redirect')->get(0)->getValue();
}
/**
* Sets the redirect destination URL data.
*
* @param string $url
* The base url of the redirect destination.
* @param array $query
* Query arguments.
* @param array $options
* The source url options.
*/
public function setRedirect($url, array $query = array(), array $options = array()) {
$uri = $url . ($query ? '?' . UrlHelper::buildQuery($query) : '');
$this->redirect_redirect->set(0, ['uri' => 'internal:/' . ltrim($uri, '/'), 'options' => $options]);
}
/**
* Gets the redirect URL.
*
* @return \Drupal\Core\Url
* The redirect URL.
*/
public function getRedirectUrl() {
return $this->get('redirect_redirect')->get(0)->getUrl();
}
/**
* Gets the redirect URL options.
*
* @return array
* The redirect URL options.
*/
public function getRedirectOptions() {
return $this->get('redirect_redirect')->options;
}
/**
* Gets a specific redirect URL option.
*
* @param string $key
* Option key.
* @param mixed $default
* Default value used in case option does not exist.
*
* @return mixed
* The option value.
*/
public function getRedirectOption($key, $default = NULL) {
$options = $this->getRedirectOptions();
return isset($options[$key]) ? $options[$key] : $default;
}
/**
* Gets the current redirect entity hash.
*
* @return string
* The hash.
*/
public function getHash() {
return $this->get('hash')->value;
}
/**
* {@inheritdoc}
*/
public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
$fields['rid'] = BaseFieldDefinition::create('integer')
->setLabel(t('Redirect ID'))
->setDescription(t('The redirect ID.'))
->setReadOnly(TRUE);
$fields['uuid'] = BaseFieldDefinition::create('uuid')
->setLabel(t('UUID'))
->setDescription(t('The record UUID.'))
->setReadOnly(TRUE);
$fields['hash'] = BaseFieldDefinition::create('string')
->setLabel(t('Hash'))
->setSetting('max_length', 64)
->setDescription(t('The redirect hash.'));
$fields['type'] = BaseFieldDefinition::create('string')
->setLabel(t('Type'))
->setDescription(t('The redirect type.'));
$fields['uid'] = BaseFieldDefinition::create('entity_reference')
->setLabel(t('User ID'))
->setDescription(t('The user ID of the node author.'))
->setDefaultValueCallback('\Drupal\redirect\Entity\Redirect::getCurrentUserId')
->setSettings(array(
'target_type' => 'user',
));
$fields['redirect_source'] = BaseFieldDefinition::create('redirect_source')
->setLabel(t('From'))
->setDescription(t("Enter an internal Drupal path or path alias to redirect (e.g. %example1 or %example2). Fragment anchors (e.g. %anchor) are <strong>not</strong> allowed.", array('%example1' => 'node/123', '%example2' => 'taxonomy/term/123', '%anchor' => '#anchor')))
->setRequired(TRUE)
->setTranslatable(FALSE)
->setDisplayOptions('form', array(
'type' => 'redirect_link',
'weight' => -5,
))
->setDisplayConfigurable('form', TRUE);
$fields['redirect_redirect'] = BaseFieldDefinition::create('link')
->setLabel(t('To'))
->setRequired(TRUE)
->setTranslatable(FALSE)
->setSettings(array(
'link_type' => LinkItemInterface::LINK_GENERIC,
'title' => DRUPAL_DISABLED
))
->setDisplayOptions('form', array(
'type' => 'link',
'weight' => -4,
))
->setDisplayConfigurable('form', TRUE);
$fields['language'] = BaseFieldDefinition::create('language')
->setLabel(t('Language'))
->setDescription(t('The redirect language.'))
->setDisplayOptions('form', array(
'type' => 'language_select',
'weight' => 2,
));
$fields['status_code'] = BaseFieldDefinition::create('integer')
->setLabel(t('Status code'))
->setDescription(t('The redirect status code.'))
->setDefaultValue(0);
$fields['created'] = BaseFieldDefinition::create('created')
->setLabel(t('Created'))
->setDescription(t('The date when the redirect was created.'));
return $fields;
}
/**
* Default value callback for 'uid' base field definition.
*
* @see ::baseFieldDefinitions()
*
* @return array
* An array of default values.
*/
public static function getCurrentUserId() {
return array(\Drupal::currentUser()->id());
}
}
@@ -0,0 +1,201 @@
<?php
namespace Drupal\redirect\EventSubscriber;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Logger\RfcLogLevel;
use Drupal\Core\Path\AliasManager;
use Drupal\Core\PathProcessor\InboundPathProcessorInterface;
use Drupal\Core\Routing\TrustedRedirectResponse;
use Drupal\Core\Url;
use Drupal\redirect\Exception\RedirectLoopException;
use Drupal\redirect\RedirectChecker;
use Drupal\redirect\RedirectRepository;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Routing\RequestContext;
/**
* Redirect subscriber for controller requests.
*/
class RedirectRequestSubscriber implements EventSubscriberInterface {
/** @var \Drupal\redirect\RedirectRepository */
protected $redirectRepository;
/**
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected $languageManager;
/**
* @var \Drupal\Core\Config\Config
*/
protected $config;
/**
* @var \Drupal\Core\Path\AliasManager
*/
protected $aliasManager;
/**
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* @var \Drupal\redirect\RedirectChecker
*/
protected $checker;
/**
* @var \Symfony\Component\Routing\RequestContext
*/
protected $context;
/**
* A path processor manager for resolving the system path.
*
* @var \Drupal\Core\PathProcessor\InboundPathProcessorInterface
*/
protected $pathProcessor;
/**
* Constructs a \Drupal\redirect\EventSubscriber\RedirectRequestSubscriber object.
*
* @param \Drupal\redirect\RedirectRepository $redirect_repository
* The redirect entity repository.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager service.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config
* The config.
* @param \Drupal\Core\Path\AliasManager $alias_manager
* The alias manager service.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler service.
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager service.
* @param \Drupal\redirect\RedirectChecker $checker
* The redirect checker service.
* @param \Symfony\Component\Routing\RequestContext
* Request context.
*/
public function __construct(RedirectRepository $redirect_repository, LanguageManagerInterface $language_manager, ConfigFactoryInterface $config, AliasManager $alias_manager, ModuleHandlerInterface $module_handler, EntityManagerInterface $entity_manager, RedirectChecker $checker, RequestContext $context, InboundPathProcessorInterface $path_processor) {
$this->redirectRepository = $redirect_repository;
$this->languageManager = $language_manager;
$this->config = $config->get('redirect.settings');
$this->aliasManager = $alias_manager;
$this->moduleHandler = $module_handler;
$this->entityManager = $entity_manager;
$this->checker = $checker;
$this->context = $context;
$this->pathProcessor = $path_processor;
}
/**
* Handles the redirect if any found.
*
* @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event
* The event to process.
*/
public function onKernelRequestCheckRedirect(GetResponseEvent $event) {
// Get a clone of the request. During inbound processing the request
// can be altered. Allowing this here can lead to unexpected behavior.
// For example the path_processor.files inbound processor provided by
// the system module alters both the path and the request; only the
// changes to the request will be propagated, while the change to the
// path will be lost.
$request = clone $event->getRequest();
if (!$this->checker->canRedirect($request)) {
return;
}
// Get URL info and process it to be used for hash generation.
parse_str($request->getQueryString(), $request_query);
// Do the inbound processing so that for example language prefixes are
// removed.
$path = $this->pathProcessor->processInbound($request->getPathInfo(), $request);
$path = trim($path, '/');
$this->context->fromRequest($request);
try {
$redirect = $this->redirectRepository->findMatchingRedirect($path, $request_query, $this->languageManager->getCurrentLanguage()->getId());
}
catch (RedirectLoopException $e) {
\Drupal::logger('redirect')->warning($e->getMessage());
$response = new Response();
$response->setStatusCode(503);
$response->setContent('Service unavailable');
$event->setResponse($response);
return;
}
if (!empty($redirect)) {
// Handle internal path.
$url = $redirect->getRedirectUrl();
if ($this->config->get('passthrough_querystring')) {
$url->setOption('query', (array) $url->getOption('query') + $request_query);
}
$headers = [
'X-Redirect-ID' => $redirect->id(),
];
$response = new TrustedRedirectResponse($url->setAbsolute()->toString(), $redirect->getStatusCode(), $headers);
$response->addCacheableDependency($redirect);
$event->setResponse($response);
}
}
/**
* 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();
$this->context->fromRequest($request);
parse_str($request->getQueryString(), $query);
$url->setOption('query', $query);
$url->setAbsolute(TRUE);
// We can only check access for routed URLs.
if (!$url->isRouted() || $this->checker->canRedirect($request, $url->getRouteName())) {
// Add the 'rendered' cache tag, so that we can invalidate all responses
// when settings are changed.
$response = new TrustedRedirectResponse($url->toString(), 301);
$response->addCacheableDependency(CacheableMetadata::createFromRenderArray([])->addCacheTags(['rendered']));
$event->setResponse($response);
}
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
// This needs to run before RouterListener::onKernelRequest(), which has
// a priority of 32. Otherwise, that aborts the request if no matching
// route is found.
$events[KernelEvents::REQUEST][] = array('onKernelRequestCheckRedirect', 33);
return $events;
}
}
@@ -0,0 +1,54 @@
<?php
namespace Drupal\redirect\EventSubscriber;
use Drupal\Core\Cache\CacheTagsInvalidatorInterface;
use Drupal\Core\Config\ConfigCrudEvent;
use Drupal\Core\Config\ConfigEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* A subscriber invalidating the 'rendered' cache tag when saving redirect settings.
*/
class RedirectSettingsCacheTag implements EventSubscriberInterface {
/**
* The cache tags invalidator.
*
* @var \Drupal\Core\Cache\CacheTagsInvalidatorInterface
*/
protected $cacheTagsInvalidator;
/**
* Constructs a RedirectSettingsCacheTag object.
*
* @param \Drupal\Core\Cache\CacheTagsInvalidatorInterface $cache_tags_invalidator
* The cache tags invalidator.
*/
public function __construct(CacheTagsInvalidatorInterface $cache_tags_invalidator) {
$this->cacheTagsInvalidator = $cache_tags_invalidator;
}
/**
* Invalidate the 'rendered' cache tag whenever the settings are modified.
*
* @param \Drupal\Core\Config\ConfigCrudEvent $event
* The Event to process.
*/
public function onSave(ConfigCrudEvent $event) {
// Changing the Redirect settings means that any cached page might
// result in a different response, so we need to invalidate them all.
if ($event->getConfig()->getName() === 'redirect.settings') {
$this->cacheTagsInvalidator->invalidateTags(['rendered']);
}
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
$events[ConfigEvents::SAVE][] = ['onSave'];
return $events;
}
}
@@ -0,0 +1,149 @@
<?php
namespace Drupal\redirect\EventSubscriber;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Path\PathMatcherInterface;
use Drupal\Core\Routing\RequestHelper;
use Drupal\Core\Routing\TrustedRedirectResponse;
use Drupal\Core\Routing\UrlGeneratorInterface;
use Drupal\redirect\RedirectChecker;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Normalizes GET requests performing a redirect if required.
*
* The normalization can be disabled by setting the "_disable_route_normalizer"
* request parameter to TRUE. However, this should be done before
* onKernelRequestRedirect() method is executed.
*/
class RouteNormalizerRequestSubscriber implements EventSubscriberInterface {
/**
* Module specific configuration.
*
* @var \Drupal\Core\Config\Config
*/
protected $config;
/**
* The URL generator service.
*
* @var \Drupal\Core\Routing\UrlGeneratorInterface
*/
protected $urlGenerator;
/**
* The path matcher service.
*
* @var \Drupal\Core\Path\PathMatcherInterface
*/
protected $pathMatcher;
/**
* The redirect checker service.
*
* @var \Drupal\redirect\RedirectChecker
*/
protected $redirectChecker;
/**
* Constructs a RouteNormalizerRequestSubscriber object.
*
* @param \Drupal\Core\Routing\UrlGeneratorInterface $url_generator
* The URL generator service.
* @param \Drupal\Core\Path\PathMatcherInterface $path_matcher
* The path matcher service.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config
* The config.
* @param \Drupal\redirect\RedirectChecker $redirect_checker
* The redirect checker service.
* The value of the route_normalizer_enabled container parameter.
*/
public function __construct(UrlGeneratorInterface $url_generator, PathMatcherInterface $path_matcher, ConfigFactoryInterface $config, RedirectChecker $redirect_checker) {
$this->urlGenerator = $url_generator;
$this->pathMatcher = $path_matcher;
$this->redirectChecker = $redirect_checker;
$this->config = $config->get('redirect.settings');
}
/**
* Performs a redirect if the URL changes in routing.
*
* The redirect happens if a URL constructed from the current route is
* different from the requested one. Examples:
* - Language negotiation system detected a language to use, and that language
* has a path prefix: perform a redirect to the language prefixed URL.
* - A route that's set as the front page is requested: redirect to the front
* page.
* - Requested path has an alias: redirect to alias.
*
* @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event
* The Event to process.
*/
public function onKernelRequestRedirect(GetResponseEvent $event) {
if (!$this->config->get('route_normalizer_enabled') || !$event->isMasterRequest()) {
return;
}
$request = $event->getRequest();
if ($request->attributes->get('_disable_route_normalizer')) {
return;
}
if ($this->redirectChecker->canRedirect($request)) {
// The "<current>" placeholder can be used for all routes except the front
// page because it's not a real route.
$route_name = $this->pathMatcher->isFrontPage() ? '<front>' : '<current>';
// Don't pass in the query here using $request->query->all()
// since that can potentially modify the query parameters.
$options = ['absolute' => TRUE];
$redirect_uri = $this->urlGenerator->generateFromRoute($route_name, [], $options);
// Strip off query parameters added by the route such as a CSRF token.
if (strpos($redirect_uri, '?') !== FALSE) {
$redirect_uri = strtok($redirect_uri, '?');
}
// Append back the request query string from $_SERVER.
$query_string = $request->server->get('QUERY_STRING');
if ($query_string) {
$redirect_uri .= '?' . $query_string;
}
// Remove /index.php from redirect uri the hard way.
if (!RequestHelper::isCleanUrl($request)) {
// This needs to be fixed differently.
$redirect_uri = str_replace('/index.php', '', $redirect_uri);
}
$original_uri = $request->getSchemeAndHttpHost() . $request->getRequestUri();
$original_uri = urldecode($original_uri);
$redirect_uri = urldecode($redirect_uri);
if ($redirect_uri != $original_uri) {
$response = new TrustedRedirectResponse($redirect_uri, $this->config->get('default_status_code'));
$response->headers->set('X-Drupal-Route-Normalizer', 1);
$event->setResponse($response);
// Disable page cache for redirects as that results in unpredictable
// behavior, e.g. when a trailing ? without query parameters is
// involved.
// @todo Remove when https://www.drupal.org/node/2761639 is fixed in
// Drupal core.
\Drupal::service('page_cache_kill_switch')->trigger();
}
}
}
/**
* {@inheritdoc}
*/
static function getSubscribedEvents() {
$events[KernelEvents::REQUEST][] = array('onKernelRequestRedirect', 30);
return $events;
}
}
@@ -0,0 +1,24 @@
<?php
namespace Drupal\redirect\Exception;
use Drupal\Component\Utility\SafeMarkup;
/**
* Exception for when a redirect loop is detected.
*/
class RedirectLoopException extends \RuntimeException {
/**
* Formats a redirect loop exception message.
*
* @param string $path
* The path that results in a redirect loop.
* @param int $rid
* The redirect ID that is involved in a loop.
*/
public function __construct($path, $rid) {
parent::__construct(SafeMarkup::format('Redirect loop identified at %path for redirect %rid', ['%path' => $path, '%rid' => $rid]));
}
}
@@ -0,0 +1,41 @@
<?php
namespace Drupal\redirect\Form;
use Drupal\Core\Entity\ContentEntityConfirmFormBase;
use Drupal\Core\Url;
use Drupal\Core\Form\FormStateInterface;
class RedirectDeleteForm extends ContentEntityConfirmFormBase {
/**
* {@inheritdoc}
*/
public function getQuestion() {
return $this->t('Are you sure you want to delete the URL redirect from %source to %redirect?', array('%source' => $this->entity->getSourceUrl(), '%redirect' => $this->entity->getRedirectUrl()->toString()));
}
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
return new Url('redirect.list');
}
/**
* {@inheritdoc}
*/
public function getConfirmText() {
return $this->t('Delete');
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->entity->delete();
drupal_set_message(t('The redirect %redirect has been deleted.', array('%redirect' => $this->entity->getRedirectUrl()->toString())));
$form_state->setRedirect('redirect.list');
}
}
@@ -0,0 +1,140 @@
<?php
namespace Drupal\redirect\Form;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\ConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Drupal\user\PrivateTempStoreFactory;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\TranslationInterface;
/**
* Provides a redirect deletion confirmation form.
*/
class RedirectDeleteMultipleForm extends ConfirmFormBase {
/**
* The array of redirects to delete.
*
* @var string[][]
*/
protected $redirects = [];
/**
* The private tempstore factory.
*
* @var \Drupal\user\PrivateTempStoreFactory
*/
protected $privateTempStoreFactory;
/**
* The redirect storage.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $redirectStorage;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* Constructs a RedirectDeleteMultiple form object.
*
* @param \Drupal\user\PrivateTempStoreFactory $temp_store_factory
* The tempstore factory.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Session\AccountInterface $account
* The current user.
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* The String translation.
*/
public function __construct(PrivateTempStoreFactory $temp_store_factory, EntityTypeManagerInterface $entity_type_manager, AccountInterface $account, TranslationInterface $string_translation) {
$this->privateTempStoreFactory = $temp_store_factory;
$this->redirectStorage = $entity_type_manager->getStorage('redirect');
$this->currentUser = $account;
$this->setStringTranslation($string_translation);
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('user.private_tempstore'),
$container->get('entity_type.manager'),
$container->get('current_user'),
$container->get('string_translation')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'redirect_multiple_delete_confirm';
}
/**
* {@inheritdoc}
*/
public function getQuestion() {
return $this->formatPlural(count($this->redirects), 'Are you sure you want to delete this redirect?', 'Are you sure you want to delete these redirects?');
}
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
return new Url('redirect.list');
}
/**
* {@inheritdoc}
*/
public function getConfirmText() {
return t('Delete');
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$this->redirects = $this->privateTempStoreFactory->get('redirect_multiple_delete_confirm')->get($this->currentUser->id());
if (empty($this->redirects)) {
return new RedirectResponse($this->getCancelUrl()->setAbsolute()->toString());
}
$form['redirects'] = [
'#theme' => 'item_list',
'#items' => array_map(function ($redirect) {
return $redirect->label();
}, $this->redirects),
];
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
if ($form_state->getValue('confirm') && !empty($this->redirects)) {
$this->redirectStorage->delete($this->redirects);
$this->privateTempStoreFactory->get('redirect_multiple_delete_confirm')->delete($this->currentUser->id());
$count = count($this->redirects);
$this->logger('redirect')->notice('Deleted @count redirects.', ['@count' => $count]);
drupal_set_message($this->stringTranslation->formatPlural($count, 'Deleted 1 redirect.', 'Deleted @count redirects.'));
}
$form_state->setRedirect('redirect.list');
}
}
@@ -0,0 +1,152 @@
<?php
namespace Drupal\redirect\Form;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Entity\ContentEntityForm;
use Drupal\Core\Language\Language;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Routing\MatchingRouteNotFoundException;
use Drupal\Core\Url;
use Drupal\redirect\Entity\Redirect;
use Drupal\Core\Form\FormStateInterface;
class RedirectForm extends ContentEntityForm {
/**
* {@inheritdoc}
*/
protected function prepareEntity() {
/** @var \Drupal\redirect\Entity\Redirect $redirect */
$redirect = $this->entity;
if ($redirect->isNew()) {
// To pass in the query set parameters into GET as follows:
// source_query[key1]=value1&source_query[key2]=value2
$source_query = array();
if ($this->getRequest()->get('source_query')) {
$source_query = $this->getRequest()->get('source_query');
}
$redirect_options = array();
$redirect_query = array();
if ($this->getRequest()->get('redirect_options')) {
$redirect_options = $this->getRequest()->get('redirect_options');
if (isset($redirect_options['query'])) {
$redirect_query = $redirect_options['query'];
unset($redirect_options['query']);
}
}
$source_url = urldecode($this->getRequest()->get('source'));
if (!empty($source_url)) {
$redirect->setSource($source_url, $source_query);
}
$redirect_url = urldecode($this->getRequest()->get('redirect'));
if (!empty($redirect_url)) {
try {
$redirect->setRedirect($redirect_url, $redirect_query, $redirect_options);
}
catch (MatchingRouteNotFoundException $e) {
drupal_set_message(t('Invalid redirect URL %url provided.', array('%url' => $redirect_url)), 'warning');
}
}
$redirect->setLanguage($this->getRequest()->get('language') ? $this->getRequest()->get('language') : Language::LANGCODE_NOT_SPECIFIED);
}
}
/**
* {@inheritdoc}
*/
public function form(array $form, FormStateInterface $form_state) {
$form = parent::form($form, $form_state);
/** @var \Drupal\redirect\Entity\Redirect $redirect */
$redirect = $this->entity;
// Only add the configured languages and a single key for all languages.
if (isset($form['language']['widget'][0]['value'])) {
foreach (\Drupal::languageManager()->getLanguages(LanguageInterface::STATE_CONFIGURABLE) as $langcode => $language) {
$form['language']['widget'][0]['value']['#options'][$langcode] = $language->getName();
}
$form['language']['widget'][0]['value']['#options'][LanguageInterface::LANGCODE_NOT_SPECIFIED] = t('- All languages -');
}
$default_code = $redirect->getStatusCode() ? $redirect->getStatusCode() : \Drupal::config('redirect.settings')->get('default_status_code');
$form['status_code'] = array(
'#type' => 'select',
'#title' => t('Redirect status'),
'#description' => t('You can find more information about HTTP redirect status codes at <a href="@status-codes">@status-codes</a>.', array('@status-codes' => 'http://en.wikipedia.org/wiki/List_of_HTTP_status_codes#3xx_Redirection')),
'#default_value' => $default_code,
'#options' => redirect_status_code_options(),
);
return $form;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
parent::validateForm($form, $form_state);
$source = $form_state->getValue(array('redirect_source', 0));
$redirect = $form_state->getValue(array('redirect_redirect', 0));
if ($source['path'] == '<front>') {
$form_state->setErrorByName('redirect_source', t('It is not allowed to create a redirect from the front page.'));
}
if (strpos($source['path'], '#') !== FALSE) {
$form_state->setErrorByName('redirect_source', t('The anchor fragments are not allowed.'));
}
if (strpos($source['path'], '/') === 0) {
$form_state->setErrorByName('redirect_source', t('The url to redirect from should not start with a forward slash (/).'));
}
try {
$source_url = Url::fromUri('internal:/' . $source['path']);
$redirect_url = Url::fromUri($redirect['uri']);
// It is relevant to do this comparison only in case the source path has
// a valid route. Otherwise the validation will fail on the redirect path
// being an invalid route.
if ($source_url->toString() == $redirect_url->toString()) {
$form_state->setErrorByName('redirect_redirect', t('You are attempting to redirect the page to itself. This will result in an infinite loop.'));
}
}
catch (\InvalidArgumentException $e) {
// Do nothing, we want to only compare the resulting URLs.
}
$parsed_url = UrlHelper::parse(trim($source['path']));
$path = isset($parsed_url['path']) ? $parsed_url['path'] : NULL;
$query = isset($parsed_url['query']) ? $parsed_url['query'] : NULL;
$hash = Redirect::generateHash($path, $query, $form_state->getValue('language')[0]['value']);
// Search for duplicate.
$redirects = \Drupal::entityManager()
->getStorage('redirect')
->loadByProperties(array('hash' => $hash));
if (!empty($redirects)) {
$redirect = array_shift($redirects);
if ($this->entity->isNew() || $redirect->id() != $this->entity->id()) {
$form_state->setErrorByName('redirect_source', t('The source path %source is already being redirected. Do you want to <a href="@edit-page">edit the existing redirect</a>?',
array(
'%source' => $source['path'],
'@edit-page' => $redirect->url('edit-form'))));
}
}
}
/**
* {@inheritdoc}
*/
public function save(array $form, FormStateInterface $form_state) {
$this->entity->save();
drupal_set_message(t('The redirect has been saved.'));
$form_state->setRedirect('redirect.list');
}
}
@@ -0,0 +1,93 @@
<?php
namespace Drupal\redirect\Form;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
class RedirectSettingsForm extends ConfigFormBase {
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'redirect_settings_form';
}
/**
* {@inheritdoc}
*/
protected function getEditableConfigNames() {
return ['redirect.settings'];
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$config = $this->config('redirect.settings');
$form['redirect_auto_redirect'] = array(
'#type' => 'checkbox',
'#title' => $this->t('Automatically create redirects when URL aliases are changed.'),
'#default_value' => $config->get('auto_redirect'),
'#disabled' => !\Drupal::moduleHandler()->moduleExists('path'),
);
$form['redirect_passthrough_querystring'] = array(
'#type' => 'checkbox',
'#title' => $this->t('Retain query string through redirect.'),
'#default_value' => $config->get('passthrough_querystring'),
'#description' => $this->t('For example, given a redirect from %source to %redirect, if a user visits %sourcequery they would be redirected to %redirectquery. The query strings in the redirection will always take precedence over the current query string.', array('%source' => 'source-path', '%redirect' => 'node?a=apples', '%sourcequery' => 'source-path?a=alligators&b=bananas', '%redirectquery' => 'node?a=apples&b=bananas')),
);
$form['redirect_warning'] = array(
'#type' => 'checkbox',
'#title' => $this->t('Display a warning message to users when they are redirected.'),
'#default_value' => $config->get('warning'),
'#access' => FALSE,
);
$form['redirect_default_status_code'] = array(
'#type' => 'select',
'#title' => $this->t('Default redirect status'),
'#description' => $this->t('You can find more information about HTTP redirect status codes at <a href="@status-codes">@status-codes</a>.', array('@status-codes' => 'http://en.wikipedia.org/wiki/List_of_HTTP_status_codes#3xx_Redirection')),
'#options' => redirect_status_code_options(),
'#default_value' => $config->get('default_status_code'),
);
$form['globals'] = array(
'#type' => 'fieldset',
'#title' => $this->t('Global redirects'),
'#description' => $this->t('(formerly Global Redirect features)'),
);
$form['globals']['redirect_route_normalizer_enabled'] = array(
'#type' => 'checkbox',
'#title' => $this->t('Enforce clean and canonical URLs.'),
'#description' => $this->t('Enabling this will automatically redirect to the canonical URL of any page. That includes redirecting to an alias if existing, removing trainling slashes, ensure the language prefix is set and similar clean-up.'),
'#default_value' => $config->get('route_normalizer_enabled'),
);
$form['globals']['redirect_ignore_admin_path'] = array(
'#type' => 'checkbox',
'#title' => $this->t('Ignore redirections on admin paths.'),
'#default_value' => $config->get('ignore_admin_path'),
);
$form['globals']['redirect_access_check'] = array(
'#type' => 'checkbox',
'#title' => $this->t('Check access to the redirected page'),
'#description' => $this->t('This helps to stop redirection on protected pages and avoids giving away <em>secret</em> URL\'s. <strong>By default this feature is disabled to avoid any unexpected behavior</strong>'),
'#default_value' => $config->get('access_check'),
);
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$config = $this->config('redirect.settings');
foreach ($form_state->getValues() as $key => $value) {
if (strpos($key, 'redirect_') !== FALSE) {
$config->set(str_replace('redirect_', '', $key), $value);
}
}
$config->save();
drupal_set_message(t('Configuration was saved.'));
}
}
@@ -0,0 +1,92 @@
<?php
namespace Drupal\redirect\Plugin\Action;
use Drupal\Core\Action\ActionBase;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\user\PrivateTempStoreFactory;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Redirects to a redirect deletion form.
*
* @Action(
* id = "redirect_delete_action",
* label = @Translation("Delete redirect"),
* type = "redirect",
* confirm_form_route_name = "entity.redirect.multiple_delete_confirm"
* )
*/
class DeleteRedirect extends ActionBase implements ContainerFactoryPluginInterface {
/**
* The tempstore object.
*
* @var \Drupal\user\SharedTempStore
*/
protected $privateTempStore;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* Constructs a new DeleteRedirect 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\user\PrivateTempStoreFactory $temp_store_factory
* The tempstore factory.
* @param AccountInterface $current_user
* Current user.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, PrivateTempStoreFactory $temp_store_factory, AccountInterface $current_user) {
$this->currentUser = $current_user;
$this->privateTempStore = $temp_store_factory->get('redirect_multiple_delete_confirm');
parent::__construct($configuration, $plugin_id, $plugin_definition);
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('user.private_tempstore'),
$container->get('current_user')
);
}
/**
* {@inheritdoc}
*/
public function executeMultiple(array $entities) {
$this->privateTempStore->set($this->currentUser->id(), $entities);
}
/**
* {@inheritdoc}
*/
public function execute($object = NULL) {
$this->executeMultiple([$object]);
}
/**
* {@inheritdoc}
*/
public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
return $object->access('delete', $account, $return_as_object);
}
}
@@ -0,0 +1,36 @@
<?php
namespace Drupal\redirect\Plugin\Field\FieldFormatter;
use Drupal\Core\Field\FormatterBase;
use Drupal\Core\Field\FieldItemListInterface;
/**
* Implementation of the 'redirect_source' formatter.
*
* @FieldFormatter(
* id = "redirect_source",
* label = @Translation("Redirect Source"),
* field_types = {
* "redirect_source",
* }
* )
*/
class RedirectSourceFormatter extends FormatterBase {
/**
* {@inheritdoc}
*/
public function viewElements(FieldItemListInterface $items, $langcode) {
$elements = array();
foreach ($items as $delta => $item) {
$elements[$delta] = array(
'#markup' => urldecode($item->getUrl()->toString()),
);
}
return $elements;
}
}
@@ -0,0 +1,112 @@
<?php
namespace Drupal\redirect\Plugin\Field\FieldType;
use Drupal\Component\Utility\Random;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemBase;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\TypedData\DataDefinition;
use Drupal\Core\TypedData\MapDataDefinition;
use Drupal\Core\Url;
/**
* Plugin implementation of the 'link' field type for redirect source.
*
* @FieldType(
* id = "redirect_source",
* label = @Translation("Redirect source"),
* description = @Translation("Stores a redirect source"),
* default_widget = "redirect_source",
* default_formatter = "redirect_source",
* no_ui = TRUE
* )
*/
class RedirectSourceItem extends FieldItemBase {
/**
* {@inheritdoc}
*/
public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
$properties['path'] = DataDefinition::create('string')
->setLabel(t('Path'));
$properties['query'] = MapDataDefinition::create()
->setLabel(t('Query'));
return $properties;
}
/**
* {@inheritdoc}
*/
public static function schema(FieldStorageDefinitionInterface $field_definition) {
return array(
'columns' => array(
'path' => array(
'description' => 'The source path',
'type' => 'varchar',
'length' => 2048,
),
'query' => array(
'description' => 'Serialized array of path queries',
'type' => 'blob',
'size' => 'big',
'serialize' => TRUE,
),
),
'indexes' => array(
'path' => array(array('path', 50)),
),
);
}
/**
* {@inheritdoc}
*/
public static function generateSampleValue(FieldDefinitionInterface $field_definition) {
// Set random length for the path.
$domain_length = mt_rand(7, 15);
$random = new Random();
$values['path'] = 'http://www.' . $random->word($domain_length);
return $values;
}
/**
* {@inheritdoc}
*/
public function isEmpty() {
return $this->path === NULL || $this->path === '';
}
/**
* {@inheritdoc}
*/
public static function mainPropertyName() {
return 'path';
}
/**
* {@inheritdoc}
*/
public function setValue($values, $notify = TRUE) {
// Unserialize the values.
// @todo The storage controller should take care of this, see
// SqlContentEntityStorage::loadFieldItems, see
// https://www.drupal.org/node/2414835
if (isset($values['query']) && is_string($values['query'])) {
$values['query'] = unserialize($values['query']);
}
parent::setValue($values, $notify);
}
/**
* {@inheritdoc}
*/
public function getUrl() {
return Url::fromUri('base:' . $this->path, ['query' => $this->query]);
}
}
@@ -0,0 +1,117 @@
<?php
namespace Drupal\redirect\Plugin\Field\FieldWidget;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\WidgetBase;
use Drupal\Core\Url;
use Drupal\Core\Form\FormStateInterface;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
/**
* Plugin implementation of the 'link' widget for the redirect module.
*
* Note that this field is meant only for the source field of the redirect
* entity as it drops validation for non existing paths.
*
* @FieldWidget(
* id = "redirect_source",
* label = @Translation("Redirect source"),
* field_types = {
* "link"
* },
* settings = {
* "placeholder_url" = "",
* "placeholder_title" = ""
* }
* )
*/
class RedirectSourceWidget extends WidgetBase {
/**
* {@inheritdoc}
*/
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
$default_url_value = $items[$delta]->path;
if ($items[$delta]->query) {
$default_url_value .= '?' . http_build_query($items[$delta]->query);
}
$element['path'] = array(
'#type' => 'textfield',
'#title' => $this->t('Path'),
'#placeholder' => $this->getSetting('placeholder_url'),
'#default_value' => $default_url_value,
'#maxlength' => 2048,
'#required' => $element['#required'],
'#field_prefix' => Url::fromRoute('<front>', array(), array('absolute' => TRUE))->toString(),
'#attributes' => array('data-disable-refocus' => 'true'),
);
// If creating new URL add checks.
if ($items->getEntity()->isNew()) {
$element['status_box'] = array(
'#prefix' => '<div id="redirect-link-status">',
'#suffix' => '</div>',
);
$source_path = $form_state->getValue(array('redirect_source', 0, 'path'));
if ($source_path) {
$source_path = trim($source_path);
// Warning about creating a redirect from a valid path.
// @todo - Hmm... exception driven logic. Find a better way how to
// determine if we have a valid path.
try {
\Drupal::service('router')->match('/' . $form_state->getValue(array('redirect_source', 0, 'path')));
$element['status_box'][]['#markup'] = '<div class="messages messages--warning">' . t('The source path %path is likely a valid path. It is preferred to <a href="@url-alias">create URL aliases</a> for existing paths rather than redirects.',
array('%path' => $source_path, '@url-alias' => Url::fromRoute('path.admin_add')->toString())) . '</div>';
}
catch (ResourceNotFoundException $e) {
// Do nothing, expected behaviour.
}
// Warning about the path being already redirected.
$parsed_url = UrlHelper::parse($source_path);
$path = isset($parsed_url['path']) ? $parsed_url['path'] : NULL;
if (!empty($path)) {
/** @var \Drupal\redirect\RedirectRepository $repository */
$repository = \Drupal::service('redirect.repository');
$redirects = $repository->findBySourcePath($path);
if (!empty($redirects)) {
$redirect = array_shift($redirects);
$element['status_box'][]['#markup'] = '<div class="messages messages--warning">' . t('The base source path %source is already being redirected. Do you want to <a href="@edit-page">edit the existing redirect</a>?', array('%source' => $source_path, '@edit-page' => $redirect->url('edit-form'))) . '</div>';
}
}
}
$element['path']['#ajax'] = array(
'callback' => 'redirect_source_link_get_status_messages',
'wrapper' => 'redirect-link-status',
);
}
return $element;
}
/**
* {@inheritdoc}
*/
public function massageFormValues(array $values, array $form, FormStateInterface $form_state) {
$values = parent::massageFormValues($values, $form, $form_state);
// It is likely that the url provided for this field is not existing and
// so the logic in the parent method did not set any defaults. Just run
// through all url values and add defaults.
foreach ($values as &$value) {
if (!empty($value['path'])) {
// In case we have query process the url.
if (strpos($value['path'], '?') !== FALSE) {
$url = UrlHelper::parse($value['path']);
$value['path'] = $url['path'];
$value['query'] = $url['query'];
}
}
}
return $values;
}
}
@@ -0,0 +1,92 @@
<?php
namespace Drupal\redirect\Plugin\Validation\Constraint;
use Drupal\link\LinkItemInterface;
use Drupal\Core\Url;
use Drupal\Core\ParamConverter\ParamNotConvertedException;
use Drupal\Component\Utility\UrlHelper;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidatorInterface;
use Symfony\Component\Validator\ExecutionContextInterface;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Validation constraint for links receiving data allowed by its settings.
*
* @Constraint(
* id = "RedirectSourceLinkType",
* label = @Translation("Link data valid for redirect source link type.", context = "Validation"),
* )
*/
class SourceLinkTypeConstraint extends Constraint implements ConstraintValidatorInterface {
public $message = 'The URL %url is not valid magor.';
/**
* @var \Symfony\Component\Validator\ExecutionContextInterface
*/
protected $context;
/**
* {@inheritDoc}
*/
public function initialize(ExecutionContextInterface $context) {
$this->context = $context;
}
/**
* {@inheritdoc}
*/
public function validatedBy() {
return get_class($this);
}
/**
* {@inheritdoc}
*/
public function validate($value, Constraint $constraint) {
if (isset($value)) {
$url_is_valid = TRUE;
/** @var $link_item \Drupal\link\LinkItemInterface */
$link_item = $value;
$link_type = $link_item->getFieldDefinition()->getSetting('link_type');
$url_string = $link_item->url;
// Validate the url property.
if ($url_string !== '') {
try {
// @todo This shouldn't be needed, but massageFormValues() may not
// run.
$parsed_url = UrlHelper::parse($url_string);
if (!empty($parsed_url['path'])) {
$url = Url::fromUri('internal:' . $parsed_url['path']);
if ($url->isExternal() && !UrlHelper::isValid($url_string, TRUE)) {
$url_is_valid = FALSE;
}
elseif ($url->isExternal() && !($link_type & LinkItemInterface::LINK_EXTERNAL)) {
$url_is_valid = FALSE;
}
}
}
catch (NotFoundHttpException $e) {
$url_is_valid = FALSE;
}
catch (ResourceNotFoundException $e) {
// User is creating a redirect from non existing path. This is not an
// error state.
$url_is_valid = TRUE;
}
catch (ParamNotConvertedException $e) {
$url_is_valid = FALSE;
}
}
if (!$url_is_valid) {
$this->context->addViolation($this->message, array('%url' => $url_string));
}
}
}
}
@@ -0,0 +1,42 @@
<?php
namespace Drupal\redirect\Plugin\migrate\process;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
/**
* @MigrateProcessPlugin(
* id = "d6_path_redirect"
* )
*/
class PathRedirect extends ProcessPluginBase {
/**
* {@inheritdoc}
*
* Transform the field as required for an iFrame field.
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
// Check if the url begins with http.
if (preg_match('#^http#', $value[0])) {
// Use it as is.
$uri = $value[0];
}
else {
// Make the link internal.
$uri = 'internal:/' . $value[0];
}
// Check if there is a query.
if (!empty($value[1])) {
// Add it to the end of the url.
$uri .= '?' . $value[1];
}
return $uri;
}
}
@@ -0,0 +1,54 @@
<?php
/**
* @file
* Contains \Drupal\redirect\Plugin\migrate\process\d7\PathRedirect.
*/
namespace Drupal\redirect\Plugin\migrate\process\d7;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
/**
* @MigrateProcessPlugin(
* id = "d7_path_redirect"
* )
*/
class PathRedirect extends ProcessPluginBase {
/**
* {@inheritdoc}
*
* Transform the field as required for an iFrame field.
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
// Check if the url begins with http.
if (preg_match('#^http#', $value[0])) {
// Use it as is.
$uri = $value[0];
}
else {
// Make the link internal.
$uri = 'internal:/' . $value[0];
}
// Check if there are options.
if (!empty($value[1])) {
// Check if there is a query.
$options = unserialize($value[1]);
if (!empty($options['query'])) {
// Add it to the end of the url.
$uri .= '?' . http_build_query($options['query']);
}
if (!empty($options['fragment'])) {
$uri .= '#' . $options['fragment'];
}
}
return $uri;
}
}
@@ -0,0 +1,41 @@
<?php
/**
* @file
* Contains \Drupal\redirect\Plugin\migrate\process\d7\RedirectSourceQuery.
*/
namespace Drupal\redirect\Plugin\migrate\process\d7;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
/**
* @MigrateProcessPlugin(
* id = "d7_redirect_source_query"
* )
*/
class RedirectSourceQuery extends ProcessPluginBase {
/**
* {@inheritdoc}
*
* Transform the field as required for an iFrame field.
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
// Check if there are options.
if (!empty($value)) {
// Check if there is a query.
$options = unserialize($value);
if (!empty($options['query'])) {
// Add it to the end of the url.
return serialize($options['query']);
}
}
return NULL;
}
}
@@ -0,0 +1,62 @@
<?php
namespace Drupal\redirect\Plugin\migrate\source;
use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
/**
* Drupal 6 path redirect source from database.
*
* @MigrateSource(
* id = "d6_path_redirect",
* source_module = "redirect"
* )
*/
class PathRedirect extends DrupalSqlBase {
/**
* {@inheritdoc}
*/
public function query() {
// Select path redirects.
$query = $this->select('path_redirect', 'p')
->fields('p', array(
'rid',
'source',
'redirect',
'query',
'fragment',
'language',
'type',
'last_used',
));
return $query;
}
/**
* {@inheritdoc}
*/
public function fields() {
$fields = array(
'rid' => $this->t('Redirect ID'),
'source' => $this->t('Source'),
'redirect' => $this->t('Redirect'),
'query' => $this->t('Query'),
'fragment' => $this->t('Fragment'),
'language' => $this->t('Language'),
'type' => $this->t('Type'),
'last_used' => $this->t('Last Used'),
);
return $fields;
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['rid']['type'] = 'integer';
return $ids;
}
}
@@ -0,0 +1,81 @@
<?php
/**
* @file
* Contains \Drupal\redirect\Plugin\migrate\source\d7\PathRedirect.
*/
namespace Drupal\redirect\Plugin\migrate\source\d7;
use Drupal\migrate\Row;
use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
/**
* Drupal 7 path redirect source from database.
*
* @MigrateSource(
* id = "d7_path_redirect",
* source_module = "redirect"
* )
*/
class PathRedirect extends DrupalSqlBase {
/**
* {@inheritdoc}
*/
public function query() {
// Select path redirects.
$query = $this->select('redirect', 'p')->fields('p');
return $query;
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
static $default_status_code;
if (!isset($default_status_code)) {
$default_status_code = unserialize($this->getDatabase()
->select('variable', 'v')
->fields('v', ['value'])
->condition('name', 'redirect_default_status_code')
->execute()
->fetchField());
}
$current_status_code = $row->getSourceProperty('status_code');
$status_code = $current_status_code != 0 ? $current_status_code : $default_status_code;
$row->setSourceProperty('status_code', $status_code);
return parent::prepareRow($row);
}
/**
* {@inheritdoc}
*/
public function fields() {
$fields = [
'rid' => $this->t('Redirect ID'),
'hash' => $this->t('Hash'),
'type' => $this->t('Type'),
'uid' => $this->t('UID'),
'source' => $this->t('Source'),
'source_options' => $this->t('Source Options'),
'redirect' => $this->t('Redirect'),
'redirect_options' => $this->t('Redirect Options'),
'language' => $this->t('Language'),
'status_code' => $this->t('Status Code'),
'count' => $this->t('Count'),
'access' => $this->t('Access'),
];
return $fields;
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['rid']['type'] = 'integer';
return $ids;
}
}
@@ -0,0 +1,98 @@
<?php
namespace Drupal\redirect;
use Drupal\Core\Access\AccessManager;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\State\StateInterface;
use Symfony\Cmf\Component\Routing\RouteObjectInterface;
use Symfony\Cmf\Component\Routing\RouteProviderInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Redirect checker class.
*/
class RedirectChecker {
/**
* @var \Drupal\Core\Config\Config
*/
protected $config;
/**
* @var \Drupal\Core\State\StateInterface
*/
protected $state;
/**
* @var \Drupal\Core\Access\AccessManager
*/
protected $accessManager;
/**
* @var \Drupal\Core\Session\AccountInterface
*/
protected $account;
/**
* @var \Drupal\Core\Routing\RouteProviderInterface
*/
protected $routeProvider;
public function __construct(ConfigFactoryInterface $config, StateInterface $state, AccessManager $access_manager, AccountInterface $account, RouteProviderInterface $route_provider) {
$this->config = $config->get('redirect.settings');
$this->accessManager = $access_manager;
$this->state = $state;
$this->account = $account;
$this->routeProvider = $route_provider;
}
/**
* Determines if redirect may be performed.
*
* @param Request $request
* The current request object.
* @param string $route_name
* The current route name.
*
* @return bool
* TRUE if redirect may be performed.
*/
public function canRedirect(Request $request, $route_name = NULL) {
$can_redirect = TRUE;
if (isset($route_name)) {
$route = $this->routeProvider->getRouteByName($route_name);
if ($this->config->get('access_check')) {
// Do not redirect if is a protected page.
$can_redirect = $this->accessManager->checkNamedRoute($route_name, [], $this->account);
}
}
else {
$route = $request->attributes->get(RouteObjectInterface::ROUTE_OBJECT);
}
if (!preg_match('/index\.php$/', $request->getScriptName())) {
// Do not redirect if the root script is not /index.php.
$can_redirect = FALSE;
}
elseif (!($request->isMethod('GET') || $request->isMethod('HEAD'))) {
// Do not redirect if this is other than GET request.
$can_redirect = FALSE;
}
elseif ($this->state->get('system.maintenance_mode') || defined('MAINTENANCE_MODE')) {
// Do not redirect in offline or maintenance mode.
$can_redirect = FALSE;
}
elseif ($request->query->has('destination')) {
$can_redirect = FALSE;
}
elseif ($this->config->get('ignore_admin_path') && isset($route)) {
// Do not redirect on admin paths.
$can_redirect &= !(bool) $route->getOption('_admin_route');
}
return $can_redirect;
}
}
@@ -0,0 +1,178 @@
<?php
namespace Drupal\redirect;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Database\Connection;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Language\Language;
use Drupal\redirect\Entity\Redirect;
use Drupal\redirect\Exception\RedirectLoopException;
class RedirectRepository {
/**
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $manager;
/**
* @var \Drupal\Core\Database\Connection
*/
protected $connection;
/**
* @var \Drupal\Core\Config\ImmutableConfig
*/
protected $config;
/**
* An array of found redirect IDs to avoid recursion.
*
* @var array
*/
protected $foundRedirects = [];
/**
* Constructs a \Drupal\redirect\EventSubscriber\RedirectRequestSubscriber object.
*
* @param \Drupal\Core\Entity\EntityManagerInterface $manager
* The entity manager service.
* @param \Drupal\Core\Database\Connection $connection
* The database connection.
*/
public function __construct(EntityManagerInterface $manager, Connection $connection, ConfigFactoryInterface $config_factory) {
$this->manager = $manager;
$this->connection = $connection;
$this->config = $config_factory->get('redirect.settings');
}
/**
* Gets a redirect for given path, query and language.
*
* @param string $source_path
* The redirect source path.
* @param array $query
* The redirect source path query.
* @param $language
* The language for which is the redirect.
*
* @return \Drupal\redirect\Entity\Redirect
* The matched redirect entity.
*
* @throws \Drupal\redirect\Exception\RedirectLoopException
*/
public function findMatchingRedirect($source_path, array $query = [], $language = Language::LANGCODE_NOT_SPECIFIED) {
$source_path = ltrim($source_path, '/');
$hashes = [Redirect::generateHash($source_path, $query, $language)];
if ($language != Language::LANGCODE_NOT_SPECIFIED) {
$hashes[] = Redirect::generateHash($source_path, $query, Language::LANGCODE_NOT_SPECIFIED);
}
// Add a hash without the query string if using passthrough querystrings.
if (!empty($query) && $this->config->get('passthrough_querystring')) {
$hashes[] = Redirect::generateHash($source_path, [], $language);
if ($language != Language::LANGCODE_NOT_SPECIFIED) {
$hashes[] = Redirect::generateHash($source_path, [], Language::LANGCODE_NOT_SPECIFIED);
}
}
// Load redirects by hash. A direct query is used to improve performance.
$rid = $this->connection->query('SELECT rid FROM {redirect} WHERE hash IN (:hashes[]) ORDER BY LENGTH(redirect_source__query) DESC', [':hashes[]' => $hashes])->fetchField();
if (!empty($rid)) {
// Check if this is a loop.
if (in_array($rid, $this->foundRedirects)) {
throw new RedirectLoopException('/' . $source_path, $rid);
}
$this->foundRedirects[] = $rid;
$redirect = $this->load($rid);
// Find chained redirects.
if ($recursive = $this->findByRedirect($redirect, $language)) {
// Reset found redirects.
$this->foundRedirects = [];
return $recursive;
}
return $redirect;
}
return NULL;
}
/**
* Helper function to find recursive redirects.
*
* @param \Drupal\redirect\Entity\Redirect
* The redirect object.
* @param string $language
* The language to use.
*/
protected function findByRedirect(Redirect $redirect, $language) {
$uri = $redirect->getRedirectUrl();
$baseUrl = \Drupal::request()->getBaseUrl();
$path = ltrim(substr($uri->toString(), strlen($baseUrl)), '/');
$query = $uri->getOption('query') ?: [];
return $this->findMatchingRedirect($path, $query, $language);
}
/**
* Finds redirects based on the source path.
*
* @param string $source_path
* The redirect source path (without the query).
*
* @return \Drupal\redirect\Entity\Redirect[]
* Array of redirect entities.
*/
public function findBySourcePath($source_path) {
$ids = $this->manager->getStorage('redirect')->getQuery()
->condition('redirect_source.path', $source_path, 'LIKE')
->execute();
return $this->manager->getStorage('redirect')->loadMultiple($ids);
}
/**
* Finds redirects based on the destination URI.
*
* @param string[] $destination_uri
* List of destination URIs, for example ['internal:/node/123'].
*
* @return \Drupal\redirect\Entity\Redirect[]
* Array of redirect entities.
*/
public function findByDestinationUri(array $destination_uri) {
$storage = $this->manager->getStorage('redirect');
$ids = $storage->getQuery()
->condition('redirect_redirect.uri', $destination_uri, 'IN')
->execute();
return $storage->loadMultiple($ids);
}
/**
* Load redirect entity by id.
*
* @param int $redirect_id
* The redirect id.
*
* @return \Drupal\redirect\Entity\Redirect
*/
public function load($redirect_id) {
return $this->manager->getStorage('redirect')->load($redirect_id);
}
/**
* Loads multiple redirect entities.
*
* @param array $redirect_ids
* Redirect ids to load.
*
* @return \Drupal\redirect\Entity\Redirect[]
* List of redirect entities.
*/
public function loadMultiple(array $redirect_ids = NULL) {
return $this->manager->getStorage('redirect')->loadMultiple($redirect_ids);
}
}
@@ -0,0 +1,31 @@
<?php
namespace Drupal\redirect;
use Drupal\Core\Entity\ContentEntityTypeInterface;
use Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema;
/**
* Defines the redirect schema.
*/
class RedirectStorageSchema extends SqlContentEntityStorageSchema {
/**
* {@inheritdoc}
*/
protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $reset = FALSE) {
$schema = parent::getEntitySchema($entity_type, $reset);
// Add indexes.
$schema['redirect']['unique keys'] += [
'hash' => ['hash'],
];
$schema['redirect']['indexes'] += [
// Limit length to 191.
'source_language' => [['redirect_source__path', 191], 'language'],
];
return $schema;
}
}
@@ -0,0 +1,28 @@
<?php
namespace Drupal\redirect\Routing;
use Drupal\Core\Routing\RouteSubscriberBase;
use Symfony\Component\Routing\RouteCollection;
/**
* Modify core routes to support redirect.
*/
class RouteSubscriber extends RouteSubscriberBase {
/**
* {@inheritdoc}
*/
protected function alterRoutes(RouteCollection $collection) {
if ($route = $collection->get('image.style_public')) {
$route->setDefault('_disable_route_normalizer', TRUE);
}
if ($route = $collection->get('image.style_private')) {
$route->setDefault('_disable_route_normalizer', TRUE);
}
if ($route = $collection->get('system.files')) {
$route->setDefault('_disable_route_normalizer', TRUE);
}
}
}
@@ -0,0 +1,53 @@
<?php
namespace Drupal\redirect\Tests;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Url;
/**
* Asserts the redirect from a given path to the expected destination path.
*/
trait AssertRedirectTrait {
/**
* Asserts the redirect from $path to the $expected_ending_url.
*
* @param string $path
* The request path.
* @param $expected_ending_url
* The path where we expect it to redirect. If NULL value provided, no
* redirect is expected.
* @param string $expected_ending_status
* The status we expect to get with the first request.
*/
public function assertRedirect($path, $expected_ending_url, $expected_ending_status = 'HTTP/1.1 301 Moved Permanently') {
$this->drupalHead($path);
$headers = $this->drupalGetHeaders(TRUE);
$ending_url = isset($headers[0]['location']) ? $headers[0]['location'] : NULL;
$message = SafeMarkup::format('Testing redirect from %from to %to. Ending url: %url', [
'%from' => $path,
'%to' => $expected_ending_url,
'%url' => $ending_url,
]);
if ($expected_ending_url == '<front>') {
$expected_ending_url = Url::fromUri('base:')->setAbsolute()->toString();
}
elseif (!empty($expected_ending_url)) {
// Check for absolute/external urls.
if (!parse_url($expected_ending_url, PHP_URL_SCHEME)) {
$expected_ending_url = Url::fromUri('base:' . $expected_ending_url)->setAbsolute()->toString();
}
}
else {
$expected_ending_url = NULL;
}
$this->assertEqual($expected_ending_url, $ending_url, $message);
$this->assertEqual($headers[0][':status'], $expected_ending_status);
}
}
@@ -0,0 +1,276 @@
<?php
namespace Drupal\redirect\Tests;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Language\Language;
use Drupal\simpletest\WebTestBase;
use Drupal\language\Entity\ConfigurableLanguage;
/**
* Global redirect test cases.
*
* @group redirect
*/
class GlobalRedirectTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = [
'path',
'node',
'redirect',
'taxonomy',
'forum',
'views',
'language',
'content_translation',
];
/**
* @var \Drupal\Core\Session\AccountInterface
*/
protected $normalUser;
/**
* @var \Drupal\Core\Session\AccountInterface
*/
protected $adminUser;
/**
* @var \Drupal\Core\Config\Config
*/
protected $config;
/**
* @var \Drupal\Core\Entity\ContentEntityInterface
*/
protected $forumTerm;
/**
* @var \Drupal\Core\Entity\ContentEntityInterface
*/
protected $term;
/**
* @var \Drupal\Core\Entity\ContentEntityInterface
*/
protected $node;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->config = $this->config('redirect.settings');
$this->drupalCreateContentType(['type' => 'page', 'name' => 'Page']);
$this->drupalCreateContentType(['type' => 'article', 'name' => 'Article']);
// Create a users for testing the access.
$this->normalUser = $this->drupalCreateUser([
'access content',
'create page content',
'create url aliases',
'access administration pages',
]);
$this->adminUser = $this->drupalCreateUser([
'administer site configuration',
'access administration pages',
'administer languages',
'administer content types',
'administer content translation',
'create page content',
'edit own page content',
'create content translations',
]);
// Save the node.
$this->node = $this->drupalCreateNode([
'type' => 'page',
'title' => 'Test Page Node',
'path' => ['alias' => '/test-node'],
'language' => Language::LANGCODE_NOT_SPECIFIED,
]);
// Create an alias for the create story path - this is used in the
// "redirect with permissions testing" test.
\Drupal::service('path.alias_storage')->save('/admin/config/system/site-information', '/site-info');
// Create a taxonomy term for the forum.
$term = entity_create('taxonomy_term', [
'name' => 'Test Forum Term',
'vid' => 'forums',
'langcode' => Language::LANGCODE_NOT_SPECIFIED,
]);
$term->save();
$this->forumTerm = $term;
// Create another taxonomy vocabulary with a term.
$vocab = entity_create('taxonomy_vocabulary', [
'name' => 'test vocab',
'vid' => 'test-vocab',
'langcode' => Language::LANGCODE_NOT_SPECIFIED,
]);
$vocab->save();
$term = entity_create('taxonomy_term', [
'name' => 'Test Term',
'vid' => $vocab->id(),
'langcode' => Language::LANGCODE_NOT_SPECIFIED,
'path' => ['alias' => '/test-term'],
]);
$term->save();
$this->term = $term;
}
/**
* Will test the redirects.
*/
public function testRedirects() {
// First test that the good stuff can be switched off.
$this->config->set('route_normalizer_enabled', FALSE)->save();
$this->assertRedirect('index.php/node/' . $this->node->id(), NULL, 'HTTP/1.1 200 OK');
$this->assertRedirect('index.php/test-node', NULL, 'HTTP/1.1 200 OK');
$this->assertRedirect('test-node/', NULL, 'HTTP/1.1 200 OK');
$this->assertRedirect('Test-node/', NULL, 'HTTP/1.1 200 OK');
$this->config->set('route_normalizer_enabled', TRUE)->save();
// Test alias normalization.
$this->assertRedirect('node/' . $this->node->id(), 'test-node');
$this->assertRedirect('Test-node', 'test-node');
// Test redirects for non-clean urls.
$this->assertRedirect('index.php/node/' . $this->node->id(), 'test-node');
$this->assertRedirect('index.php/test-node', 'test-node');
// Test deslashing.
$this->assertRedirect('test-node/', 'test-node');
// Test front page redirects.
$this->config('system.site')->set('page.front', '/node')->save();
$this->assertRedirect('node', '<front>');
// Test front page redirects with an alias.
\Drupal::service('path.alias_storage')->save('/node', '/node-alias');
$this->assertRedirect('node-alias', '<front>');
// Test post request.
$this->drupalPost('Test-node', 'application/json', array());
// Does not do a redirect, stays in the same path.
$this->assertEqual(basename($this->getUrl()), 'Test-node');
// Test the access checking.
$this->config->set('access_check', TRUE)->save();
$this->assertRedirect('admin/config/system/site-information', NULL, 'HTTP/1.1 403 Forbidden');
$this->config->set('access_check', FALSE)->save();
// @todo - here it seems that the access check runs prior to our redirecting
// check why so and enable the test.
//$this->assertRedirect('admin/config/system/site-information', 'site-info');
// Test original query string is preserved with alias normalization.
$this->assertRedirect('Test-node?&foo&.bar=baz', 'test-node?&foo&.bar=baz');
// Test alias normalization with trailing ?.
$this->assertRedirect('test-node?', 'test-node');
$this->assertRedirect('Test-node?', 'test-node');
// Test alias normalization still works without trailing ?.
$this->assertRedirect('test-node', NULL, 'HTTP/1.1 200 OK');
$this->assertRedirect('Test-node', 'test-node');
// Login as user with admin privileges.
$this->drupalLogin($this->adminUser);
// Test ignoring admin paths.
$this->config->set('ignore_admin_path', FALSE)->save();
$this->assertRedirect('admin/config/system/site-information', 'site-info');
// Test alias normalization again with ignore_admin_path false.
$this->assertRedirect('Test-node', 'test-node');
$this->config->set('ignore_admin_path', TRUE)->save();
$this->assertRedirect('admin/config/system/site-information', NULL, 'HTTP/1.1 200 OK');
// Test alias normalization again with ignore_admin_path true.
$this->assertRedirect('Test-node', 'test-node');
}
/**
* Test that redirects work properly with content_translation enabled.
*/
public function testLanguageRedirects() {
$this->drupalLogin($this->adminUser);
// Add a new language.
ConfigurableLanguage::createFromLangcode('es')
->save();
// Enable URL language detection and selection.
$edit = ['language_interface[enabled][language-url]' => '1'];
$this->drupalPostForm('admin/config/regional/language/detection', $edit, t('Save settings'));
// Set page content type to use multilingual support.
$edit = [
'language_configuration[language_alterable]' => TRUE,
'language_configuration[content_translation]' => TRUE,
];
$this->drupalPostForm('admin/structure/types/manage/page', $edit, t('Save content type'));
$this->assertRaw(t('The content type %type has been updated.', array('%type' => 'Page')), 'Basic page content type has been updated.');
$spanish_node = $this->drupalCreateNode([
'type' => 'page',
'title' => 'Spanish Test Page Node',
'path' => ['alias' => '/spanish-test-node'],
'langcode' => 'es',
]);
// Test multilingual redirect.
$this->assertRedirect('es/node/' . $spanish_node->id(), 'es/spanish-test-node');
}
/**
* Asserts the redirect from $path to the $expected_ending_url.
*
* @param string $path
* The request path.
* @param $expected_ending_url
* The path where we expect it to redirect. If NULL value provided, no
* redirect is expected.
* @param string $expected_ending_status
* The status we expect to get with the first request.
*/
public function assertRedirect($path, $expected_ending_url, $expected_ending_status = 'HTTP/1.1 301 Moved Permanently') {
$this->drupalHead($GLOBALS['base_url'] . '/' . $path);
$headers = $this->drupalGetHeaders(TRUE);
$ending_url = isset($headers[0]['location']) ? $headers[0]['location'] : NULL;
$message = SafeMarkup::format('Testing redirect from %from to %to. Ending url: %url', array(
'%from' => $path,
'%to' => $expected_ending_url,
'%url' => $ending_url,
));
if ($expected_ending_url == '<front>') {
$expected_ending_url = $GLOBALS['base_url'] . '/';
}
elseif (!empty($expected_ending_url)) {
$expected_ending_url = $GLOBALS['base_url'] . '/' . $expected_ending_url;
}
else {
$expected_ending_url = NULL;
}
$this->assertEqual($expected_ending_url, $ending_url);
$this->assertEqual($headers[0][':status'], $expected_ending_status);
}
}
@@ -0,0 +1,94 @@
<?php
namespace Drupal\redirect\Tests;
use Drupal\language\Entity\ConfigurableLanguage;
/**
* UI tests for redirect module with language and content translation modules.
*
* This runs the exact same tests as RedirectUITest, but with both the language
* and content translation modules enabled.
*
* @group redirect
*/
class RedirectUILanguageTest extends RedirectUITest {
/**
* {@inheritdoc}
*/
public static $modules = ['redirect', 'node', 'path', 'dblog', 'views', 'taxonomy', 'language', 'content_translation'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$language = ConfigurableLanguage::createFromLangcode('de');
$language->save();
$language = ConfigurableLanguage::createFromLangcode('es');
$language->save();
}
/**
* Test multilingual scenarios.
*/
public function testLanguageSpecificRedirects() {
$this->drupalLogin($this->adminUser);
$this->drupalGet('admin/config/search/redirect/add');
$this->assertOption('edit-language-0-value', 'en');
$this->assertOption('edit-language-0-value', 'de');
$this->assertOption('edit-language-0-value', 'es');
$this->assertOption('edit-language-0-value', 'und');
$this->assertNoOption('edit-language-0-value', 'zxx');
$this->assertOptionByText('edit-language-0-value', 'English');
$this->assertOptionByText('edit-language-0-value', 'German');
$this->assertOptionByText('edit-language-0-value', 'Spanish');
$this->assertOptionByText('edit-language-0-value', '- All languages -');
// Add a redirect for english.
$this->drupalPostForm('admin/config/search/redirect/add', array(
'redirect_source[0][path]' => 'langpath',
'redirect_redirect[0][uri]' => '/user',
'language[0][value]' => 'en',
), t('Save'));
// Add a redirect for germany.
$this->drupalPostForm('admin/config/search/redirect/add', array(
'redirect_source[0][path]' => 'langpath',
'redirect_redirect[0][uri]' => '<front>',
'language[0][value]' => 'de',
), t('Save'));
// Check redirect for english.
$this->assertRedirect('langpath', '/user', 'HTTP/1.1 301 Moved Permanently');
// Check redirect for germany.
$this->assertRedirect('de/langpath', '/de', 'HTTP/1.1 301 Moved Permanently');
// Check no redirect for spanish.
$this->assertRedirect('es/langpath', NULL, 'HTTP/1.1 404 Not Found');
}
/**
* Test non-language specific redirect.
*/
public function testUndefinedLangugageRedirects() {
$this->drupalLogin($this->adminUser);
// Add a redirect for english.
$this->drupalPostForm('admin/config/search/redirect/add', array(
'redirect_source[0][path]' => 'langpath',
'redirect_redirect[0][uri]' => '/user',
'language[0][value]' => 'und',
), t('Save'));
// Check redirect for english.
$this->assertRedirect('langpath', '/user', 'HTTP/1.1 301 Moved Permanently');
// Check redirect for spanish.
$this->assertRedirect('es/langpath', '/es/user', 'HTTP/1.1 301 Moved Permanently');
}
}
@@ -0,0 +1,437 @@
<?php
namespace Drupal\redirect\Tests;
use Drupal\Component\Utility\Unicode;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Language\Language;
use Drupal\Core\Logger\RfcLogLevel;
use Drupal\Core\Url;
use Drupal\simpletest\WebTestBase;
/**
* UI tests for redirect module.
*
* @group redirect
*/
class RedirectUITest extends WebTestBase {
use AssertRedirectTrait;
/**
* @var \Drupal\Core\Session\AccountInterface
*/
protected $adminUser;
/**
* @var \Drupal\redirect\RedirectRepository
*/
protected $repository;
/**
* @var \Drupal\Core\Entity\Sql\SqlContentEntityStorage
*/
protected $storage;
/**
* {@inheritdoc}
*/
public static $modules = ['redirect', 'node', 'path', 'dblog', 'views', 'taxonomy'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->drupalCreateContentType(array('type' => 'article', 'name' => 'Article'));
$this->adminUser = $this->drupalCreateUser(array(
'administer redirects',
'administer redirect settings',
'access content',
'bypass node access',
'create url aliases',
'administer taxonomy',
'administer url aliases',
));
$this->repository = \Drupal::service('redirect.repository');
$this->storage = $this->container->get('entity.manager')->getStorage('redirect');
}
/**
* Test the redirect UI.
*/
public function testRedirectUI() {
$this->drupalLogin($this->adminUser);
// Test populating the redirect form with predefined values.
$this->drupalGet('admin/config/search/redirect/add', array('query' => array(
'source' => 'non-existing',
'source_query' => array('key' => 'val', 'key1' => 'val1'),
'redirect' => 'node',
'redirect_options' => array('query' => array('key' => 'val', 'key1' => 'val1')),
)));
$this->assertFieldByName('redirect_source[0][path]', 'non-existing?key=val&key1=val1');
$this->assertFieldByName('redirect_redirect[0][uri]', '/node?key=val&key1=val1');
// Test creating a new redirect via UI.
$this->drupalPostForm('admin/config/search/redirect/add', array(
'redirect_source[0][path]' => 'non-existing',
'redirect_redirect[0][uri]' => '/node',
), t('Save'));
// Try to find the redirect we just created.
$redirect = $this->repository->findMatchingRedirect('non-existing');
$this->assertEqual($redirect->getSourceUrl(), Url::fromUri('base:non-existing')->toString());
$this->assertEqual($redirect->getRedirectUrl()->toString(), Url::fromUri('base:node')->toString());
// After adding the redirect we should end up in the list. Check if the
// redirect is listed.
$this->assertUrl('admin/config/search/redirect');
$this->assertText('non-existing');
$this->assertLink(Url::fromUri('base:node')->toString());
$this->assertText(t('Not specified'));
// Test the edit form and update action.
$this->clickLink(t('Edit'));
$this->assertFieldByName('redirect_source[0][path]', 'non-existing');
$this->assertFieldByName('redirect_redirect[0][uri]', '/node');
$this->assertFieldByName('status_code', $redirect->getStatusCode());
// Append a query string to see if we handle query data properly.
$this->drupalPostForm(NULL, array(
'redirect_source[0][path]' => 'non-existing?key=value',
), t('Save'));
// Check the location after update and check if the value has been updated
// in the list.
$this->assertUrl('admin/config/search/redirect');
$this->assertText('non-existing?key=value');
// The path field should not contain the query string and therefore we
// should be able to load the redirect using only the url part without
// query.
$this->storage->resetCache();
$redirects = $this->repository->findBySourcePath('non-existing');
$redirect = array_shift($redirects);
$this->assertEqual($redirect->getSourceUrl(), Url::fromUri('base:non-existing', ['query' => ['key' => 'value']])->toString());
// Test the source url hints.
// The hint about an existing base path.
$this->drupalPostAjaxForm('admin/config/search/redirect/add', array(
'redirect_source[0][path]' => 'non-existing?key=value',
), 'redirect_source[0][path]');
$this->assertRaw(t('The base source path %source is already being redirected. Do you want to <a href="@edit-page">edit the existing redirect</a>?',
array('%source' => 'non-existing?key=value', '@edit-page' => $redirect->url('edit-form'))));
// The hint about a valid path.
$this->drupalPostAjaxForm('admin/config/search/redirect/add', array(
'redirect_source[0][path]' => 'node',
), 'redirect_source[0][path]');
$this->assertRaw(t('The source path %path is likely a valid path. It is preferred to <a href="@url-alias">create URL aliases</a> for existing paths rather than redirects.',
array('%path' => 'node', '@url-alias' => Url::fromRoute('path.admin_add')->toString())));
// Test validation.
// Duplicate redirect.
$this->drupalPostForm('admin/config/search/redirect/add', array(
'redirect_source[0][path]' => 'non-existing?key=value',
'redirect_redirect[0][uri]' => '/node',
), t('Save'));
$this->assertRaw(t('The source path %source is already being redirected. Do you want to <a href="@edit-page">edit the existing redirect</a>?',
array('%source' => 'non-existing?key=value', '@edit-page' => $redirect->url('edit-form'))));
// Redirecting to itself.
$this->drupalPostForm('admin/config/search/redirect/add', array(
'redirect_source[0][path]' => 'node',
'redirect_redirect[0][uri]' => '/node',
), t('Save'));
$this->assertRaw(t('You are attempting to redirect the page to itself. This will result in an infinite loop.'));
// Redirecting the front page.
$this->drupalPostForm('admin/config/search/redirect/add', array(
'redirect_source[0][path]' => '<front>',
'redirect_redirect[0][uri]' => '/node',
), t('Save'));
$this->assertRaw(t('It is not allowed to create a redirect from the front page.'));
// Redirecting a url with fragment.
$this->drupalPostForm('admin/config/search/redirect/add', array(
'redirect_source[0][path]' => 'page-to-redirect#content',
'redirect_redirect[0][uri]' => '/node',
), t('Save'));
$this->assertRaw(t('The anchor fragments are not allowed.'));
// Adding path that starts with /
$this->drupalPostForm('admin/config/search/redirect/add', array(
'redirect_source[0][path]' => '/page-to-redirect',
'redirect_redirect[0][uri]' => '/node',
), t('Save'));
$this->assertRaw(t('The url to redirect from should not start with a forward slash (/).'));
// Test filters.
// Add a new redirect.
$this->drupalPostForm('admin/config/search/redirect/add', array(
'redirect_source[0][path]' => 'test27',
'redirect_redirect[0][uri]' => '/node',
), t('Save'));
// Filter with non existing value.
$this->drupalGet('admin/config/search/redirect', array(
'query' => array(
'status_code' => '3',
),
));
$rows = $this->xpath('//tbody/tr');
// Check if the list has no rows.
$this->assertTrue(count($rows) == 0);
// Filter with existing values.
$this->drupalGet('admin/config/search/redirect', array(
'query' => array(
'redirect_source__path' => 'test',
'status_code' => '2',
),
));
$rows = $this->xpath('//tbody/tr');
// Check if the list has 1 row.
$this->assertTrue(count($rows) == 1);
$this->drupalGet('admin/config/search/redirect', array(
'query' => array(
'redirect_redirect__uri' => 'nod',
),
));
$rows = $this->xpath('//tbody/tr');
// Check if the list has 2 rows.
$this->assertTrue(count($rows) == 2);
// Test the plural form of the bulk delete action.
$this->drupalGet('admin/config/search/redirect');
$edit = [
'redirect_bulk_form[0]' => TRUE,
'redirect_bulk_form[1]' => TRUE,
];
$this->drupalPostForm(NULL, $edit, t('Apply to selected items'));
$this->assertText('Are you sure you want to delete these redirects?');
$this->clickLink('Cancel');
// Test the delete action.
$this->clickLink(t('Delete'));
$this->assertRaw(t('Are you sure you want to delete the URL redirect from %source to %redirect?',
array('%source' => Url::fromUri('base:non-existing', ['query' => ['key' => 'value']])->toString(), '%redirect' => Url::fromUri('base:node')->toString())));
$this->drupalPostForm(NULL, array(), t('Delete'));
$this->assertUrl('admin/config/search/redirect');
// Test the bulk delete action.
$this->drupalPostForm(NULL, ['redirect_bulk_form[0]' => TRUE], t('Apply to selected items'));
$this->assertText('Are you sure you want to delete this redirect?');
$this->assertText('test27');
$this->drupalPostForm(NULL, [], t('Delete'));
$this->assertText(t('There is no redirect yet.'));
}
/**
* Tests redirects being automatically created upon path alias change.
*/
public function testAutomaticRedirects() {
$this->drupalLogin($this->adminUser);
// Create a node and update its path alias which should result in a redirect
// being automatically created from the old alias to the new one.
$node = $this->drupalCreateNode(array(
'type' => 'article',
'langcode' => Language::LANGCODE_NOT_SPECIFIED,
'path' => array('alias' => '/node_test_alias'),
));
$this->drupalGet('node/' . $node->id() . '/edit');
$this->assertText(t('No URL redirects available.'));
$this->drupalPostForm('node/' . $node->id() . '/edit', array('path[0][alias]' => '/node_test_alias_updated'), t('Save'));
$redirect = $this->repository->findMatchingRedirect('node_test_alias', array(), Language::LANGCODE_NOT_SPECIFIED);
$this->assertEqual($redirect->getRedirectUrl()->toString(), Url::fromUri('base:node_test_alias_updated')->toString());
// Test if the automatically created redirect works.
$this->assertRedirect('node_test_alias', 'node_test_alias_updated');
// Test that changing the path back deletes the first redirect, creates
// a new one and does not result in a loop.
$this->drupalPostForm('node/' . $node->id() . '/edit', array('path[0][alias]' => '/node_test_alias'), t('Save'));
$redirect = $this->repository->findMatchingRedirect('node_test_alias', array(), Language::LANGCODE_NOT_SPECIFIED);
$this->assertTrue(empty($redirect));
\Drupal::service('path.alias_manager')->cacheClear();
$redirect = $this->repository->findMatchingRedirect('node_test_alias_updated', array(), Language::LANGCODE_NOT_SPECIFIED);
$this->drupalGet('node/' . $node->id() . '/edit');
$this->assertText($redirect->getSourcePathWithQuery());
$this->assertLinkByHref(Url::fromRoute('entity.redirect.edit_form', ['redirect' => $redirect->id()])->toString());
$this->assertLinkByHref(Url::fromRoute('entity.redirect.delete_form', ['redirect' => $redirect->id()])->toString());
$this->assertEqual($redirect->getRedirectUrl()->toString(), Url::fromUri('base:node_test_alias')->toString());
// Test if the automatically created redirect works.
$this->assertRedirect('node_test_alias_updated', 'node_test_alias');
// Test that the redirect will be deleted upon node deletion.
$this->drupalPostForm('node/' . $node->id() . '/delete', array(), t('Delete'));
$redirect = $this->repository->findMatchingRedirect('node_test_alias_updated', array(), Language::LANGCODE_NOT_SPECIFIED);
$this->assertTrue(empty($redirect));
// Create a term and update its path alias and check if we have a redirect
// from the previous path alias to the new one.
$term = $this->createTerm($this->createVocabulary());
$this->drupalPostForm('taxonomy/term/' . $term->id() . '/edit', array('path[0][alias]' => '/term_test_alias_updated'), t('Save'));
$redirect = $this->repository->findMatchingRedirect('term_test_alias');
$this->assertEqual($redirect->getRedirectUrl()->toString(), Url::fromUri('base:term_test_alias_updated')->toString());
// Test if the automatically created redirect works.
$this->assertRedirect('term_test_alias', 'term_test_alias_updated');
// Test the path alias update via the admin path form.
$this->drupalPostForm('admin/config/search/path/add', array(
'source' => '/node',
'alias' => '/aaa_path_alias',
), t('Save'));
// Note that here we rely on fact that we land on the path alias list page
// and the default sort is by the alias, which implies that the first edit
// link leads to the edit page of the aaa_path_alias.
$this->clickLink(t('Edit'));
$this->drupalPostForm(NULL, array('alias' => '/aaa_path_alias_updated'), t('Save'));
$redirect = $this->repository->findMatchingRedirect('aaa_path_alias', array(), 'en');
$this->assertEqual($redirect->getRedirectUrl()->toString(), Url::fromUri('base:aaa_path_alias_updated')->toString());
// Test if the automatically created redirect works.
$this->assertRedirect('aaa_path_alias', 'aaa_path_alias_updated');
// Test the automatically created redirect shows up in the form correctly.
$this->drupalGet('admin/config/search/redirect/edit/' . $redirect->id());
$this->assertFieldByName('redirect_source[0][path]', 'aaa_path_alias');
$this->assertFieldByName('redirect_redirect[0][uri]', '/node');
}
/**
* Test the redirect loop protection and logging.
*/
function testRedirectLoop() {
// Redirect loop redirection only works when page caching is disabled.
\Drupal::service('module_installer')->uninstall(['page_cache']);
/** @var \Drupal\redirect\Entity\Redirect $redirect1 */
$redirect1 = $this->storage->create();
$redirect1->setSource('node');
$redirect1->setRedirect('admin');
$redirect1->setStatusCode(301);
$redirect1->save();
/** @var \Drupal\redirect\Entity\Redirect $redirect2 */
$redirect2 = $this->storage->create();
$redirect2->setSource('admin');
$redirect2->setRedirect('node');
$redirect2->setStatusCode(301);
$redirect2->save();
$this->maximumRedirects = 10;
$this->drupalGet('node');
$this->assertText('Service unavailable');
$this->assertResponse(503);
$log = db_select('watchdog')->fields('watchdog')->condition('type', 'redirect')->execute()->fetchAll();
if (count($log) == 0) {
$this->fail('Redirect loop has not been logged');
}
else {
$log = reset($log);
$this->assertEqual($log->severity, RfcLogLevel::WARNING);
$this->assertEqual(SafeMarkup::format($log->message, unserialize($log->variables)),
SafeMarkup::format('Redirect loop identified at %path for redirect %id', array('%path' => '/node', '%id' => $redirect1->id())));
}
}
/**
* Returns a new vocabulary with random properties.
*/
function createVocabulary() {
// Create a vocabulary.
$vocabulary = entity_create('taxonomy_vocabulary', array(
'name' => $this->randomMachineName(),
'description' => $this->randomMachineName(),
'vid' => Unicode::strtolower($this->randomMachineName()),
'langcode' => Language::LANGCODE_NOT_SPECIFIED,
'weight' => mt_rand(0, 10),
));
$vocabulary->save();
return $vocabulary;
}
/**
* Returns a new term with random properties in vocabulary $vid.
*/
function createTerm($vocabulary) {
$filter_formats = filter_formats();
$format = array_pop($filter_formats);
$term = entity_create('taxonomy_term', array(
'name' => $this->randomMachineName(),
'description' => array(
'value' => $this->randomMachineName(),
// Use the first available text format.
'format' => $format->id(),
),
'vid' => $vocabulary->id(),
'langcode' => Language::LANGCODE_NOT_SPECIFIED,
'path' => array('alias' => '/term_test_alias'),
));
$term->save();
return $term;
}
/**
* Test cache tags.
*
* @todo Not sure this belongs in a UI test, but a full web test is needed.
*/
public function testCacheTags() {
/** @var \Drupal\redirect\Entity\Redirect $redirect1 */
$redirect1 = $this->storage->create();
$redirect1->setSource('test-redirect');
$redirect1->setRedirect('node');
$redirect1->setStatusCode(301);
$redirect1->save();
$this->assertRedirect('test-redirect', 'node');
$headers = $this->drupalGetHeaders(TRUE);
// Note, self::assertCacheTag() cannot be used here since it only looks at
// the final set of headers.
$expected = 'http_response ' . implode(' ', $redirect1->getCacheTags());
$this->assertEqual($expected, $headers[0]['x-drupal-cache-tags'], 'Redirect cache tags properly set.');
// First request should be a cache MISS.
$this->assertEqual($headers[0]['x-drupal-cache'], 'MISS', 'First request to the redirect was not cached.');
// Second request should be cached.
$this->assertRedirect('test-redirect', 'node');
$headers = $this->drupalGetHeaders(TRUE);
$this->assertEqual($headers[0]['x-drupal-cache'], 'HIT', 'The second request to the redirect was cached.');
// Ensure that the redirect has been cleared from cache when deleted.
$redirect1->delete();
$this->drupalGet('test-redirect');
$this->assertResponse(404, 'Deleted redirect properly clears the internal page cache.');
}
/**
* Test external destinations.
*/
public function testExternal() {
$redirect = $this->storage->create();
$redirect->setSource('a-path');
// @todo Redirect::setRedirect() assumes that all redirects are internal.
$redirect->redirect_redirect->set(0, ['uri' => 'https://www.example.org']);
$redirect->setStatusCode(301);
$redirect->save();
$this->assertRedirect('a-path', 'https://www.example.org');
$this->drupalLogin($this->adminUser);
}
}
@@ -0,0 +1,187 @@
<?php
/**
* @file
* A database agnostic dump for testing purposes.
*/
use Drupal\Core\Database\Database;
$connection = Database::getConnection();
$connection->schema()->createTable('path_redirect', array(
'fields' => array(
'rid' => array(
'type' => 'serial',
'not null' => TRUE,
'size' => 'normal',
),
'source' => array(
'type' => 'varchar',
'length' => 255,
'not null' => TRUE,
),
'redirect' => array(
'type' => 'varchar',
'length' => 255,
'not null' => TRUE,
),
'query' => array(
'type' => 'varchar',
'length' => 255,
'not null' => FALSE,
),
'fragment' => array(
'type' => 'varchar',
'length' => 50,
'not null' => FALSE,
),
'language' => array(
'type' => 'varchar',
'length' => 12,
'not null' => TRUE,
'default' => '',
),
'type' => array(
'type' => 'int',
'size' => 'small',
'not null' => TRUE,
),
'last_used' => array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
),
),
'primary key' => array('rid'),
'unique keys' => array('source_language' => array('source', 'language')),
'mysql_character_set' => 'utf8',
));
$connection->insert('path_redirect')
->fields(array(
'rid',
'source',
'redirect',
'query',
'fragment',
'language',
'type',
'last_used',
))
->values(array(
'rid' => 5,
'source' => 'test/source/url',
'redirect' => 'test/redirect/url',
'query' => NULL,
'fragment' => NULL,
'language' => '',
'type' => 301,
'last_used' => 1449497138,
))
->values(array(
'rid' => 7,
'source' => 'test/source/url2',
'redirect' => 'http://test/external/redirect/url',
'query' => 'foo=bar&biz=buz',
'fragment' => NULL,
'language' => 'en',
'type' => 302,
'last_used' => 1449497139,
))
->execute();
$connection->schema()->createTable('system', array(
'fields' => array(
'filename' => array(
'type' => 'varchar',
'not null' => TRUE,
'length' => '255',
'default' => '',
),
'name' => array(
'type' => 'varchar',
'not null' => TRUE,
'length' => '255',
'default' => '',
),
'type' => array(
'type' => 'varchar',
'not null' => TRUE,
'length' => '255',
'default' => '',
),
'owner' => array(
'type' => 'varchar',
'not null' => TRUE,
'length' => '255',
'default' => '',
),
'status' => array(
'type' => 'int',
'not null' => TRUE,
'size' => 'normal',
'default' => '0',
),
'throttle' => array(
'type' => 'int',
'not null' => TRUE,
'size' => 'normal',
'default' => '0',
),
'bootstrap' => array(
'type' => 'int',
'not null' => TRUE,
'size' => 'normal',
'default' => '0',
),
'schema_version' => array(
'type' => 'int',
'not null' => TRUE,
'size' => 'normal',
'default' => '-1',
),
'weight' => array(
'type' => 'int',
'not null' => TRUE,
'size' => 'normal',
'default' => '0',
),
'info' => array(
'type' => 'text',
'not null' => FALSE,
'size' => 'normal',
),
),
'primary key' => array(
'filename',
),
'mysql_character_set' => 'utf8',
));
$connection->insert('system')
->fields(array(
'filename',
'name',
'type',
'owner',
'status',
'bootstrap',
'schema_version',
'weight',
'info',
))
->values(array(
'filename' => 'modules/contrib/redirect/redirect.module',
'name' => 'redirect',
'type' => 'module',
'owner' => '',
'status' => '1',
'bootstrap' => '0',
'schema_version' => '7000',
'weight' => '0',
'info' => 'a:10:{s:4:"name";s:8:"Redirect";s:11:"description";s:51:"Allows users to redirect from old URLs to new URLs.";s:7:"package";s:5:"Other";s:7:"version";s:3:"6.0";s:4:"core";s:3:"6.x";s:7:"project";s:8:"redirect";s:9:"datestamp";s:10:"1347989995";s:12:"dependencies";a:0:{}s:10:"dependents";a:0:{}s:3:"php";s:5:"4.3.5";}',
))
->execute();
@@ -0,0 +1,240 @@
<?php
/**
* @file
* A database agnostic dump for testing purposes.
*/
use Drupal\Core\Database\Database;
$connection = Database::getConnection();
$connection->schema()->createTable('redirect', array(
'fields' => array(
'rid' => array(
'type' => 'serial',
'not null' => TRUE,
'size' => 'normal',
),
'hash' => array(
'type' => 'varchar',
'length' => 64,
'not null' => TRUE,
),
'type' => array(
'type' => 'varchar',
'length' => 64,
'not null' => TRUE,
),
'uid' => array(
'type' => 'int',
'not null' => TRUE,
),
'source' => array(
'type' => 'varchar',
'length' => 255,
'not null' => TRUE,
),
'source_options' => array(
'type' => 'text',
'not null' => TRUE,
),
'redirect' => array(
'type' => 'varchar',
'length' => 255,
'not null' => TRUE,
),
'redirect_options' => array(
'type' => 'text',
'not null' => TRUE,
),
'language' => array(
'type' => 'varchar',
'length' => 12,
'not null' => TRUE,
'default' => '',
),
'status_code' => array(
'type' => 'int',
'size' => 'small',
'not null' => TRUE,
),
'count' => array(
'type' => 'int',
'not null' => TRUE,
),
'access' => array(
'type' => 'int',
'not null' => TRUE,
),
),
'primary key' => array('rid'),
'unique keys' => array(
'source_language' => array('source', 'language'),
'expires' => array('type', 'access')
),
'mysql_character_set' => 'utf8',
));
$connection->insert('redirect')
->fields(array(
'rid',
'hash',
'type',
'uid',
'source',
'source_options',
'redirect',
'redirect_options',
'language',
'status_code',
'count',
'access',
))
->values(array(
'rid' => 5,
'hash' => 'MwmDbnA65ag646gtEdLqmAqTbF0qQerse63RkQmJK_Y',
'type' => 'redirect',
'uid' => 5,
'source' => 'test/source/url',
'source_options' => '',
'redirect' => 'test/redirect/url',
'redirect_options' => '',
'language' => 'und',
'status_code' => 301,
'count' => 2518,
'access' => 1449497138,
))
->values(array(
'rid' => 7,
'hash' => 'GvD5bBB71W8qBvp9I9hHmbSoqZfTvUz0mIkEWjlP8M4',
'type' => 'redirect',
'uid' => 6,
'source' => 'test/source/url2',
'source_options' => '',
'redirect' => 'http://test/external/redirect/url',
'redirect_options' => 'a:2:{s:5:"query";a:2:{s:3:"foo";s:3:"bar";s:3:"biz";s:3:"buz";}s:8:"fragment";s:10:"fragment-1";}',
'language' => 'und',
'status_code' => 0,
'count' => 419,
'access' => 1449497139,
))
->execute();
$connection->schema()->createTable('variable', array(
'fields' => array(
'name' => array(
'type' => 'varchar',
'not null' => TRUE,
'length' => '128',
'default' => '',
),
'value' => array(
'type' => 'blob',
'not null' => TRUE,
'size' => 'normal',
),
),
'primary key' => array(
'name',
),
'mysql_character_set' => 'utf8',
));
$connection->insert('variable')
->fields(array(
'name',
'value',
))
->values(array(
'name' => 'redirect_default_status_code',
'value' => 's:3:"307";',
))
->execute();
$connection->schema()->createTable('system', array(
'fields' => array(
'filename' => array(
'type' => 'varchar',
'not null' => TRUE,
'length' => '255',
'default' => '',
),
'name' => array(
'type' => 'varchar',
'not null' => TRUE,
'length' => '255',
'default' => '',
),
'type' => array(
'type' => 'varchar',
'not null' => TRUE,
'length' => '12',
'default' => '',
),
'owner' => array(
'type' => 'varchar',
'not null' => TRUE,
'length' => '255',
'default' => '',
),
'status' => array(
'type' => 'int',
'not null' => TRUE,
'size' => 'normal',
'default' => '0',
),
'bootstrap' => array(
'type' => 'int',
'not null' => TRUE,
'size' => 'normal',
'default' => '0',
),
'schema_version' => array(
'type' => 'int',
'not null' => TRUE,
'size' => 'normal',
'default' => '-1',
),
'weight' => array(
'type' => 'int',
'not null' => TRUE,
'size' => 'normal',
'default' => '0',
),
'info' => array(
'type' => 'blob',
'not null' => FALSE,
'size' => 'normal',
),
),
'primary key' => array(
'filename',
),
'mysql_character_set' => 'utf8',
));
$connection->insert('system')
->fields(array(
'filename',
'name',
'type',
'owner',
'status',
'bootstrap',
'schema_version',
'weight',
'info',
))
->values(array(
'filename' => 'modules/contrib/redirect/redirect.module',
'name' => 'redirect',
'type' => 'module',
'owner' => '',
'status' => '1',
'bootstrap' => '0',
'schema_version' => '7000',
'weight' => '0',
'info' => 'a:13:{s:4:"name";s:8:"Redirect";s:11:"description";s:51:"Allows users to redirect from old URLs to new URLs.";s:4:"core";s:3:"7.x";s:5:"files";a:11:{i:0;s:15:"redirect.module";i:1;s:18:"redirect.admin.inc";i:2;s:16:"redirect.install";i:3;s:13:"redirect.test";i:4;s:24:"views/redirect.views.inc";i:5;s:47:"views/redirect_handler_filter_redirect_type.inc";i:6;s:48:"views/redirect_handler_field_redirect_source.inc";i:7;s:50:"views/redirect_handler_field_redirect_redirect.inc";i:8;s:52:"views/redirect_handler_field_redirect_operations.inc";i:9;s:51:"views/redirect_handler_field_redirect_link_edit.inc";i:10;s:53:"views/redirect_handler_field_redirect_link_delete.inc";}s:9:"configure";s:37:"admin/config/search/redirect/settings";s:7:"version";s:11:"7.x-1.0-rc1";s:7:"project";s:8:"redirect";s:9:"datestamp";s:10:"1347989995";s:5:"mtime";i:1347989995;s:12:"dependencies";a:0:{}s:7:"package";s:5:"Other";s:3:"php";s:5:"5.2.4";s:9:"bootstrap";i:0;}',
))
->execute();
@@ -0,0 +1,50 @@
<?php
namespace Drupal\Tests\redirect\Kernel\Migrate\d6;
use Drupal\redirect\Entity\Redirect;
use Drupal\Tests\migrate_drupal\Kernel\MigrateDrupalTestBase;
/**
* Tests the d6_path_redirect source plugin.
*
* @group redirect
*/
class PathRedirectTest extends MigrateDrupalTestBase {
/**
* {@inheritdoc}
*/
public static $modules = array('redirect', 'link');
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installSchema('system', array('router'));
$this->installEntitySchema('redirect');
$this->loadFixture( __DIR__ . '/../../../../../tests/fixtures/drupal6.php');
$this->executeMigrations(['d6_path_redirect']);
}
/**
* Tests the Drupal 6 path redirect to Drupal 8 migration.
*/
public function testPathRedirect() {
/** @var Redirect $redirect */
$redirect = Redirect::load(5);
$this->assertSame($this->getMigration('d6_path_redirect')
->getIdMap()
->lookupDestinationID(array(5)), array($redirect->id()));
$this->assertSame("/test/source/url", $redirect->getSourceUrl());
$this->assertSame("base:test/redirect/url", $redirect->getRedirectUrl()->toUriString());
$redirect = Redirect::load(7);
$this->assertSame("/test/source/url2", $redirect->getSourceUrl());
$this->assertSame("http://test/external/redirect/url?foo=bar&biz=buz", $redirect->getRedirectUrl()->toUriString());
}
}
@@ -0,0 +1,68 @@
<?php
/**
* @file
* Contains \Drupal\Tests\redirect\Kernel\Migrate\d7\PathRedirectTest.
*/
namespace Drupal\Tests\redirect\Kernel\Migrate\d7;
use Drupal\redirect\Entity\Redirect;
use Drupal\Tests\migrate_drupal\Kernel\MigrateDrupalTestBase;
/**
* Tests the d7_path_redirect source plugin.
*
* @group redirect
*/
class PathRedirectTest extends MigrateDrupalTestBase {
/**
* {@inheritdoc}
*/
public static $modules = array('redirect', 'link');
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installEntitySchema('redirect');
$this->loadFixture(__DIR__ . '/../../../../fixtures/drupal7.php');
$this->executeMigration('d7_path_redirect');
}
/**
* Asserts various aspects of a redirect entity.
*
* @param int $id
* The entity ID in the form ENTITY_TYPE.BUNDLE.FIELD_NAME.
* @param string $source_url
* The expected source url.
* @param string $redirect_url
* The expected redirect url.
* @param string $status_code
* The expected status code.
*/
protected function assertEntity($id, $source_url, $redirect_url, $status_code) {
/** @var Redirect $redirect */
$redirect = Redirect::load($id);
$this->assertSame($this->getMigration('d7_path_redirect')
->getIdMap()
->lookupDestinationID([$id]), [$redirect->id()]);
$this->assertSame($source_url, $redirect->getSourceUrl());
$this->assertSame($redirect_url, $redirect->getRedirectUrl()
->toUriString());
$this->assertSame($status_code, $redirect->getStatusCode());
}
/**
* Tests the Drupal 7 path redirect to Drupal 8 migration.
*/
public function testPathRedirect() {
$this->assertEntity(5, '/test/source/url', 'base:test/redirect/url', '301');
$this->assertEntity(7, '/test/source/url2', 'http://test/external/redirect/url?foo=bar&biz=buz#fragment-1', '307');
}
}
@@ -0,0 +1,292 @@
<?php
namespace Drupal\Tests\redirect\Kernel;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\redirect\Entity\Redirect;
use Drupal\Core\Language\Language;
use Drupal\redirect\Exception\RedirectLoopException;
use Drupal\KernelTests\KernelTestBase;
/**
* Redirect entity and redirect API test coverage.
*
* @group redirect
*/
class RedirectAPITest extends KernelTestBase {
/**
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $controller;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('redirect', 'link', 'field', 'system', 'user', 'language', 'views');
/**
* {@inheritdoc}
*/
public function setUp() {
parent::setUp();
$this->installEntitySchema('redirect');
$this->installEntitySchema('user');
$this->installSchema('system', ['router']);
$this->installConfig(array('redirect'));
$language = ConfigurableLanguage::createFromLangcode('de');
$language->save();
$this->controller = $this->container->get('entity.manager')->getStorage('redirect');
}
/**
* Test redirect entity logic.
*/
public function testRedirectEntity() {
// Create a redirect and test if hash has been generated correctly.
/** @var \Drupal\redirect\Entity\Redirect $redirect */
$redirect = $this->controller->create();
$redirect->setSource('some-url', array('key' => 'val'));
$redirect->setRedirect('node');
$redirect->save();
$this->assertEquals(Redirect::generateHash('some-url', array('key' => 'val'), Language::LANGCODE_NOT_SPECIFIED), $redirect->getHash());
// Update the redirect source query and check if hash has been updated as
// expected.
$redirect->setSource('some-url', array('key1' => 'val1'));
$redirect->save();
$this->assertEqual(Redirect::generateHash('some-url', array('key1' => 'val1'), Language::LANGCODE_NOT_SPECIFIED), $redirect->getHash());
// Update the redirect source path and check if hash has been updated as
// expected.
$redirect->setSource('another-url', array('key1' => 'val1'));
$redirect->save();
$this->assertEqual(Redirect::generateHash('another-url', array('key1' => 'val1'), Language::LANGCODE_NOT_SPECIFIED), $redirect->getHash());
// Update the redirect language and check if hash has been updated as
// expected.
$redirect->setLanguage('de');
$redirect->save();
$this->assertEqual(Redirect::generateHash('another-url', array('key1' => 'val1'), 'de'), $redirect->getHash());
// Create a few more redirects to test the select.
for ($i = 0; $i < 5; $i++) {
$redirect = $this->controller->create();
$redirect->setSource($this->randomMachineName());
$redirect->save();
}
/** @var \Drupal\redirect\RedirectRepository $repository */
$repository = \Drupal::service('redirect.repository');
$redirect = $repository->findMatchingRedirect('another-url', array('key1' => 'val1'), 'de');
if (!empty($redirect)) {
$this->assertEqual($redirect->getSourceUrl(), '/another-url?key1=val1');
}
else {
$this->fail(t('Failed to find matching redirect.'));
}
// Load the redirect based on url.
$redirects = $repository->findBySourcePath('another-url');
$redirect = array_shift($redirects);
if (!empty($redirect)) {
$this->assertEqual($redirect->getSourceUrl(), '/another-url?key1=val1');
}
else {
$this->fail(t('Failed to find redirect by source path.'));
}
// Test passthrough_querystring.
$redirect = $this->controller->create();
$redirect->setSource('a-different-url');
$redirect->setRedirect('node');
$redirect->save();
$redirect = $repository->findMatchingRedirect('a-different-url', ['key1' => 'val1'], 'de');
if (!empty($redirect)) {
$this->assertEqual($redirect->getSourceUrl(), '/a-different-url');
}
else {
$this->fail('Failed to find redirect by source path with query string.');
}
// Add another redirect to the same path, with a query. This should always
// be found before the source without a query set.
/** @var \Drupal\redirect\Entity\Redirect $new_redirect */
$new_redirect = $this->controller->create();
$new_redirect->setSource('a-different-url', ['foo' => 'bar']);
$new_redirect->setRedirect('node');
$new_redirect->save();
$found = $repository->findMatchingRedirect('a-different-url', ['foo' => 'bar'], 'de');
if (!empty($found)) {
$this->assertEqual($found->getSourceUrl(), '/a-different-url?foo=bar');
}
else {
$this->fail('Failed to find a redirect by source path with query string.');
}
// Hashes should be case-insensitive since the source paths are.
/** @var \Drupal\redirect\Entity\Redirect $redirect */
$redirect = $this->controller->create();
$redirect->setSource('Case-Sensitive-Path');
$redirect->setRedirect('node');
$redirect->save();
$found = $repository->findBySourcePath('case-sensitive-path');
if (!empty($found)) {
$found = reset($found);
$this->assertEqual($found->getSourceUrl(), '/Case-Sensitive-Path');
}
else {
$this->fail('findBySourcePath is case sensitive');
}
$found = $repository->findMatchingRedirect('case-sensitive-path');
if (!empty($found)) {
$this->assertEqual($found->getSourceUrl(), '/Case-Sensitive-Path');
}
else {
$this->fail('findMatchingRedirect is case sensitive.');
}
}
/**
* Test slash is removed from source path in findMatchingRedirect.
*/
public function testDuplicateRedirectEntry() {
$redirect = $this->controller->create();
$redirect->setSource('/foo/foo', []);
$redirect->setRedirect('foo');
$redirect->save();
$redirect_repository = \Drupal::service('redirect.repository');
$matched_redirect = $redirect_repository->findMatchingRedirect('/foo/foo', [], 'en-AU');
$this->assertNotNull($matched_redirect);
$null_redirect = $redirect_repository->findMatchingRedirect('/foo/foo-bar', [], 'en-AU');
$this->assertNull($null_redirect);
}
/**
* Test redirect_sort_recursive().
*/
public function testSortRecursive() {
$test_cases = array(
array(
'input' => array('b' => 'aa', 'c' => array('c2' => 'aa', 'c1' => 'aa'), 'a' => 'aa'),
'expected' => array('a' => 'aa', 'b' => 'aa', 'c' => array('c1' => 'aa', 'c2' => 'aa')),
'callback' => 'ksort',
),
);
foreach ($test_cases as $index => $test_case) {
$output = $test_case['input'];
redirect_sort_recursive($output, $test_case['callback']);
$this->assertIdentical($output, $test_case['expected']);
}
}
/**
* Test loop detection.
*/
public function testLoopDetection() {
// Add a chained redirect that isn't a loop.
/** @var \Drupal\redirect\Entity\Redirect $one */
$one = $this->controller->create();
$one->setSource('my-path');
$one->setRedirect('node');
$one->save();
/** @var \Drupal\redirect\Entity\Redirect $two */
$two = $this->controller->create();
$two->setSource('second-path');
$two->setRedirect('my-path');
$two->save();
/** @var \Drupal\redirect\Entity\Redirect $three */
$three = $this->controller->create();
$three->setSource('third-path');
$three->setRedirect('second-path');
$three->save();
/** @var \Drupal\redirect\RedirectRepository $repository */
$repository = \Drupal::service('redirect.repository');
$found = $repository->findMatchingRedirect('third-path');
if (!empty($found)) {
$this->assertEqual($found->getRedirectUrl()->toString(), '/node', 'Chained redirects properly resolved in findMatchingRedirect.');
}
else {
$this->fail('Failed to resolve a chained redirect.');
}
// Create a loop.
$one->setRedirect('third-path');
$one->save();
try {
$repository->findMatchingRedirect('third-path');
$this->fail('Failed to detect a redirect loop.');
}
catch (RedirectLoopException $e) {
$this->pass('Properly detected a redirect loop.');
}
}
/**
* Test redirect_parse_url().
*/
public function testParseURL() {
//$test_cases = array(
// array(
// 'input' => array('b' => 'aa', 'c' => array('c2' => 'aa', 'c1' => 'aa'), 'a' => 'aa'),
// 'expected' => array('a' => 'aa', 'b' => 'aa', 'c' => array('c1' => 'aa', 'c2' => 'aa')),
// ),
//);
//foreach ($test_cases as $index => $test_case) {
// $output = redirect_parse_url($test_case['input']);
// $this->assertIdentical($output, $test_case['expected']);
//}
}
/**
* Test multilingual redirects.
*/
public function testMultilanguageCases() {
// Add a redirect for english.
/** @var \Drupal\redirect\Entity\Redirect $en_redirect */
$en_redirect = $this->controller->create();
$en_redirect->setSource('langpath');
$en_redirect->setRedirect('/about');
$en_redirect->setLanguage('en');
$en_redirect->save();
// Add a redirect for germany.
/** @var \Drupal\redirect\Entity\Redirect $en_redirect */
$en_redirect = $this->controller->create();
$en_redirect->setSource('langpath');
$en_redirect->setRedirect('node');
$en_redirect->setLanguage('de');
$en_redirect->save();
// Check redirect for english.
/** @var \Drupal\redirect\RedirectRepository $repository */
$repository = \Drupal::service('redirect.repository');
$found = $repository->findBySourcePath('langpath');
if (!empty($found)) {
$this->assertEqual($found[1]->getRedirectUrl()->toString(), '/about', 'Multilingual redirect resolved properly.');
$this->assertEqual($found[1]->get('language')[0]->value, 'en', 'Multilingual redirect resolved properly.');
}
else {
$this->fail('Failed to resolve the multilingual redirect.');
}
// Check redirect for germany.
\Drupal::configFactory()->getEditable('system.site')->set('default_langcode', 'de')->save();
/** @var \Drupal\redirect\RedirectRepository $repository */
$repository = \Drupal::service('redirect.repository');
$found = $repository->findBySourcePath('langpath');
if (!empty($found)) {
$this->assertEqual($found[2]->getRedirectUrl()->toString(), '/node', 'Multilingual redirect resolved properly.');
$this->assertEqual($found[2]->get('language')[0]->value, 'de', 'Multilingual redirect resolved properly.');
}
else {
$this->fail('Failed to resolve the multilingual redirect.');
}
}
}
@@ -0,0 +1,54 @@
<?php
namespace Drupal\Tests\redirect\Unit\Migrate\d7;
use Drupal\Tests\migrate\Unit\MigrateSqlSourceTestCase;
/**
* Tests D7 redirect source plugin.
*
* @group redirect
*/
class PathRedirectTest extends MigrateSqlSourceTestCase {
const PLUGIN_CLASS = 'Drupal\redirect\Plugin\migrate\source\d7\PathRedirect';
protected $migrationConfiguration = [
'id' => 'test',
'source' => [
'plugin' => 'd7_path_redirect',
],
];
protected $expectedResults = [
[
'rid' => 5,
'hash' => 'MwmDbnA65ag646gtEdLqmAqTbF0qQerse63RkQmJK_Y',
'type' => 'redirect',
'uid' => 5,
'source' => 'test/source/url',
'source_options' => '',
'redirect' => 'test/redirect/url',
'redirect_options' => '',
'language' => 'und',
'status_code' => 301,
'count' => 2518,
'access' => 1449497138,
],
];
/**
* {@inheritdoc}
*/
protected function setUp() {
$this->databaseContents['variable'] = [
[
'name' => 'redirect_default_status_code',
'value' => 's:3:"307";',
]
];
$this->databaseContents['redirect'] = $this->expectedResults;
parent::setUp();
}
}
@@ -0,0 +1,155 @@
<?php
namespace Drupal\Tests\redirect\Unit;
use Drupal\redirect\RedirectChecker;
use Drupal\Tests\UnitTestCase;
use PHPUnit_Framework_MockObject_MockObject;
use Symfony\Cmf\Component\Routing\RouteObjectInterface;
use Symfony\Component\HttpFoundation\ParameterBag;
use Symfony\Component\Routing\Route;
/**
* Tests the redirect logic.
*
* @group redirect
*/
class RedirectCheckerTest extends UnitTestCase {
/**
* Tests the can redirect check.
*/
public function testCanRedirect() {
$config = array('redirect.settings' => array('ignore_admin_path' => FALSE, 'access_check' => TRUE));
$state = $this->getMockBuilder('Drupal\Core\State\StateInterface')
->getMock();
$state->expects($this->any())
->method('get')
->with('system.maintenance_mode')
->will($this->returnValue(FALSE));
$access = $this->getMockBuilder('Drupal\Core\Access\AccessManager')
->disableOriginalConstructor()
->getMock();
$account = $this->getMockBuilder('Drupal\Core\Session\AccountInterface')
->getMock();
$route_provider = $this->getMockBuilder('Drupal\Core\Routing\RouteProviderInterface')
->getMock();
$route = new Route('/example');
$route_provider->expects($this->any())
->method('getRouteByName')
->willReturn($route);
$access->expects($this->any())
->method('checkNamedRoute')
->willReturnMap([
['denied_route', [], $account, FALSE, FALSE],
['allowed_route', [], $account, FALSE, TRUE],
]);
$checker = new RedirectChecker($this->getConfigFactoryStub($config), $state, $access, $account, $route_provider);
// All fine - we can redirect.
$request = $this->getRequestStub('index.php', 'GET');
$this->assertTrue($checker->canRedirect($request), 'Can redirect');
// The script name is not index.php.
$request = $this->getRequestStub('statistics.php', 'GET');
$this->assertFalse($checker->canRedirect($request), 'Cannot redirect script name not index.php');
// The request method is not GET.
$request = $this->getRequestStub('index.php', 'POST');
$this->assertFalse($checker->canRedirect($request), 'Cannot redirect other than GET method');
// Route access check, deny access.
$request = $this->getRequestStub('index.php', 'GET');
$this->assertFalse($checker->canRedirect($request, 'denied_route'), 'Can not redirect');
// Route access check, allow access.
$request = $this->getRequestStub('index.php', 'GET');
$this->assertTrue($checker->canRedirect($request, 'allowed_route'), 'Can redirect');
// Check destination parameter.
$request = $this->getRequestStub('index.php', 'GET', [], ['destination' => 'paradise']);
$this->assertFalse($checker->canRedirect($request), 'Cannot redirect');
// Maintenance mode is on.
$state = $this->getMockBuilder('Drupal\Core\State\StateInterface')
->getMock();
$state->expects($this->any())
->method('get')
->with('system.maintenance_mode')
->will($this->returnValue(TRUE));
$checker = new RedirectChecker($this->getConfigFactoryStub($config), $state, $access, $account, $route_provider);
$request = $this->getRequestStub('index.php', 'GET');
$this->assertFalse($checker->canRedirect($request), 'Cannot redirect if maintenance mode is on');
// We are at a admin path.
$state = $this->getMockBuilder('Drupal\Core\State\StateInterface')
->getMock();
$state->expects($this->any())
->method('get')
->with('system.maintenance_mode')
->will($this->returnValue(FALSE));
// $checker = new RedirectChecker($this->getConfigFactoryStub($config), $state);
//
// $route = $this->getMockBuilder('Symfony\Component\Routing\Route')
// ->disableOriginalConstructor()
// ->getMock();
// $route->expects($this->any())
// ->method('getOption')
// ->with('_admin_route')
// ->will($this->returnValue('system.admin_config_search'));
//
// $request = $this->getRequestStub('index.php', 'GET',
// array(RouteObjectInterface::ROUTE_OBJECT => $route));
// $this->assertFalse($checker->canRedirect($request), 'Cannot redirect if we are requesting a admin path');
//
// // We are at admin path with ignore_admin_path set to TRUE.
// $config['redirect.settings']['ignore_admin_path'] = TRUE;
// $checker = new RedirectChecker($this->getConfigFactoryStub($config), $state);
//
// $request = $this->getRequestStub('index.php', 'GET',
// array(RouteObjectInterface::ROUTE_OBJECT => $route));
// $this->assertTrue($checker->canRedirect($request), 'Can redirect a admin with ignore_admin_path set to TRUE');
}
/**
* Gets request mock object.
*
* @param string $script_name
* The result of the getScriptName() method.
* @param string $method
* The request method.
* @param array $attributes
* Attributes to be passed into request->attributes.
* @param array $query
* Query paramter to be passed into request->query.
*
* @return PHPUnit_Framework_MockObject_MockObject
* Mocked request object.
*/
protected function getRequestStub($script_name, $method, array $attributes = [], array $query = []) {
$request = $this->getMockBuilder('Symfony\Component\HttpFoundation\Request')
->disableOriginalConstructor()
->getMock();
$request->expects($this->any())
->method('getScriptName')
->will($this->returnValue($script_name));
$request->expects($this->any())
->method('isMethod')
->with($this->anything())
->will($this->returnValue($method == 'GET'));
$request->query = new ParameterBag($query);
$request->attributes = new ParameterBag($attributes);
return $request;
}
}
@@ -0,0 +1,282 @@
<?php
namespace Drupal\Tests\redirect\Unit;
use Drupal\Core\Language\Language;
use Drupal\redirect\EventSubscriber\RedirectRequestSubscriber;
use Drupal\Tests\UnitTestCase;
use PHPUnit_Framework_MockObject_MockObject;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\Event\PostResponseEvent;
/**
* Tests the redirect logic.
*
* @group redirect
*
* @coversDefaultClass Drupal\redirect\EventSubscriber\RedirectRequestSubscriber
*/
class RedirectRequestSubscriberTest extends UnitTestCase {
/**
* @covers ::onKernelRequestCheckRedirect
* @dataProvider getRedirectData
*/
public function testRedirectLogicWithQueryRetaining($request_uri, $request_query, $redirect_uri, $redirect_query) {
// The expected final query. This query must contain values defined
// by the redirect entity and values from the accessed url.
$final_query = $redirect_query + $request_query;
$url = $this->getMockBuilder('Drupal\Core\Url')
->disableOriginalConstructor()
->getMock();
$url->expects($this->once())
->method('setAbsolute')
->with(TRUE)
->willReturn($url);
$url->expects($this->once())
->method('getOption')
->with('query')
->willReturn($redirect_query);
$url->expects($this->once())
->method('setOption')
->with('query', $final_query);
$url->expects($this->once())
->method('toString')
->willReturn($redirect_uri);
$redirect = $this->getRedirectStub($url);
$event = $this->callOnKernelRequestCheckRedirect($redirect, $request_uri, $request_query, TRUE);
$this->assertTrue($event->getResponse() instanceof RedirectResponse);
$response = $event->getResponse();
$this->assertEquals('/test-path', $response->getTargetUrl());
$this->assertEquals(301, $response->getStatusCode());
$this->assertEquals(1, $response->headers->get('X-Redirect-ID'));
}
/**
* @covers ::onKernelRequestCheckRedirect
* @dataProvider getRedirectData
*/
public function testRedirectLogicWithoutQueryRetaining($request_uri, $request_query, $redirect_uri) {
$url = $this->getMockBuilder('Drupal\Core\Url')
->disableOriginalConstructor()
->getMock();
$url->expects($this->once())
->method('setAbsolute')
->with(TRUE)
->willReturn($url);
// No query retaining, so getOption should not be called.
$url->expects($this->never())
->method('getOption');
$url->expects($this->never())
->method('setOption');
$url->expects($this->once())
->method('toString')
->willReturn($redirect_uri);
$redirect = $this->getRedirectStub($url);
$event = $this->callOnKernelRequestCheckRedirect($redirect, $request_uri, $request_query, FALSE);
$this->assertTrue($event->getResponse() instanceof RedirectResponse);
$response = $event->getResponse();
$this->assertEquals($redirect_uri, $response->getTargetUrl());
$this->assertEquals(301, $response->getStatusCode());
$this->assertEquals(1, $response->headers->get('X-Redirect-ID'));
}
/**
* Data provider for both tests.
*/
public function getRedirectData() {
return [
['non-existing', ['key' => 'val'], '/test-path', ['dummy' => 'value']],
['non-existing/', ['key' => 'val'], '/test-path', ['dummy' => 'value']],
];
}
/**
* Instantiates the subscriber and runs onKernelRequestCheckRedirect()
*
* @param $redirect
* The redirect entity.
* @param $request_uri
* The URI of the request.
* @param array $request_query
* The query that is supposed to come via request.
* @param bool $retain_query
* Flag if to retain the query through the redirect.
*
* @return \Symfony\Component\HttpKernel\Event\GetResponseEvent
* THe response event.
*/
protected function callOnKernelRequestCheckRedirect($redirect, $request_uri, $request_query, $retain_query) {
$event = $this->getGetResponseEventStub($request_uri, http_build_query($request_query));
$request = $event->getRequest();
$checker = $this->getMockBuilder('Drupal\redirect\RedirectChecker')
->disableOriginalConstructor()
->getMock();
$checker->expects($this->any())
->method('canRedirect')
->will($this->returnValue(TRUE));
$checker->expects($this->any())
->method('isLoop')
->will($this->returnValue(FALSE));
$context = $this->getMock('Symfony\Component\Routing\RequestContext');
$inbound_path_processor = $this->getMockBuilder('Drupal\Core\PathProcessor\InboundPathProcessorInterface')
->disableOriginalConstructor()
->getMock();
$inbound_path_processor->expects($this->any())
->method('processInbound')
->with($request->getPathInfo(), $request)
->will($this->returnValue($request->getPathInfo()));
$alias_manager = $this->getMockBuilder('Drupal\Core\Path\AliasManager')
->disableOriginalConstructor()
->getMock();
$module_handler = $this->getMockBuilder('Drupal\Core\Extension\ModuleHandlerInterface')
->getMock();
$entity_manager = $this->getMockBuilder('Drupal\Core\Entity\EntityManagerInterface')
->getMock();
$subscriber = new RedirectRequestSubscriber(
$this->getRedirectRepositoryStub('findMatchingRedirect', $redirect),
$this->getLanguageManagerStub(),
$this->getConfigFactoryStub(array('redirect.settings' => array('passthrough_querystring' => $retain_query))),
$alias_manager,
$module_handler,
$entity_manager,
$checker,
$context,
$inbound_path_processor
);
// Run the main redirect method.
$subscriber->onKernelRequestCheckRedirect($event);
return $event;
}
/**
* Gets the redirect repository mock object.
*
* @param $method
* Method to mock - either load() or findMatchingRedirect().
* @param $redirect
* The redirect object to be returned.
*
* @return PHPUnit_Framework_MockObject_MockObject
* The redirect repository.
*/
protected function getRedirectRepositoryStub($method, $redirect) {
$repository = $this->getMockBuilder('Drupal\redirect\RedirectRepository')
->disableOriginalConstructor()
->getMock();
$repository->expects($this->any())
->method($method)
->will($this->returnValue($redirect));
return $repository;
}
/**
* Gets the redirect mock object.
*
* @param $url
* Url to be returned from getRedirectUrl
* @param int $status_code
* The redirect status code.
*
* @return PHPUnit_Framework_MockObject_MockObject
* The mocked redirect object.
*/
protected function getRedirectStub($url, $status_code = 301) {
$redirect = $this->getMockBuilder('Drupal\redirect\Entity\Redirect')
->disableOriginalConstructor()
->getMock();
$redirect->expects($this->once())
->method('getRedirectUrl')
->will($this->returnValue($url));
$redirect->expects($this->any())
->method('getStatusCode')
->will($this->returnValue($status_code));
$redirect->expects($this->any())
->method('id')
->willReturn(1);
$redirect->expects($this->once())
->method('getCacheTags')
->willReturn(['redirect:1']);
return $redirect;
}
/**
* Gets post response event.
*
* @param array $headers
* Headers to be set into the response.
*
* @return \Symfony\Component\HttpKernel\Event\PostResponseEvent
* The post response event object.
*/
protected function getPostResponseEvent($headers = array()) {
$http_kernel = $this->getMockBuilder('\Symfony\Component\HttpKernel\HttpKernelInterface')
->getMock();
$request = $this->getMockBuilder('Symfony\Component\HttpFoundation\Request')
->disableOriginalConstructor()
->getMock();
$response = new Response('', 301, $headers);
return new PostResponseEvent($http_kernel, $request, $response);
}
/**
* Gets response event object.
*
* @param $path_info
* @param $query_string
*
* @return GetResponseEvent
*/
protected function getGetResponseEventStub($path_info, $query_string) {
$request = Request::create($path_info . '?' . $query_string, 'GET', [], [], [], ['SCRIPT_NAME' => 'index.php']);
$http_kernel = $this->getMockBuilder('\Symfony\Component\HttpKernel\HttpKernelInterface')
->getMock();
return new GetResponseEvent($http_kernel, $request, 'test');
}
/**
* Gets the language manager mock object.
*
* @return \Drupal\language\ConfigurableLanguageManagerInterface|PHPUnit_Framework_MockObject_MockObject
*/
protected function getLanguageManagerStub() {
$language_manager = $this->getMockBuilder('Drupal\language\ConfigurableLanguageManagerInterface')
->getMock();
$language_manager->expects($this->any())
->method('getCurrentLanguage')
->will($this->returnValue(new Language(array('id' => 'en'))));
return $language_manager;
}
}
@@ -0,0 +1,225 @@
<?php
namespace Drupal\Tests\redirect\Unit;
use Drupal\Tests\UnitTestCase;
use Drupal\redirect\EventSubscriber\RouteNormalizerRequestSubscriber;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpFoundation\Request;
use Drupal\Core\DependencyInjection\ContainerBuilder;
/**
* Tests the route normalizer.
*
* @group redirect
*
* @coversDefaultClass \Drupal\redirect\EventSubscriber\RouteNormalizerRequestSubscriber
*/
class RouteNormalizerRequestSubscriberTest extends UnitTestCase {
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$kill_switch = $this->getMock('\Drupal\Core\PageCache\ResponsePolicy\KillSwitch');
$kill_switch->expects($this->any())
->method('trigger')
->withAnyParameters()
->will($this->returnValue(NULL));
$container = new ContainerBuilder();
$container->set('page_cache_kill_switch', $kill_switch);
\Drupal::setContainer($container);
}
/**
* @covers ::onKernelRequestRedirect
*/
public function testSkipIfFlagNotEnabled() {
$request_uri = 'https://example.com/route-to-normalize';
$request_query = [];
$event = $this->getGetResponseEventStub($request_uri, http_build_query($request_query));
// We set 'route_normalizer_enabled' config to FALSE and expect to leave onKernelRequestRedirect at the beginning,
// i.e. $this->redirectChecker->canRedirect($request) should never be called.
$subscriber = $this->getSubscriber($request_uri, FALSE, FALSE);
$subscriber->onKernelRequestRedirect($event);
}
/**
* @covers ::onKernelRequestRedirect
*/
public function testSkipIfSubRequest() {
$request_uri = 'https://example.com/route-to-normalize';
$request_query = [];
$event = $this->getGetResponseEventStub($request_uri, http_build_query($request_query), HttpKernelInterface::SUB_REQUEST);
// We are using SUB_REQUEST as the request type and expect to leave onKernelRequestRedirect at the beginning,
// i.e. $this->redirectChecker->canRedirect($request) should never be called.
$subscriber = $this->getSubscriber($request_uri, TRUE, FALSE);
$subscriber->onKernelRequestRedirect($event);
}
/**
* @covers ::onKernelRequestRedirect
*/
public function testSkipIfRequestAttribute() {
$request_uri = 'https://example.com/route-to-normalize';
$request_query = [];
$event = $this->getGetResponseEventStub($request_uri, http_build_query($request_query), HttpKernelInterface::MASTER_REQUEST, TRUE);
// We set '_disable_route_normalizer' as a request attribute and expect to leave onKernelRequestRedirect at the beginning,
// i.e. $this->redirectChecker->canRedirect($request) should never be called.
$subscriber = $this->getSubscriber($request_uri, TRUE, FALSE);
$subscriber->onKernelRequestRedirect($event);
}
/**
* @covers ::onKernelRequestRedirect
* @dataProvider getTestUrls
*/
public function testOnKernelRequestRedirect($request_uri, $request_query, $expected, $expect_normalization) {
$event = $this->getGetResponseEventStub($request_uri, http_build_query($request_query));
$subscriber = $this->getSubscriber($request_uri);
$subscriber->onKernelRequestRedirect($event);
if ($expect_normalization) {
$response = $event->getResponse();
$this->assertEquals($expected, $response->getTargetUrl());
}
}
/**
* Data provider for testOnKernelRequestRedirect().
*/
public function getTestUrls() {
return [
['https://example.com/route-to-normalize', [], 'https://example.com/route-to-normalize', FALSE],
['https://example.com/route-to-normalize', ['key' => 'value'], 'https://example.com/route-to-normalize?key=value', FALSE],
['https://example.com/index.php/', ['q' => 'node/1'], 'https://example.com/?q=node/1', TRUE],
];
}
/**
* Create a RouteNormalizerRequestSubscriber object.
*
* @param string $request_uri
* The return value for the generateFromRoute method.
* @param bool $enabled
* Flag indicating if the normalizer shoud be enabled.
* @param bool $call_expected
* If true, canRedirect() and other methods should be called once.
*
* @return \Drupal\redirect\EventSubscriber\RouteNormalizerRequestSubscriber
*/
protected function getSubscriber($request_uri, $enabled = TRUE, $call_expected = TRUE) {
return new RouteNormalizerRequestSubscriber(
$this->getUrlGeneratorStub($request_uri, $call_expected),
$this->getPathMatcherStub($call_expected),
$this->getConfigFactoryStub([
'redirect.settings' => [
'route_normalizer_enabled' => $enabled,
'default_status_code' => 301,
],
]),
$this->getRedirectCheckerStub($call_expected)
);
}
/**
* Gets the UrlGenerator mock object.
*
* @param string $request_uri
* The return value for the generateFromRoute method.
* @param bool $call_expected
* If true, we expect generateFromRoute() to be called once.
*
* @return \Drupal\Core\Routing\UrlGeneratorInterface|PHPUnit_Framework_MockObject_MockObject
*/
protected function getUrlGeneratorStub($request_uri, $call_expected = TRUE) {
$url_generator = $this->getMockBuilder('\Drupal\Core\Routing\UrlGeneratorInterface')
->getMock();
$options = ['absolute' => TRUE];
$expectation = $call_expected ? $this->once() : $this->never();
$url_generator->expects($expectation)
->method('generateFromRoute')
->with('<current>', [], $options)
->willReturn($request_uri);
return $url_generator;
}
/**
* Gets the PathMatcher mock object.
*
* @param bool $call_expected
* If true, we expect isFrontPage() to be called once.
*
* @return \Drupal\Core\Path\PathMatcherInterface|PHPUnit_Framework_MockObject_MockObject
*/
protected function getPathMatcherStub($call_expected = TRUE) {
$path_matcher = $this->getMockBuilder('\Drupal\Core\Path\PathMatcherInterface')
->getMock();
$expectation = $call_expected ? $this->once() : $this->never();
$path_matcher->expects($expectation)
->method('isFrontPage')
->withAnyParameters()
->willReturn(FALSE);
return $path_matcher;
}
/**
* Gets the RedirectChecker mock object.
*
* @param bool $call_expected
* If true, we expect canRedirect() to be called once.
*
* @return \Drupal\redirect\RedirectChecker|PHPUnit_Framework_MockObject_MockObject
*/
protected function getRedirectCheckerStub($call_expected = TRUE) {
$redirect_checker = $this->getMockBuilder('\Drupal\redirect\RedirectChecker')
->disableOriginalConstructor()
->getMock();
$expectation = $call_expected ? $this->once() : $this->never();
$redirect_checker->expects($expectation)
->method('canRedirect')
->withAnyParameters()
->willReturn(TRUE);
return $redirect_checker;
}
/**
* Returns a GET response event object.
*
* @param string $path_info
* The path of the request.
* @param array $query_string
* The query string of the request.
* @param int $request_type
* The request type of the request.
* @param bool $set_request_attribute
* If true, the request attribute '_disable_route_normalizer' will be set.
*
* @return \Symfony\Component\HttpKernel\Event\GetResponseEvent
*/
protected function getGetResponseEventStub($path_info, $query_string, $request_type = HttpKernelInterface::MASTER_REQUEST, $set_request_attribute = FALSE) {
$request = Request::create($path_info . '?' . $query_string, 'GET', [], [], [], ['SCRIPT_NAME' => 'index.php', 'SCRIPT_FILENAME' => 'index.php']);
if ($set_request_attribute === TRUE) {
$request->attributes->add(['_disable_route_normalizer' => TRUE]);
}
$http_kernel = $this->getMockBuilder('\Symfony\Component\HttpKernel\HttpKernelInterface')
->getMock();
return new GetResponseEvent($http_kernel, $request, $request_type);
}
}
@@ -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.

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