diff --git a/composer.json b/composer.json
index 8f9c1781aee3522bbe59aaeffe67bd6f856ab1fc..a54e303b5fc971ea87f1253e25591fa00f36a730 100644
--- a/composer.json
+++ b/composer.json
@@ -150,6 +150,7 @@
         "drupal/paragraphs": "1.6",
         "drupal/pathauto": "1.0",
         "drupal/realname": "^1.0@RC",
+        "drupal/redirect": "^1.3",
         "drupal/redis": "1.0",
         "drupal/roleassign": "^1.0@alpha",
         "drupal/scheduler": "1.0",
diff --git a/composer.lock b/composer.lock
index e7d86359a61c562b2ee6eb10f3b7277212f6fe70..e4a3ce90a53f11532924a82d5accc0dd97ff6c7d 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
         "This file is @generated automatically"
     ],
-    "content-hash": "d57a48dd84079ed2212fe2b3563de84d",
+    "content-hash": "47248f33666390611f53aeadf8f0ecef",
     "packages": [
         {
             "name": "alchemy/zippy",
@@ -5195,8 +5195,7 @@
             "homepage": "https://www.drupal.org/project/migrate_devel",
             "support": {
                 "source": "http://cgit.drupalcode.org/migrate_devel"
-            },
-            "time": "2017-06-25T23:46:13+00:00"
+            }
         },
         {
             "name": "drupal/migrate_plus",
@@ -5600,6 +5599,61 @@
                 "issues": "https://www.drupal.org/project/issues/realname"
             }
         },
+        {
+            "name": "drupal/redirect",
+            "version": "1.3.0",
+            "source": {
+                "type": "git",
+                "url": "https://git.drupalcode.org/project/redirect.git",
+                "reference": "8.x-1.3"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://ftp.drupal.org/files/projects/redirect-8.x-1.3.zip",
+                "reference": "8.x-1.3",
+                "shasum": "3f9620d186e25f36ac56755979932b8ea965b8c7"
+            },
+            "require": {
+                "drupal/core": "~8"
+            },
+            "type": "drupal-module",
+            "extra": {
+                "branch-alias": {
+                    "dev-1.x": "1.x-dev"
+                },
+                "drupal": {
+                    "version": "8.x-1.3",
+                    "datestamp": "1539682684",
+                    "security-coverage": {
+                        "status": "covered",
+                        "message": "Covered by Drupal's security advisory policy"
+                    }
+                }
+            },
+            "notification-url": "https://packages.drupal.org/8/downloads",
+            "license": [
+                "GPL-2.0+"
+            ],
+            "authors": [
+                {
+                    "name": "Berdir",
+                    "homepage": "https://www.drupal.org/user/214652"
+                },
+                {
+                    "name": "Dave Reid",
+                    "homepage": "https://www.drupal.org/user/53892"
+                },
+                {
+                    "name": "pifagor",
+                    "homepage": "https://www.drupal.org/user/2375692"
+                }
+            ],
+            "description": "Allows users to redirect from old URLs to new URLs.",
+            "homepage": "https://www.drupal.org/project/redirect",
+            "support": {
+                "source": "https://git.drupalcode.org/project/redirect"
+            }
+        },
         {
             "name": "drupal/redis",
             "version": "1.0.0",
diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json
index 9b5c6855df7544df0dc95cb7ffe603742ce82b90..1e8f75e51e13f3ff673455d708ceb74747747d97 100644
--- a/vendor/composer/installed.json
+++ b/vendor/composer/installed.json
@@ -5776,6 +5776,63 @@
             "issues": "https://www.drupal.org/project/issues/realname"
         }
     },
+    {
+        "name": "drupal/redirect",
+        "version": "1.3.0",
+        "version_normalized": "1.3.0.0",
+        "source": {
+            "type": "git",
+            "url": "https://git.drupalcode.org/project/redirect.git",
+            "reference": "8.x-1.3"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://ftp.drupal.org/files/projects/redirect-8.x-1.3.zip",
+            "reference": "8.x-1.3",
+            "shasum": "3f9620d186e25f36ac56755979932b8ea965b8c7"
+        },
+        "require": {
+            "drupal/core": "~8"
+        },
+        "type": "drupal-module",
+        "extra": {
+            "branch-alias": {
+                "dev-1.x": "1.x-dev"
+            },
+            "drupal": {
+                "version": "8.x-1.3",
+                "datestamp": "1539682684",
+                "security-coverage": {
+                    "status": "covered",
+                    "message": "Covered by Drupal's security advisory policy"
+                }
+            }
+        },
+        "installation-source": "dist",
+        "notification-url": "https://packages.drupal.org/8/downloads",
+        "license": [
+            "GPL-2.0+"
+        ],
+        "authors": [
+            {
+                "name": "Berdir",
+                "homepage": "https://www.drupal.org/user/214652"
+            },
+            {
+                "name": "Dave Reid",
+                "homepage": "https://www.drupal.org/user/53892"
+            },
+            {
+                "name": "pifagor",
+                "homepage": "https://www.drupal.org/user/2375692"
+            }
+        ],
+        "description": "Allows users to redirect from old URLs to new URLs.",
+        "homepage": "https://www.drupal.org/project/redirect",
+        "support": {
+            "source": "https://git.drupalcode.org/project/redirect"
+        }
+    },
     {
         "name": "drupal/redis",
         "version": "1.0.0",
diff --git a/web/modules/redirect/LICENSE.txt b/web/modules/redirect/LICENSE.txt
new file mode 100644
index 0000000000000000000000000000000000000000..d159169d1050894d3ea3b98e1c965c4058208fe1
--- /dev/null
+++ b/web/modules/redirect/LICENSE.txt
@@ -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.
diff --git a/web/modules/redirect/README.txt b/web/modules/redirect/README.txt
new file mode 100644
index 0000000000000000000000000000000000000000..5ae8276ab441ba83f75ef20e5a1a9b1b9d279038
--- /dev/null
+++ b/web/modules/redirect/README.txt
@@ -0,0 +1,61 @@
+CONTENTS OF THIS FILE
+---------------------
+
+ * Introduction
+ * Requirements
+ * Installation
+ * Configuration
+ * Maintainers
+
+
+INTRODUCTION
+------------
+
+The Redirect module provides a unified redirection API (also replaces
+path_redirect and globalredirect).
+
+
+ * For a full description of the module visit:
+   https://www.drupal.org/project/redirect
+
+ * To submit bug reports and feature suggestions, or to track changes visit:
+   https://www.drupal.org/project/issues/redirect
+
+
+REQUIREMENTS
+------------
+
+This module requires no modules outside of Drupal core.
+
+
+INSTALLATION
+------------
+
+ * Install the Redirect module as you would normally install a contributed
+   Drupal module. Visit https://www.drupal.org/node/1897420 for further
+   information.
+
+
+CONFIGURATION
+-------------
+
+    1. Navigate to Administration > Extend and enable the module.
+    2. Navigate to Administration > Configuration > Search and Metadata > URL
+       redirects for configuration.
+    3. Select "Add redirect" and in the "Path" field add the old path.
+    4. In the "To" field, start typing the title of a piece of content to select
+       it. You can also enter an internal path such as /node/add or an external
+       URL such as http://example.com. Enter <front> to link to the front page.
+    5. Select the Redirect status: 300 Multiple Choices, 301 Moved Permanently,
+       302 Found, 303 See Other, 304 Not Modified, 305 Use Proxy, or 307
+       Temporary Redirect. Save.
+    6. Once a redirect has been added, it will be listed in the URL Redirects
+       vertical tab group on the content's edit page.
+
+
+MAINTAINERS
+-----------
+
+Supporting organization for 8.x-1.x port:
+
+ * MD Systems - https://www.drupal.org/md-systems
diff --git a/web/modules/redirect/composer.json b/web/modules/redirect/composer.json
new file mode 100644
index 0000000000000000000000000000000000000000..b4f147895e65dd233ae60cd398fd322d2320a2bd
--- /dev/null
+++ b/web/modules/redirect/composer.json
@@ -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"
+  }
+}
diff --git a/web/modules/redirect/config/install/redirect.settings.yml b/web/modules/redirect/config/install/redirect.settings.yml
new file mode 100644
index 0000000000000000000000000000000000000000..70a96888dce3ced6bfe581ac1fc5a8e3d264f05e
--- /dev/null
+++ b/web/modules/redirect/config/install/redirect.settings.yml
@@ -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
diff --git a/web/modules/redirect/config/install/system.action.redirect_delete_action.yml b/web/modules/redirect/config/install/system.action.redirect_delete_action.yml
new file mode 100644
index 0000000000000000000000000000000000000000..4b23fc81b964ebb4edf43117868839ab272367d9
--- /dev/null
+++ b/web/modules/redirect/config/install/system.action.redirect_delete_action.yml
@@ -0,0 +1,13 @@
+langcode: en
+status: true
+dependencies:
+  enforced:
+    module:
+      - redirect
+  module:
+    - redirect
+id: redirect_delete_action
+label: 'Delete redirect'
+type: redirect
+plugin: redirect_delete_action
+configuration: {  }
diff --git a/web/modules/redirect/config/install/views.view.redirect.yml b/web/modules/redirect/config/install/views.view.redirect.yml
new file mode 100644
index 0000000000000000000000000000000000000000..6a1c65f55aec035bc014ab1e9a823ead21609a8e
--- /dev/null
+++ b/web/modules/redirect/config/install/views.view.redirect.yml
@@ -0,0 +1,597 @@
+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: Filter
+          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
+      max-age: 0
+      tags: {  }
+  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
+      max-age: 0
+      tags: {  }
diff --git a/web/modules/redirect/config/optional/language.content_settings.redirect.redirect.yml b/web/modules/redirect/config/optional/language.content_settings.redirect.redirect.yml
new file mode 100644
index 0000000000000000000000000000000000000000..e6dfad3fd64f79ff4bcb00ca7f3a23136d1df114
--- /dev/null
+++ b/web/modules/redirect/config/optional/language.content_settings.redirect.redirect.yml
@@ -0,0 +1,10 @@
+langcode: en
+status: true
+dependencies:
+  module:
+    - redirect
+id: redirect.redirect
+target_entity_type_id: redirect
+target_bundle: redirect
+default_langcode: und
+language_alterable: true
diff --git a/web/modules/redirect/config/schema/redirect.schema.yml b/web/modules/redirect/config/schema/redirect.schema.yml
new file mode 100644
index 0000000000000000000000000000000000000000..bd9ccd5d6e5fa4c6c7888a9980c47064ad51d896
--- /dev/null
+++ b/web/modules/redirect/config/schema/redirect.schema.yml
@@ -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'
diff --git a/web/modules/redirect/config/schema/redirect.views.schema.yml b/web/modules/redirect/config/schema/redirect.views.schema.yml
new file mode 100644
index 0000000000000000000000000000000000000000..59a565ce4188f8cdc9339efa23216f00f863f69d
--- /dev/null
+++ b/web/modules/redirect/config/schema/redirect.views.schema.yml
@@ -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'
diff --git a/web/modules/redirect/css/redirect.admin.css b/web/modules/redirect/css/redirect.admin.css
new file mode 100644
index 0000000000000000000000000000000000000000..f613931ed7a7a6a4264ea56beebb63022f81ed76
--- /dev/null
+++ b/web/modules/redirect/css/redirect.admin.css
@@ -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;
+}
diff --git a/web/modules/redirect/migrations/d6_path_redirect.yml b/web/modules/redirect/migrations/d6_path_redirect.yml
new file mode 100644
index 0000000000000000000000000000000000000000..1f3ddef96980c4e0f50b240ed4be17ab018d3494
--- /dev/null
+++ b/web/modules/redirect/migrations/d6_path_redirect.yml
@@ -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
\ No newline at end of file
diff --git a/web/modules/redirect/migrations/d7_path_redirect.yml b/web/modules/redirect/migrations/d7_path_redirect.yml
new file mode 100644
index 0000000000000000000000000000000000000000..3ee9a287dc49acbd042fc1206bd1ed071782be59
--- /dev/null
+++ b/web/modules/redirect/migrations/d7_path_redirect.yml
@@ -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
diff --git a/web/modules/redirect/modules/redirect_404/config/install/redirect_404.settings.yml b/web/modules/redirect/modules/redirect_404/config/install/redirect_404.settings.yml
new file mode 100644
index 0000000000000000000000000000000000000000..030db9f0ad6dd89b24993210cab656fddc395b24
--- /dev/null
+++ b/web/modules/redirect/modules/redirect_404/config/install/redirect_404.settings.yml
@@ -0,0 +1,3 @@
+row_limit: 10000
+pages: ''
+suppress_404: FALSE
diff --git a/web/modules/redirect/modules/redirect_404/config/install/views.view.redirect_404.yml b/web/modules/redirect/modules/redirect_404/config/install/views.view.redirect_404.yml
new file mode 100644
index 0000000000000000000000000000000000000000..75777dee4b65203231239af7fe5c9dc80f238f15
--- /dev/null
+++ b/web/modules/redirect/modules/redirect_404/config/install/views.view.redirect_404.yml
@@ -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: {  }
diff --git a/web/modules/redirect/modules/redirect_404/config/schema/redirect_404.schema.yml b/web/modules/redirect/modules/redirect_404/config/schema/redirect_404.schema.yml
new file mode 100644
index 0000000000000000000000000000000000000000..3aaeb9d14ad08b7dd01ef6c7e178873917cc1d39
--- /dev/null
+++ b/web/modules/redirect/modules/redirect_404/config/schema/redirect_404.schema.yml
@@ -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."
diff --git a/web/modules/redirect/modules/redirect_404/config/schema/redirect_404.views.schema.yml b/web/modules/redirect/modules/redirect_404/config/schema/redirect_404.views.schema.yml
new file mode 100644
index 0000000000000000000000000000000000000000..5798b983ea607a191f279a55b5e5cabedf5bbd65
--- /dev/null
+++ b/web/modules/redirect/modules/redirect_404/config/schema/redirect_404.views.schema.yml
@@ -0,0 +1,7 @@
+views.field.redirect_404_operations:
+  type: views_field
+  label: 'Redirect 404 operations'
+  mapping:
+    text:
+      type: label
+      label: 'Redirect 404 operations'
diff --git a/web/modules/redirect/modules/redirect_404/redirect_404.info.yml b/web/modules/redirect/modules/redirect_404/redirect_404.info.yml
new file mode 100644
index 0000000000000000000000000000000000000000..1fbcea4167bd98cf624cfb8693f3e94be29d9181
--- /dev/null
+++ b/web/modules/redirect/modules/redirect_404/redirect_404.info.yml
@@ -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-10-16
+version: '8.x-1.3'
+core: '8.x'
+project: 'redirect'
+datestamp: 1539682690
diff --git a/web/modules/redirect/modules/redirect_404/redirect_404.install b/web/modules/redirect/modules/redirect_404/redirect_404.install
new file mode 100644
index 0000000000000000000000000000000000000000..09190e7cf7d22a40356f3711bae13f04163f0308
--- /dev/null
+++ b/web/modules/redirect/modules/redirect_404/redirect_404.install
@@ -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');
+}
diff --git a/web/modules/redirect/modules/redirect_404/redirect_404.links.action.yml b/web/modules/redirect/modules/redirect_404/redirect_404.links.action.yml
new file mode 100644
index 0000000000000000000000000000000000000000..24c721adb11049c31af8b771c4b05eb5f6716b3a
--- /dev/null
+++ b/web/modules/redirect/modules/redirect_404/redirect_404.links.action.yml
@@ -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'
diff --git a/web/modules/redirect/modules/redirect_404/redirect_404.links.menu.yml b/web/modules/redirect/modules/redirect_404/redirect_404.links.menu.yml
new file mode 100644
index 0000000000000000000000000000000000000000..28622669f8e32f74921357e71623449e3a2065e3
--- /dev/null
+++ b/web/modules/redirect/modules/redirect_404/redirect_404.links.menu.yml
@@ -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
diff --git a/web/modules/redirect/modules/redirect_404/redirect_404.links.task.yml b/web/modules/redirect/modules/redirect_404/redirect_404.links.task.yml
new file mode 100644
index 0000000000000000000000000000000000000000..955161a1124acc9083e30959abd9980540ea4cc1
--- /dev/null
+++ b/web/modules/redirect/modules/redirect_404/redirect_404.links.task.yml
@@ -0,0 +1,5 @@
+redirect_404.fix_404:
+  route_name: redirect_404.fix_404
+  base_route: redirect.list
+  title: Fix 404 pages
+  weight: 30
diff --git a/web/modules/redirect/modules/redirect_404/redirect_404.module b/web/modules/redirect/modules/redirect_404/redirect_404.module
new file mode 100644
index 0000000000000000000000000000000000000000..b90d166e1cf84b47117a023438c267c19834c510
--- /dev/null
+++ b/web/modules/redirect/modules/redirect_404/redirect_404.module
@@ -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);
+}
diff --git a/web/modules/redirect/modules/redirect_404/redirect_404.routing.yml b/web/modules/redirect/modules/redirect_404/redirect_404.routing.yml
new file mode 100644
index 0000000000000000000000000000000000000000..15146cba9ccc2ac801beebe60fac6b23aaa06d08
--- /dev/null
+++ b/web/modules/redirect/modules/redirect_404/redirect_404.routing.yml
@@ -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'
diff --git a/web/modules/redirect/modules/redirect_404/redirect_404.services.yml b/web/modules/redirect/modules/redirect_404/redirect_404.services.yml
new file mode 100644
index 0000000000000000000000000000000000000000..8221015ab8b7cd2c6d2a0ff57d0cad46de8086b4
--- /dev/null
+++ b/web/modules/redirect/modules/redirect_404/redirect_404.services.yml
@@ -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']
diff --git a/web/modules/redirect/modules/redirect_404/redirect_404.views.inc b/web/modules/redirect/modules/redirect_404/redirect_404.views.inc
new file mode 100644
index 0000000000000000000000000000000000000000..bc18f93e1e53dd9d813818b6d677540ea80ab9ea
--- /dev/null
+++ b/web/modules/redirect/modules/redirect_404/redirect_404.views.inc
@@ -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;
+}
diff --git a/web/modules/redirect/modules/redirect_404/src/Controller/Fix404IgnoreController.php b/web/modules/redirect/modules/redirect_404/src/Controller/Fix404IgnoreController.php
new file mode 100644
index 0000000000000000000000000000000000000000..a00331188cd45b061bcb7a1de0af9f3d4b4982d4
--- /dev/null
+++ b/web/modules/redirect/modules/redirect_404/src/Controller/Fix404IgnoreController.php
@@ -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);
+  }
+
+}
diff --git a/web/modules/redirect/modules/redirect_404/src/EventSubscriber/Redirect404Subscriber.php b/web/modules/redirect/modules/redirect_404/src/EventSubscriber/Redirect404Subscriber.php
new file mode 100644
index 0000000000000000000000000000000000000000..f7cd1416ae101907769db7c98d04ce4ba7369945
--- /dev/null
+++ b/web/modules/redirect/modules/redirect_404/src/EventSubscriber/Redirect404Subscriber.php
@@ -0,0 +1,129 @@
+<?php
+
+namespace Drupal\redirect_404\EventSubscriber;
+
+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 = mb_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(mb_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);
+    }
+  }
+
+}
diff --git a/web/modules/redirect/modules/redirect_404/src/Form/RedirectFix404Form.php b/web/modules/redirect/modules/redirect_404/src/Form/RedirectFix404Form.php
new file mode 100644
index 0000000000000000000000000000000000000000..7e01d1af587a556f9a0b856b7eda7c5113d1e8d9
--- /dev/null
+++ b/web/modules/redirect/modules/redirect_404/src/Form/RedirectFix404Form.php
@@ -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');
+    }
+  }
+
+}
diff --git a/web/modules/redirect/modules/redirect_404/src/Plugin/views/field/Language.php b/web/modules/redirect/modules/redirect_404/src/Plugin/views/field/Language.php
new file mode 100644
index 0000000000000000000000000000000000000000..4fc03d3341caaa6e069ac5da8184c07a2bf3fc87
--- /dev/null
+++ b/web/modules/redirect/modules/redirect_404/src/Plugin/views/field/Language.php
@@ -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();
+  }
+
+}
diff --git a/web/modules/redirect/modules/redirect_404/src/Plugin/views/field/Redirect404Operations.php b/web/modules/redirect/modules/redirect_404/src/Plugin/views/field/Redirect404Operations.php
new file mode 100644
index 0000000000000000000000000000000000000000..b1c0e6da574cc7fd77f3e7ad365404adc788d3f6
--- /dev/null
+++ b/web/modules/redirect/modules/redirect_404/src/Plugin/views/field/Redirect404Operations.php
@@ -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();
+  }
+
+}
diff --git a/web/modules/redirect/modules/redirect_404/src/RedirectNotFoundStorageInterface.php b/web/modules/redirect/modules/redirect_404/src/RedirectNotFoundStorageInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..27bc56de54325241db67a477c0bfba040e7bc980
--- /dev/null
+++ b/web/modules/redirect/modules/redirect_404/src/RedirectNotFoundStorageInterface.php
@@ -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();
+
+}
diff --git a/web/modules/redirect/modules/redirect_404/src/Render/Redirect404LogSuppressor.php b/web/modules/redirect/modules/redirect_404/src/Render/Redirect404LogSuppressor.php
new file mode 100644
index 0000000000000000000000000000000000000000..cd74cabd84dc66a59553e2164317c764471924de
--- /dev/null
+++ b/web/modules/redirect/modules/redirect_404/src/Render/Redirect404LogSuppressor.php
@@ -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);
+  }
+
+}
diff --git a/web/modules/redirect/modules/redirect_404/src/SqlRedirectNotFoundStorage.php b/web/modules/redirect/modules/redirect_404/src/SqlRedirectNotFoundStorage.php
new file mode 100644
index 0000000000000000000000000000000000000000..98003ebaeda3ba8ffc668c6463db652889ed4ee2
--- /dev/null
+++ b/web/modules/redirect/modules/redirect_404/src/SqlRedirectNotFoundStorage.php
@@ -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 (mb_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;
+  }
+
+}
diff --git a/web/modules/redirect/modules/redirect_404/src/Tests/Fix404RedirectUILanguageTest.php b/web/modules/redirect/modules/redirect_404/src/Tests/Fix404RedirectUILanguageTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..89a14d2dc525e5128d72aefb5f6fb036847852e2
--- /dev/null
+++ b/web/modules/redirect/modules/redirect_404/src/Tests/Fix404RedirectUILanguageTest.php
@@ -0,0 +1,203 @@
+<?php
+
+namespace Drupal\redirect_404\Tests;
+
+use Drupal\Component\Utility\UrlHelper;
+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();
+    $expected_query = [
+      'destination' => $destination,
+      'language' => 'fr',
+      'source' => 'testing',
+    ];
+    $parsed_url = UrlHelper::parse($this->getUrl());
+    $this->assertEqual(Url::fromRoute('redirect.add')->setAbsolute()->toString(), $parsed_url['path']);
+    $this->assertEqual($expected_query, $parsed_url['query']);
+    $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');
+    $expected_query = [
+      'destination' => $destination,
+      'language' => 'en',
+      'source' => 'testing1',
+    ];
+    $parsed_url = UrlHelper::parse($this->getUrl());
+    $this->assertEqual(Url::fromRoute('redirect.add')->setAbsolute()->toString(), $parsed_url['path']);
+    $this->assertEqual($expected_query, $parsed_url['query']);
+    $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');
+  }
+
+}
diff --git a/web/modules/redirect/modules/redirect_404/src/Tests/Fix404RedirectUITest.php b/web/modules/redirect/modules/redirect_404/src/Tests/Fix404RedirectUITest.php
new file mode 100644
index 0000000000000000000000000000000000000000..088cbba47a7a982e7282f2fbc8563ac8ee1e214e
--- /dev/null
+++ b/web/modules/redirect/modules/redirect_404/src/Tests/Fix404RedirectUITest.php
@@ -0,0 +1,186 @@
+<?php
+
+namespace Drupal\redirect_404\Tests;
+
+use Drupal\Component\Utility\UrlHelper;
+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();
+    $expected_query = [
+      'destination' => $destination,
+      'language' => 'en',
+      'source' => 'non-existing0',
+    ];
+    $parsed_url = UrlHelper::parse($this->getUrl());
+    $this->assertEqual(Url::fromRoute('redirect.add')->setAbsolute()->toString(), $parsed_url['path']);
+    $this->assertEqual($expected_query, $parsed_url['query']);
+    $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');
+    $expected_query = [
+      'source' => 'non-existing2',
+      'language' => 'en',
+      'destination' => $destination,
+    ];
+    $parsed_url = UrlHelper::parse($this->getUrl());
+    $this->assertEqual(Url::fromRoute('redirect.add')->setAbsolute()->toString(), $parsed_url['path']);
+    $this->assertEqual($expected_query, $parsed_url['query']);
+    $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");
+  }
+
+}
diff --git a/web/modules/redirect/modules/redirect_404/src/Tests/Redirect404LogSuppressorTest.php b/web/modules/redirect/modules/redirect_404/src/Tests/Redirect404LogSuppressorTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..ef021b553e24eb7d72b0a64e7c321931fce4edb1
--- /dev/null
+++ b/web/modules/redirect/modules/redirect_404/src/Tests/Redirect404LogSuppressorTest.php
@@ -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);
+
+  }
+}
diff --git a/web/modules/redirect/modules/redirect_404/src/Tests/Redirect404TestBase.php b/web/modules/redirect/modules/redirect_404/src/Tests/Redirect404TestBase.php
new file mode 100644
index 0000000000000000000000000000000000000000..2842aeb14c1572f0c5a5b1df418852d92024adcb
--- /dev/null
+++ b/web/modules/redirect/modules/redirect_404/src/Tests/Redirect404TestBase.php
@@ -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);
+    }
+  }
+
+}
diff --git a/web/modules/redirect/modules/redirect_404/tests/src/Kernel/Fix404RedirectCronJobTest.php b/web/modules/redirect/modules/redirect_404/tests/src/Kernel/Fix404RedirectCronJobTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..6ed6d6774b1cea9f9716bbd7099961a3a5ed3d09
--- /dev/null
+++ b/web/modules/redirect/modules/redirect_404/tests/src/Kernel/Fix404RedirectCronJobTest.php
@@ -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);
+    }
+  }
+}
diff --git a/web/modules/redirect/modules/redirect_404/tests/src/Unit/SqlRedirectNotFoundStorageTest.php b/web/modules/redirect/modules/redirect_404/tests/src/Unit/SqlRedirectNotFoundStorageTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..42e0224e565a5a87e82918ac958ecf79d8fb7175
--- /dev/null
+++ b/web/modules/redirect/modules/redirect_404/tests/src/Unit/SqlRedirectNotFoundStorageTest.php
@@ -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');
+  }
+
+}
diff --git a/web/modules/redirect/modules/redirect_domain/config/install/redirect_domain.domains.yml b/web/modules/redirect/modules/redirect_domain/config/install/redirect_domain.domains.yml
new file mode 100644
index 0000000000000000000000000000000000000000..05c0310cc0579f318497be0aea290cb10e0fe847
--- /dev/null
+++ b/web/modules/redirect/modules/redirect_domain/config/install/redirect_domain.domains.yml
@@ -0,0 +1 @@
+domain_redirects: { }
diff --git a/web/modules/redirect/modules/redirect_domain/config/schema/redirect_domain.schema.yml b/web/modules/redirect/modules/redirect_domain/config/schema/redirect_domain.schema.yml
new file mode 100644
index 0000000000000000000000000000000000000000..5e29a4c57bfd0b83950686acf2631c316a37cf19
--- /dev/null
+++ b/web/modules/redirect/modules/redirect_domain/config/schema/redirect_domain.schema.yml
@@ -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'
diff --git a/web/modules/redirect/modules/redirect_domain/redirect_domain.info.yml b/web/modules/redirect/modules/redirect_domain/redirect_domain.info.yml
new file mode 100644
index 0000000000000000000000000000000000000000..aeb66d45354fadfae540f2f970ef8f84a75f59d3
--- /dev/null
+++ b/web/modules/redirect/modules/redirect_domain/redirect_domain.info.yml
@@ -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-10-16
+version: '8.x-1.3'
+core: '8.x'
+project: 'redirect'
+datestamp: 1539682690
diff --git a/web/modules/redirect/modules/redirect_domain/redirect_domain.links.task.yml b/web/modules/redirect/modules/redirect_domain/redirect_domain.links.task.yml
new file mode 100644
index 0000000000000000000000000000000000000000..1dcee94c0c685a1ecfdcf7b79e87bcb99ce7408e
--- /dev/null
+++ b/web/modules/redirect/modules/redirect_domain/redirect_domain.links.task.yml
@@ -0,0 +1,5 @@
+redirect_domain.domain_list:
+  title: 'Domain redirects'
+  base_route: redirect.list
+  route_name: redirect_domain.domain_list
+  weight: 60
diff --git a/web/modules/redirect/modules/redirect_domain/redirect_domain.module b/web/modules/redirect/modules/redirect_domain/redirect_domain.module
new file mode 100644
index 0000000000000000000000000000000000000000..f60b329bf111ccd96e475ce5713f124549501234
--- /dev/null
+++ b/web/modules/redirect/modules/redirect_domain/redirect_domain.module
@@ -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;
+  }
+}
diff --git a/web/modules/redirect/modules/redirect_domain/redirect_domain.routing.yml b/web/modules/redirect/modules/redirect_domain/redirect_domain.routing.yml
new file mode 100644
index 0000000000000000000000000000000000000000..ad38a0716e6de8bd75bcb8c04a58a289cceb9e22
--- /dev/null
+++ b/web/modules/redirect/modules/redirect_domain/redirect_domain.routing.yml
@@ -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'
diff --git a/web/modules/redirect/modules/redirect_domain/redirect_domain.services.yml b/web/modules/redirect/modules/redirect_domain/redirect_domain.services.yml
new file mode 100644
index 0000000000000000000000000000000000000000..94503c61706793f08d0592f5c927e839ed0f8b89
--- /dev/null
+++ b/web/modules/redirect/modules/redirect_domain/redirect_domain.services.yml
@@ -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 }
diff --git a/web/modules/redirect/modules/redirect_domain/src/EventSubscriber/DomainRedirectRequestSubscriber.php b/web/modules/redirect/modules/redirect_domain/src/EventSubscriber/DomainRedirectRequestSubscriber.php
new file mode 100644
index 0000000000000000000000000000000000000000..bc949b81fd11ef308d9c167c750d6c704235faaf
--- /dev/null
+++ b/web/modules/redirect/modules/redirect_domain/src/EventSubscriber/DomainRedirectRequestSubscriber.php
@@ -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;
+  }
+
+}
diff --git a/web/modules/redirect/modules/redirect_domain/src/Form/RedirectDomainForm.php b/web/modules/redirect/modules/redirect_domain/src/Form/RedirectDomainForm.php
new file mode 100644
index 0000000000000000000000000000000000000000..1bf5b9a870ec5e9b7ffef195bcf9b0905eee1cb3
--- /dev/null
+++ b/web/modules/redirect/modules/redirect_domain/src/Form/RedirectDomainForm.php
@@ -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', $this->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($this->t('The domain redirects have been saved.'));
+  }
+}
diff --git a/web/modules/redirect/modules/redirect_domain/src/Tests/RedirectDomainUITest.php b/web/modules/redirect/modules/redirect_domain/src/Tests/RedirectDomainUITest.php
new file mode 100644
index 0000000000000000000000000000000000000000..d50791f377f2419279a164b6643886be1ad3a1a1
--- /dev/null
+++ b/web/modules/redirect/modules/redirect_domain/src/Tests/RedirectDomainUITest.php
@@ -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]', '/');
+  }
+}
diff --git a/web/modules/redirect/modules/redirect_domain/tests/src/Unit/DomainRedirectRequestSubscriberTest.php b/web/modules/redirect/modules/redirect_domain/tests/src/Unit/DomainRedirectRequestSubscriberTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..89453ab9d8a5588daf49b66fead5f1a8f4057860
--- /dev/null
+++ b/web/modules/redirect/modules/redirect_domain/tests/src/Unit/DomainRedirectRequestSubscriberTest.php
@@ -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;
+  }
+}
diff --git a/web/modules/redirect/redirect.api.php b/web/modules/redirect/redirect.api.php
new file mode 100644
index 0000000000000000000000000000000000000000..9ec1bdff7d4b2f7c394de2aab00247e700577a76
--- /dev/null
+++ b/web/modules/redirect/redirect.api.php
@@ -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".
+ */
diff --git a/web/modules/redirect/redirect.drush.inc b/web/modules/redirect/redirect.drush.inc
new file mode 100644
index 0000000000000000000000000000000000000000..ffe98ce05c47213f6b5c945d85e9f9b7358760d9
--- /dev/null
+++ b/web/modules/redirect/redirect.drush.inc
@@ -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;
+}
diff --git a/web/modules/redirect/redirect.generate.inc b/web/modules/redirect/redirect.generate.inc
new file mode 100644
index 0000000000000000000000000000000000000000..6120ae19b36e4fb0c34736a30927deb99e60ef9a
--- /dev/null
+++ b/web/modules/redirect/redirect.generate.inc
@@ -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;
+}
diff --git a/web/modules/redirect/redirect.info.yml b/web/modules/redirect/redirect.info.yml
new file mode 100644
index 0000000000000000000000000000000000000000..a1e6eb15d65bbe755a1f6ce15d8c1215c30cc773
--- /dev/null
+++ b/web/modules/redirect/redirect.info.yml
@@ -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-10-16
+version: '8.x-1.3'
+core: '8.x'
+project: 'redirect'
+datestamp: 1539682690
diff --git a/web/modules/redirect/redirect.install b/web/modules/redirect/redirect.install
new file mode 100644
index 0000000000000000000000000000000000000000..31a8ee693f77cdd4de61758548a803047b881c9a
--- /dev/null
+++ b/web/modules/redirect/redirect.install
@@ -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();
+}
diff --git a/web/modules/redirect/redirect.libraries.yml b/web/modules/redirect/redirect.libraries.yml
new file mode 100644
index 0000000000000000000000000000000000000000..9874872643dc024b5c5456ce4919571e40ac79b8
--- /dev/null
+++ b/web/modules/redirect/redirect.libraries.yml
@@ -0,0 +1,5 @@
+drupal.redirect.admin:
+  version: VERSION
+  css:
+    component:
+      css/redirect.admin.css: {}
diff --git a/web/modules/redirect/redirect.links.action.yml b/web/modules/redirect/redirect.links.action.yml
new file mode 100644
index 0000000000000000000000000000000000000000..3952ca5944706254814c02ddf7f4e039afa8126a
--- /dev/null
+++ b/web/modules/redirect/redirect.links.action.yml
@@ -0,0 +1,5 @@
+redirect.add:
+  route_name: redirect.add
+  title: 'Add redirect'
+  appears_on:
+    - 'redirect.list'
diff --git a/web/modules/redirect/redirect.links.menu.yml b/web/modules/redirect/redirect.links.menu.yml
new file mode 100644
index 0000000000000000000000000000000000000000..b034d47fdafac16f2ddc21830df8895ae860b488
--- /dev/null
+++ b/web/modules/redirect/redirect.links.menu.yml
@@ -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
diff --git a/web/modules/redirect/redirect.links.task.yml b/web/modules/redirect/redirect.links.task.yml
new file mode 100644
index 0000000000000000000000000000000000000000..4216cbc30cf6b837684a6f765adba3a4c5571a74
--- /dev/null
+++ b/web/modules/redirect/redirect.links.task.yml
@@ -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
diff --git a/web/modules/redirect/redirect.module b/web/modules/redirect/redirect.module
new file mode 100644
index 0000000000000000000000000000000000000000..bf4d4f9022f3b8b35d8202d25ed5d617d7677ebd
--- /dev/null
+++ b/web/modules/redirect/redirect.module
@@ -0,0 +1,504 @@
+<?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;
+use Symfony\Component\Routing\Exception\RouteNotFoundException;
+
+/**
+ * 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) {
+  try {
+    if ($entity->getEntityType()->hasLinkTemplate('canonical') && $entity->toUrl('canonical')->isRouted()) {
+      redirect_delete_by_path('internal:/' . $entity->toUrl('canonical')->getInternalPath());
+      redirect_delete_by_path('entity:' . $entity->getEntityTypeId() . '/' . $entity->id());
+    }
+  }
+  catch (RouteNotFoundException $e) {
+    // This can happen if a module incorrectly defines a link template, ignore
+    // such errors.
+  }
+}
+
+/**
+ * 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>',
+      ];
+    }
+  }
+}
diff --git a/web/modules/redirect/redirect.permissions.yml b/web/modules/redirect/redirect.permissions.yml
new file mode 100644
index 0000000000000000000000000000000000000000..8596ee1ca4beeab3911da236b69463e0a105e566
--- /dev/null
+++ b/web/modules/redirect/redirect.permissions.yml
@@ -0,0 +1,4 @@
+administer redirects:
+  title: 'Administer individual URL redirections'
+administer redirect settings:
+  title: 'Administer global URL redirection settings'
diff --git a/web/modules/redirect/redirect.routing.yml b/web/modules/redirect/redirect.routing.yml
new file mode 100644
index 0000000000000000000000000000000000000000..4195cbb597c9ae4d7b5ea700ec6aabdc31e59665
--- /dev/null
+++ b/web/modules/redirect/redirect.routing.yml
@@ -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'
diff --git a/web/modules/redirect/redirect.services.yml b/web/modules/redirect/redirect.services.yml
new file mode 100644
index 0000000000000000000000000000000000000000..743bd9392565e231a893b34112343c82d380125b
--- /dev/null
+++ b/web/modules/redirect/redirect.services.yml
@@ -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 }
diff --git a/web/modules/redirect/src/Entity/Redirect.php b/web/modules/redirect/src/Entity/Redirect.php
new file mode 100644
index 0000000000000000000000000000000000000000..cb5d4c56dd54f1bd12c3af2d4a5260133d590806
--- /dev/null
+++ b/web/modules/redirect/src/Entity/Redirect.php
@@ -0,0 +1,347 @@
+<?php
+
+namespace Drupal\redirect\Entity;
+
+use Drupal\Component\Utility\Crypt;
+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\redirect\RedirectViewsData",
+ *     "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' => mb_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) {
+    // Get the language code directly from the field as language() might not
+    // be up to date if the language was just changed.
+    $this->set('hash', Redirect::generateHash($this->redirect_source->path, (array) $this->redirect_source->query, $this->get('language')->value));
+  }
+
+  /**
+   * 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) : '');
+    $external = UrlHelper::isValid($url, TRUE);
+    $uri = ($external ? $url : 'internal:/' . ltrim($uri, '/'));
+    $this->redirect_redirect->set(0, ['uri' => $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());
+  }
+
+}
diff --git a/web/modules/redirect/src/EventSubscriber/RedirectRequestSubscriber.php b/web/modules/redirect/src/EventSubscriber/RedirectRequestSubscriber.php
new file mode 100644
index 0000000000000000000000000000000000000000..87095defff75593152752ce047b74f417ed74703
--- /dev/null
+++ b/web/modules/redirect/src/EventSubscriber/RedirectRequestSubscriber.php
@@ -0,0 +1,211 @@
+<?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\AliasManagerInterface;
+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\AliasManagerInterface $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, AliasManagerInterface $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);
+
+    if (strpos($request->getPathInfo(), '/system/files/') === 0 && !$request->query->has('file')) {
+      // Private files paths are split by the inbound path processor and the
+      // relative file path is moved to the 'file' query string parameter. This
+      // is because the route system does not allow an arbitrary amount of
+      // parameters. We preserve the path as is returned by the request object.
+      // @see \Drupal\system\PathProcessor\PathProcessorFiles::processInbound()
+      $path = $request->getPathInfo();
+    }
+    else {
+      // 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;
+  }
+
+}
diff --git a/web/modules/redirect/src/EventSubscriber/RedirectSettingsCacheTag.php b/web/modules/redirect/src/EventSubscriber/RedirectSettingsCacheTag.php
new file mode 100644
index 0000000000000000000000000000000000000000..7dfb063b36e7bffc8d77bfc43336a3355223acb8
--- /dev/null
+++ b/web/modules/redirect/src/EventSubscriber/RedirectSettingsCacheTag.php
@@ -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;
+  }
+
+}
diff --git a/web/modules/redirect/src/EventSubscriber/RouteNormalizerRequestSubscriber.php b/web/modules/redirect/src/EventSubscriber/RouteNormalizerRequestSubscriber.php
new file mode 100644
index 0000000000000000000000000000000000000000..800e022a568b2b3be2bfa0d0fe5ab45ceddfd5fa
--- /dev/null
+++ b/web/modules/redirect/src/EventSubscriber/RouteNormalizerRequestSubscriber.php
@@ -0,0 +1,143 @@
+<?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);
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  static function getSubscribedEvents() {
+    $events[KernelEvents::REQUEST][] = array('onKernelRequestRedirect', 30);
+    return $events;
+  }
+
+}
diff --git a/web/modules/redirect/src/Exception/RedirectLoopException.php b/web/modules/redirect/src/Exception/RedirectLoopException.php
new file mode 100644
index 0000000000000000000000000000000000000000..f2793f3e862d35c5aa4aeb8fbb6c64f98153c3be
--- /dev/null
+++ b/web/modules/redirect/src/Exception/RedirectLoopException.php
@@ -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]));
+  }
+
+}
diff --git a/web/modules/redirect/src/Form/RedirectDeleteForm.php b/web/modules/redirect/src/Form/RedirectDeleteForm.php
new file mode 100644
index 0000000000000000000000000000000000000000..eeee993d9c4c321eb094a75839a0b139aa06882d
--- /dev/null
+++ b/web/modules/redirect/src/Form/RedirectDeleteForm.php
@@ -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($this->t('The redirect %redirect has been deleted.', array('%redirect' => $this->entity->getRedirectUrl()->toString())));
+    $form_state->setRedirect('redirect.list');
+  }
+
+}
diff --git a/web/modules/redirect/src/Form/RedirectDeleteMultipleForm.php b/web/modules/redirect/src/Form/RedirectDeleteMultipleForm.php
new file mode 100644
index 0000000000000000000000000000000000000000..79c79467cc922e253322b51bb822214f938ed582
--- /dev/null
+++ b/web/modules/redirect/src/Form/RedirectDeleteMultipleForm.php
@@ -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 $this->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');
+  }
+
+}
diff --git a/web/modules/redirect/src/Form/RedirectForm.php b/web/modules/redirect/src/Form/RedirectForm.php
new file mode 100644
index 0000000000000000000000000000000000000000..702e99158023ef648b2ea1dee64e600a0738dce7
--- /dev/null
+++ b/web/modules/redirect/src/Form/RedirectForm.php
@@ -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($this->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] = $this->t('- All languages -');
+    }
+
+    $default_code = $redirect->getStatusCode() ? $redirect->getStatusCode() : \Drupal::config('redirect.settings')->get('default_status_code');
+
+    $form['status_code'] = array(
+      '#type' => 'select',
+      '#title' => $this->t('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')),
+      '#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', $this->t('It is not allowed to create a redirect from the front page.'));
+    }
+    if (strpos($source['path'], '#') !== FALSE) {
+      $form_state->setErrorByName('redirect_source', $this->t('The anchor fragments are not allowed.'));
+    }
+    if (strpos($source['path'], '/') === 0) {
+      $form_state->setErrorByName('redirect_source', $this->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', $this->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', $this->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($this->t('The redirect has been saved.'));
+    $form_state->setRedirect('redirect.list');
+  }
+}
diff --git a/web/modules/redirect/src/Form/RedirectSettingsForm.php b/web/modules/redirect/src/Form/RedirectSettingsForm.php
new file mode 100644
index 0000000000000000000000000000000000000000..029973dcc15c7420a39a4770bb345889056d1861
--- /dev/null
+++ b/web/modules/redirect/src/Form/RedirectSettingsForm.php
@@ -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 trailing 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($this->t('Configuration was saved.'));
+  }
+
+}
diff --git a/web/modules/redirect/src/Plugin/Action/DeleteRedirect.php b/web/modules/redirect/src/Plugin/Action/DeleteRedirect.php
new file mode 100644
index 0000000000000000000000000000000000000000..8b7f579727f266b3faaf66345a63175bc321c7c1
--- /dev/null
+++ b/web/modules/redirect/src/Plugin/Action/DeleteRedirect.php
@@ -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);
+  }
+
+}
diff --git a/web/modules/redirect/src/Plugin/Field/FieldFormatter/RedirectSourceFormatter.php b/web/modules/redirect/src/Plugin/Field/FieldFormatter/RedirectSourceFormatter.php
new file mode 100644
index 0000000000000000000000000000000000000000..7ef400de268c96147be4f8b0af4f6f799b9c74f3
--- /dev/null
+++ b/web/modules/redirect/src/Plugin/Field/FieldFormatter/RedirectSourceFormatter.php
@@ -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;
+  }
+
+}
diff --git a/web/modules/redirect/src/Plugin/Field/FieldType/RedirectSourceItem.php b/web/modules/redirect/src/Plugin/Field/FieldType/RedirectSourceItem.php
new file mode 100644
index 0000000000000000000000000000000000000000..53a0efc66a5f7bfd400acb961be481b1b1c8acf3
--- /dev/null
+++ b/web/modules/redirect/src/Plugin/Field/FieldType/RedirectSourceItem.php
@@ -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]);
+  }
+
+}
diff --git a/web/modules/redirect/src/Plugin/Field/FieldWidget/RedirectSourceWidget.php b/web/modules/redirect/src/Plugin/Field/FieldWidget/RedirectSourceWidget.php
new file mode 100644
index 0000000000000000000000000000000000000000..0a43411167b0115575c79819d7153b0e7f0b372b
--- /dev/null
+++ b/web/modules/redirect/src/Plugin/Field/FieldWidget/RedirectSourceWidget.php
@@ -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">' . $this->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">' . $this->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;
+  }
+}
diff --git a/web/modules/redirect/src/Plugin/Validation/Constraint/SourceLinkTypeConstraint.php b/web/modules/redirect/src/Plugin/Validation/Constraint/SourceLinkTypeConstraint.php
new file mode 100644
index 0000000000000000000000000000000000000000..5d354a07d232a9efa33ecc1a5515ca1d83e0ef99
--- /dev/null
+++ b/web/modules/redirect/src/Plugin/Validation/Constraint/SourceLinkTypeConstraint.php
@@ -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));
+      }
+    }
+  }
+}
+
diff --git a/web/modules/redirect/src/Plugin/migrate/process/PathRedirect.php b/web/modules/redirect/src/Plugin/migrate/process/PathRedirect.php
new file mode 100644
index 0000000000000000000000000000000000000000..ecff15446d884471efa15e056b2200f77443f2b5
--- /dev/null
+++ b/web/modules/redirect/src/Plugin/migrate/process/PathRedirect.php
@@ -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;
+  }
+
+}
diff --git a/web/modules/redirect/src/Plugin/migrate/process/d7/PathRedirect.php b/web/modules/redirect/src/Plugin/migrate/process/d7/PathRedirect.php
new file mode 100644
index 0000000000000000000000000000000000000000..0f4417096dffa739960a03e32900aea4ab15eb55
--- /dev/null
+++ b/web/modules/redirect/src/Plugin/migrate/process/d7/PathRedirect.php
@@ -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;
+  }
+
+}
\ No newline at end of file
diff --git a/web/modules/redirect/src/Plugin/migrate/process/d7/RedirectSourceQuery.php b/web/modules/redirect/src/Plugin/migrate/process/d7/RedirectSourceQuery.php
new file mode 100644
index 0000000000000000000000000000000000000000..8548d80a5f1529a3058c0a9b37b05bd9e26c4e63
--- /dev/null
+++ b/web/modules/redirect/src/Plugin/migrate/process/d7/RedirectSourceQuery.php
@@ -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;
+  }
+
+}
diff --git a/web/modules/redirect/src/Plugin/migrate/source/PathRedirect.php b/web/modules/redirect/src/Plugin/migrate/source/PathRedirect.php
new file mode 100644
index 0000000000000000000000000000000000000000..3ffd4b64edd513b324899edf2952d745dde8ef92
--- /dev/null
+++ b/web/modules/redirect/src/Plugin/migrate/source/PathRedirect.php
@@ -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 = "path_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;
+  }
+
+}
diff --git a/web/modules/redirect/src/Plugin/migrate/source/d7/PathRedirect.php b/web/modules/redirect/src/Plugin/migrate/source/d7/PathRedirect.php
new file mode 100644
index 0000000000000000000000000000000000000000..c9ec720d68b0531e193d716063d87c5454aeb480
--- /dev/null
+++ b/web/modules/redirect/src/Plugin/migrate/source/d7/PathRedirect.php
@@ -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;
+  }
+
+}
diff --git a/web/modules/redirect/src/RedirectChecker.php b/web/modules/redirect/src/RedirectChecker.php
new file mode 100644
index 0000000000000000000000000000000000000000..02da7ad2e90b3ba3efcd2dbf77e2d69ad5528965
--- /dev/null
+++ b/web/modules/redirect/src/RedirectChecker.php
@@ -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;
+  }
+
+}
diff --git a/web/modules/redirect/src/RedirectRepository.php b/web/modules/redirect/src/RedirectRepository.php
new file mode 100644
index 0000000000000000000000000000000000000000..236c927f6349c5724c3b2f5d63e559fbfd872ceb
--- /dev/null
+++ b/web/modules/redirect/src/RedirectRepository.php
@@ -0,0 +1,180 @@
+<?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();
+    $base_url = \Drupal::request()->getBaseUrl();
+    $generated_url = $uri->toString(TRUE);
+    $path = ltrim(substr($generated_url->getGeneratedUrl(), strlen($base_url)), '/');
+    $query = $uri->getOption('query') ?: [];
+    $return_value = $this->findMatchingRedirect($path, $query, $language);
+    return $return_value ? $return_value->addCacheableDependency($generated_url) : $return_value;
+  }
+
+  /**
+   * 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);
+  }
+}
diff --git a/web/modules/redirect/src/RedirectStorageSchema.php b/web/modules/redirect/src/RedirectStorageSchema.php
new file mode 100644
index 0000000000000000000000000000000000000000..e034362f109e316c565ab66f50fb52824e1bb654
--- /dev/null
+++ b/web/modules/redirect/src/RedirectStorageSchema.php
@@ -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;
+  }
+
+}
diff --git a/web/modules/redirect/src/RedirectViewsData.php b/web/modules/redirect/src/RedirectViewsData.php
new file mode 100644
index 0000000000000000000000000000000000000000..7e33ea51fc9ee6006945bfe4076ce0f6bd9041c1
--- /dev/null
+++ b/web/modules/redirect/src/RedirectViewsData.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace Drupal\redirect;
+
+use Drupal\views\EntityViewsData;
+
+/**
+ * Provides views integration for Redirect entities.
+ */
+class RedirectViewsData extends EntityViewsData {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getViewsData() {
+    $data = parent::getViewsData();
+
+    // Views defaults to the 'redirect_source' field that is configured as
+    // the redirect label. Since this is a composed field, change the default
+    // field to its 'path' value.
+    $data['redirect']['table']['base']['defaults']['field'] = 'redirect_source__path';
+    return $data;
+  }
+
+}
diff --git a/web/modules/redirect/src/Routing/RouteSubscriber.php b/web/modules/redirect/src/Routing/RouteSubscriber.php
new file mode 100644
index 0000000000000000000000000000000000000000..439093a5f1e0f8fe4cd290df137b18c82cf16aac
--- /dev/null
+++ b/web/modules/redirect/src/Routing/RouteSubscriber.php
@@ -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);
+    }
+  }
+
+}
diff --git a/web/modules/redirect/src/Tests/AssertRedirectTrait.php b/web/modules/redirect/src/Tests/AssertRedirectTrait.php
new file mode 100644
index 0000000000000000000000000000000000000000..ae9368886da3ef7c78775e1a1d38295a594228ba
--- /dev/null
+++ b/web/modules/redirect/src/Tests/AssertRedirectTrait.php
@@ -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);
+  }
+
+}
diff --git a/web/modules/redirect/src/Tests/GlobalRedirectTest.php b/web/modules/redirect/src/Tests/GlobalRedirectTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..75e1f99871aaabe83eb8458340acb17578b80a0d
--- /dev/null
+++ b/web/modules/redirect/src/Tests/GlobalRedirectTest.php
@@ -0,0 +1,296 @@
+<?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);
+  }
+
+  /**
+   * @inheritdoc}
+   */
+  protected function drupalHead($path, array $options = [], array $headers = []) {
+    // Always just use getAbsolutePath() so that generating the link does not
+    // alter special requests.
+    $url = $this->getAbsoluteUrl($path);
+    $out = $this->curlExec([CURLOPT_NOBODY => TRUE, CURLOPT_URL => $url, CURLOPT_HTTPHEADER => $headers]);
+    // Ensure that any changes to variables in the other thread are picked up.
+    $this->refreshVariables();
+
+    if ($this->dumpHeaders) {
+      $this->verbose('GET request to: ' . $path .
+        '<hr />Ending URL: ' . $this->getUrl() .
+        '<hr />Headers: <pre>' . Html::escape(var_export(array_map('trim', $this->headers), TRUE)) . '</pre>');
+    }
+
+    return $out;
+  }
+}
diff --git a/web/modules/redirect/src/Tests/RedirectUILanguageTest.php b/web/modules/redirect/src/Tests/RedirectUILanguageTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..6d9a72791893370b16188adb0b4c0a5e6daf14ce
--- /dev/null
+++ b/web/modules/redirect/src/Tests/RedirectUILanguageTest.php
@@ -0,0 +1,125 @@
+<?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');
+  }
+
+  /**
+   * Test editing the redirect language.
+   */
+  public function testEditRedirectLanguage() {
+    $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]' => 'en',
+    ), t('Save'));
+
+    // Check redirect for english.
+    $this->assertRedirect('langpath', '/user', 'HTTP/1.1 301 Moved Permanently');
+
+    // Check that redirect for Germany is not working.
+    $this->assertRedirect('de/langpath', NULL, 'HTTP/1.1 404 Not Found');
+
+    // Edit the redirect and change the language.
+    $this->drupalGet('admin/config/search/redirect');
+    $this->clickLink('Edit');
+    $this->drupalPostForm(NULL, ['language[0][value]' => 'de'], t('Save'));
+
+    // Check redirect for english is NOT working now.
+    $this->assertRedirect('langpath', NULL, 'HTTP/1.1 404 Not Found');
+
+    // Check that redirect for Germany is now working.
+    $this->assertRedirect('de/langpath', '/de/user', 'HTTP/1.1 301 Moved Permanently');
+  }
+
+}
diff --git a/web/modules/redirect/src/Tests/RedirectUITest.php b/web/modules/redirect/src/Tests/RedirectUITest.php
new file mode 100644
index 0000000000000000000000000000000000000000..67b066614c9ee8127e33aa1f98f0ca30c14597bc
--- /dev/null
+++ b/web/modules/redirect/src/Tests/RedirectUITest.php
@@ -0,0 +1,436 @@
+<?php
+
+namespace Drupal\redirect\Tests;
+
+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' => mb_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);
+  }
+
+}
diff --git a/web/modules/redirect/tests/fixtures/drupal6.php b/web/modules/redirect/tests/fixtures/drupal6.php
new file mode 100644
index 0000000000000000000000000000000000000000..e6f9c47588326fdec6bdecc1cd63fcc7a42c4dd5
--- /dev/null
+++ b/web/modules/redirect/tests/fixtures/drupal6.php
@@ -0,0 +1,186 @@
+<?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/path_redirect/path_redirect.module',
+    'name' => 'path_redirect',
+    'type' => 'module',
+    'owner' => '',
+    'status' => '1',
+    'bootstrap' => '0',
+    'schema_version' => '7000',
+    'weight' => '0',
+    'info' => 'a:10:{s:4:"name";s:13:"Path 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:13:"path_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();
diff --git a/web/modules/redirect/tests/fixtures/drupal7.php b/web/modules/redirect/tests/fixtures/drupal7.php
new file mode 100644
index 0000000000000000000000000000000000000000..20510761775f43ca7538ee0e2d9890cfbb4423c4
--- /dev/null
+++ b/web/modules/redirect/tests/fixtures/drupal7.php
@@ -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();
diff --git a/web/modules/redirect/tests/src/Kernel/Migrate/d6/PathRedirectTest.php b/web/modules/redirect/tests/src/Kernel/Migrate/d6/PathRedirectTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..9383adcd1ad2d343b0e4bfd25d310641e8cc34da
--- /dev/null
+++ b/web/modules/redirect/tests/src/Kernel/Migrate/d6/PathRedirectTest.php
@@ -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());
+  }
+}
diff --git a/web/modules/redirect/tests/src/Kernel/Migrate/d7/PathRedirectTest.php b/web/modules/redirect/tests/src/Kernel/Migrate/d7/PathRedirectTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..469e5ccd5c65efc22980ecea1700466271e8b62f
--- /dev/null
+++ b/web/modules/redirect/tests/src/Kernel/Migrate/d7/PathRedirectTest.php
@@ -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');
+  }
+}
diff --git a/web/modules/redirect/tests/src/Kernel/RedirectAPITest.php b/web/modules/redirect/tests/src/Kernel/RedirectAPITest.php
new file mode 100644
index 0000000000000000000000000000000000000000..6d982f7c1f5da9bdb49e76de1805a03a26dbc102
--- /dev/null
+++ b/web/modules/redirect/tests/src/Kernel/RedirectAPITest.php
@@ -0,0 +1,305 @@
+<?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.');
+    }
+
+    // Add a redirect to an external URL.
+    $external_redirect = $this->controller->create();
+    $external_redirect->setSource('google');
+    $external_redirect->setRedirect('https://google.com');
+    $external_redirect->save();
+    $found = $repository->findMatchingRedirect('google');
+    if (!empty($found)) {
+      $this->assertEqual($found->getRedirectUrl()->toString(), 'https://google.com');
+    }
+    else {
+      $this->fail('Failed to find a redirect for google.');
+    }
+
+    // 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.');
+    }
+  }
+
+}
diff --git a/web/modules/redirect/tests/src/Unit/Migrate/d7/PathRedirectTest.php b/web/modules/redirect/tests/src/Unit/Migrate/d7/PathRedirectTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..8a5c593e6b44073a5c70642699d470c90c902665
--- /dev/null
+++ b/web/modules/redirect/tests/src/Unit/Migrate/d7/PathRedirectTest.php
@@ -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();
+  }
+
+}
diff --git a/web/modules/redirect/tests/src/Unit/RedirectCheckerTest.php b/web/modules/redirect/tests/src/Unit/RedirectCheckerTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..35f82332323dd7266858cf45e8eaf58cad0f3f63
--- /dev/null
+++ b/web/modules/redirect/tests/src/Unit/RedirectCheckerTest.php
@@ -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;
+  }
+
+}
diff --git a/web/modules/redirect/tests/src/Unit/RedirectRequestSubscriberTest.php b/web/modules/redirect/tests/src/Unit/RedirectRequestSubscriberTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..e664e95bab49756366a8dd9c1b893a3f5c9c534f
--- /dev/null
+++ b/web/modules/redirect/tests/src/Unit/RedirectRequestSubscriberTest.php
@@ -0,0 +1,301 @@
+<?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']],
+      ['system/files/file.txt', [], '/test-path', []],
+    ];
+  }
+
+  /**
+   * 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));
+
+    $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)
+      ->willReturnCallback(function ($path, Request $request) {
+        if (strpos($path, '/system/files/') === 0 && !$request->query->has('file')) {
+          // Private files paths are split by the inbound path processor and the
+          // relative file path is moved to the 'file' query string parameter.
+          // This is because the route system does not allow an arbitrary amount
+          // of parameters.
+          // @see \Drupal\system\PathProcessor\PathProcessorFiles::processInbound()
+          $path = '/system/files';
+        }
+        return $path;
+      });
+
+    $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();
+
+    if ($method === 'findMatchingRedirect') {
+      $repository->expects($this->any())
+        ->method($method)
+        ->willReturnCallback(function ($source_path) use ($redirect) {
+          // No redirect with source path 'system/files' exists. The stored
+          // redirect has 'system/files/file.txt' as source path.
+          return $source_path === 'system/files' ? NULL : $redirect;
+        });
+    }
+    else {
+      $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;
+  }
+
+}
diff --git a/web/modules/redirect/tests/src/Unit/RouteNormalizerRequestSubscriberTest.php b/web/modules/redirect/tests/src/Unit/RouteNormalizerRequestSubscriberTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..87d3c313584e30bfa1b068ad405f53f61666a3fd
--- /dev/null
+++ b/web/modules/redirect/tests/src/Unit/RouteNormalizerRequestSubscriberTest.php
@@ -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);
+  }
+
+}