diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 000000000..cf5687650
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,3 @@
+# These are supported funding model platforms
+
+github: bugzilla
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 000000000..f6e5b3b6f
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,26 @@
+<!--
+
+#### Instructions
+
+1. All pull requests to an officially supported branch must be accompanied by a bug report on bugzilla.mozilla.org. Please stop and file one there if you haven't already, or look for an existing one that fits what you're doing (the bug entry form will automatically search for things that look similar to the Summary you enter).
+   File a bug report: https://bugzilla.mozilla.org/enter_bug.cgi?product=Bugzilla
+2. GitHub will automatically attach your pull request to the bug report in Bugzilla if you have "Bug #######" in the title of the pull request, where ####### is the bug number. For readability, we recommend putting it at the beginning of the title.
+3. If it's not otherwise obvious from the title which branch the pull request applies to, please put the name of the branch in square brackets at the beginning of the title.
+
+ Example title:
+   [5.2] Bug 1234: Change Foo to do Bar
+-->
+
+#### Details
+<!-- Explain what you did -->
+This PR fixes/adds a feature...
+
+#### Additional info
+<!-- Paste the bug number after the # and after the = in the following line. -->
+* [bug#](https://bugzilla.mozilla.org/show_bug.cgi?id=)
+
+#### Test Plan
+<!-- How did you verify the fix/feature in steps -->
+1. Open the show_bug view
+2. Edit the bug
+3. ...
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 000000000..6d7ea079b
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,74 @@
+# This is a basic workflow to help you get started with Actions
+
+name: Release Tests
+
+# Controls when the action will run. Triggers the workflow on push or pull request
+# events but only for the main branch
+on:
+  push:
+    branches: [ 5.2 ]
+  pull_request:
+    branches: [ 5.2 ]
+  # Allows you to run this workflow manually from the Actions tab
+  workflow_dispatch:
+
+# A workflow run is made up of one or more jobs that can run sequentially or in parallel
+jobs:
+  ubuntu:
+    name: Release Tests on Ubuntu 20.04
+    runs-on: ubuntu-20.04
+    steps:
+      - name: Checkout the repository
+        uses: actions/checkout@v4
+      - name: apt install
+        run: |
+          sudo apt-get update
+          sudo apt-get -y dist-upgrade
+          sudo apt-get install --ignore-hold --allow-downgrades -y \
+          apache2 \
+          mariadb-client-10.3 \
+          netcat \
+          libappconfig-perl \
+          libdate-calc-perl \
+          libtemplate-perl \
+          build-essential \
+          libdatetime-timezone-perl \
+          libdatetime-perl \
+          libemail-address-perl \
+          libemail-sender-perl \
+          libemail-mime-perl \
+          libemail-mime-modifier-perl \
+          libdbi-perl \
+          libdbix-connector-perl \
+          libdbd-mysql-perl \
+          libcgi-pm-perl \
+          libmath-random-isaac-perl \
+          libmath-random-isaac-xs-perl \
+          libapache2-mod-perl2 \
+          libapache2-mod-perl2-dev \
+          libchart-perl \
+          libxml-perl \
+          libxml-twig-perl \
+          perlmagick \
+          libgd-graph-perl \
+          libtemplate-plugin-gd-perl \
+          libsoap-lite-perl \
+          libhtml-scrubber-perl \
+          libjson-rpc-perl \
+          libdaemon-generic-perl \
+          libtheschwartz-perl \
+          libtest-taint-perl \
+          libauthen-radius-perl \
+          libfile-slurp-perl \
+          libencode-detect-perl \
+          libmodule-build-perl \
+          libnet-ldap-perl \
+          libauthen-sasl-perl \
+          libfile-mimeinfo-perl \
+          libhtml-formattext-withlinks-perl \
+          libpod-coverage-perl \
+          graphviz
+      - name: Get Perl Version and debug info
+        run: '/usr/bin/perl -V'
+      - name: Run tests
+        run: '/usr/bin/perl runtests.pl'
diff --git a/.gitignore b/.gitignore
index ba98f70c2..46336f825 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,6 +9,4 @@
 /data
 /localconfig
 /index.html
-
-/skins/contrib/Dusk/admin.css
-/skins/contrib/Dusk/bug.css
+.DS_Store
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
new file mode 100644
index 000000000..ff4cc83b4
--- /dev/null
+++ b/.readthedocs.yaml
@@ -0,0 +1,17 @@
+version: 2
+build:
+  os: "ubuntu-22.04"
+  tools:
+    python: "3.11"
+  apt_packages:
+    - libfile-copy-recursive-perl
+  jobs:
+    post_build:
+      - perl docs/makedocs.pl --pod-only
+      - cp -rp docs/en/html/integrating/api/ "$READTHEDOCS_OUTPUT/html/integrating/api"
+      - cp -p docs/en/html/style.css "$READTHEDOCS_OUTPUT/html/"
+sphinx:
+  configuration: docs/en/rst/conf.py
+python:
+  install:
+    - requirements: docs/requirements.txt
diff --git a/Bugzilla.pm b/Bugzilla.pm
index deffa259e..12d9be33f 100644
--- a/Bugzilla.pm
+++ b/Bugzilla.pm
@@ -7,10 +7,12 @@
 
 package Bugzilla;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
+BEGIN { eval { utf8->import; require 'utf8_heavy.pl' }; }
+
 # We want any compile errors to get to the browser, if possible.
 BEGIN {
   # This makes sure we're in a CGI.
@@ -459,7 +461,6 @@ sub error_mode {
     $class->request_cache->{error_mode} = $newval;
   }
 
-  # XXX - Once we require Perl 5.10.1, this test can be replaced by //.
   if (exists $class->request_cache->{error_mode}) {
     return $class->request_cache->{error_mode};
   }
@@ -507,7 +508,6 @@ sub usage_mode {
     $class->request_cache->{usage_mode} = $newval;
   }
 
-  # XXX - Once we require Perl 5.10.1, this test can be replaced by //.
   if (exists $class->request_cache->{usage_mode}) {
     return $class->request_cache->{usage_mode};
   }
@@ -596,7 +596,7 @@ sub fields {
   my $fields = $cache->{fields};
   my %requested;
   if (my $types = delete $criteria->{type}) {
-    $types = ref($types) ? $types : [$types];
+    $types     = ref($types) ? $types : [$types];
     %requested = map { %{$fields->{by_type}->{$_} || {}} } @$types;
   }
   else {
@@ -701,7 +701,6 @@ sub _cleanup {
   foreach my $dbh ($main, $shadow) {
     next if !$dbh;
     $dbh->bz_rollback_transaction() if $dbh->bz_in_transaction;
-    $dbh->disconnect;
   }
   my $smtp = $cache->{smtp};
   $smtp->disconnect if $smtp;
diff --git a/Bugzilla/Attachment.pm b/Bugzilla/Attachment.pm
index 26f768c2f..4e81e3166 100644
--- a/Bugzilla/Attachment.pm
+++ b/Bugzilla/Attachment.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::Attachment;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
@@ -48,7 +48,7 @@ use File::Copy;
 use List::Util qw(max);
 use Storable qw(dclone);
 
-use parent qw(Bugzilla::Object);
+use base qw(Bugzilla::Object);
 
 ###############################
 ####    Initialization     ####
@@ -619,7 +619,7 @@ sub _check_is_private {
   if (
     (
          (!ref $invocant && $is_private)
-      || (ref $invocant && $invocant->isprivate != $is_private)
+      || (ref $invocant  && $invocant->isprivate != $is_private)
     )
     && !Bugzilla->user->is_insider
     )
diff --git a/Bugzilla/Attachment/PatchReader.pm b/Bugzilla/Attachment/PatchReader.pm
index 01ed51518..fd9a33d68 100644
--- a/Bugzilla/Attachment/PatchReader.pm
+++ b/Bugzilla/Attachment/PatchReader.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::Attachment::PatchReader;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/Bugzilla/Auth.pm b/Bugzilla/Auth.pm
index 23de9b4bd..39145198e 100644
--- a/Bugzilla/Auth.pm
+++ b/Bugzilla/Auth.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::Auth;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/Bugzilla/Auth/Login.pm b/Bugzilla/Auth/Login.pm
index 68c7464f2..50f454c10 100644
--- a/Bugzilla/Auth/Login.pm
+++ b/Bugzilla/Auth/Login.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::Auth::Login;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/Bugzilla/Auth/Login/APIKey.pm b/Bugzilla/Auth/Login/APIKey.pm
index 79c16825e..fdb7475ff 100644
--- a/Bugzilla/Auth/Login/APIKey.pm
+++ b/Bugzilla/Auth/Login/APIKey.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::Auth::Login::APIKey;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/Bugzilla/Auth/Login/CGI.pm b/Bugzilla/Auth/Login/CGI.pm
index 0824f1ebd..c3152be46 100644
--- a/Bugzilla/Auth/Login/CGI.pm
+++ b/Bugzilla/Auth/Login/CGI.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::Auth::Login::CGI;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Bugzilla::Auth::Login);
+use base qw(Bugzilla::Auth::Login);
 use constant user_can_create_account => 1;
 
 use Bugzilla::Constants;
diff --git a/Bugzilla/Auth/Login/Cookie.pm b/Bugzilla/Auth/Login/Cookie.pm
index 1983dbd4c..37b033036 100644
--- a/Bugzilla/Auth/Login/Cookie.pm
+++ b/Bugzilla/Auth/Login/Cookie.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::Auth::Login::Cookie;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/Bugzilla/Auth/Login/Env.pm b/Bugzilla/Auth/Login/Env.pm
index 5fc33921b..d5f859063 100644
--- a/Bugzilla/Auth/Login/Env.pm
+++ b/Bugzilla/Auth/Login/Env.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::Auth::Login::Env;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Bugzilla::Auth::Login);
+use base qw(Bugzilla::Auth::Login);
 
 use Bugzilla::Constants;
 use Bugzilla::Error;
diff --git a/Bugzilla/Auth/Login/Stack.pm b/Bugzilla/Auth/Login/Stack.pm
index 7786f26c8..fc356d67d 100644
--- a/Bugzilla/Auth/Login/Stack.pm
+++ b/Bugzilla/Auth/Login/Stack.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::Auth::Login::Stack;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/Bugzilla/Auth/Persist/Cookie.pm b/Bugzilla/Auth/Persist/Cookie.pm
index af6b0d77d..5712f0b45 100644
--- a/Bugzilla/Auth/Persist/Cookie.pm
+++ b/Bugzilla/Auth/Persist/Cookie.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::Auth::Persist::Cookie;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/Bugzilla/Auth/Verify.pm b/Bugzilla/Auth/Verify.pm
index 639760c2b..6fd6687f7 100644
--- a/Bugzilla/Auth/Verify.pm
+++ b/Bugzilla/Auth/Verify.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::Auth::Verify;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/Bugzilla/Auth/Verify/DB.pm b/Bugzilla/Auth/Verify/DB.pm
index 6ed9ba15c..b8b8c2ae9 100644
--- a/Bugzilla/Auth/Verify/DB.pm
+++ b/Bugzilla/Auth/Verify/DB.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::Auth::Verify::DB;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Bugzilla::Auth::Verify);
+use base qw(Bugzilla::Auth::Verify);
 
 use Bugzilla::Constants;
 use Bugzilla::Token;
diff --git a/Bugzilla/Auth/Verify/LDAP.pm b/Bugzilla/Auth/Verify/LDAP.pm
index c92a38909..c7561187f 100644
--- a/Bugzilla/Auth/Verify/LDAP.pm
+++ b/Bugzilla/Auth/Verify/LDAP.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::Auth::Verify::LDAP;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/Bugzilla/Auth/Verify/RADIUS.pm b/Bugzilla/Auth/Verify/RADIUS.pm
index 2cbde0404..a715d847d 100644
--- a/Bugzilla/Auth/Verify/RADIUS.pm
+++ b/Bugzilla/Auth/Verify/RADIUS.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::Auth::Verify::RADIUS;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Bugzilla::Auth::Verify);
+use base qw(Bugzilla::Auth::Verify);
 
 use Bugzilla::Constants;
 use Bugzilla::Error;
diff --git a/Bugzilla/Auth/Verify/Stack.pm b/Bugzilla/Auth/Verify/Stack.pm
index 9a9412915..a54432a19 100644
--- a/Bugzilla/Auth/Verify/Stack.pm
+++ b/Bugzilla/Auth/Verify/Stack.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::Auth::Verify::Stack;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/Bugzilla/Bug.pm b/Bugzilla/Bug.pm
index bce3661f1..d34a5cc13 100644
--- a/Bugzilla/Bug.pm
+++ b/Bugzilla/Bug.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::Bug;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
@@ -36,7 +36,7 @@ use List::Util qw(min max first);
 use Storable qw(dclone);
 use Scalar::Util qw(blessed);
 
-use parent qw(Bugzilla::Object Exporter);
+use base qw(Bugzilla::Object Exporter);
 @Bugzilla::Bug::EXPORT = qw(
   bug_alias_to_id
   LogActivityEntry
@@ -620,7 +620,7 @@ sub possible_duplicates {
     foreach my $word (@words) {
       my ($term, $rel_term)
         = $dbh->sql_fulltext_search('bugs_fulltext.short_desc', $word);
-      push(@where, $term);
+      push(@where,     $term);
       push(@relevance, $rel_term || $term);
     }
 
@@ -1554,8 +1554,8 @@ sub _check_bug_status {
     ThrowUserError(
       'comment_required',
       {
-        old => $old_status ? $old_status->name : undef,
-        new => $new_status->name,
+        old   => $old_status ? $old_status->name : undef,
+        new   => $new_status->name,
         field => 'bug_status'
       }
     );
@@ -3755,17 +3755,6 @@ sub comments {
     foreach my $comment (@{$self->{'comments'}}) {
       $comment->{count} = $count++;
       $comment->{bug}   = $self;
-
-      # XXX - hack for MySQL. Convert [U+....] back into its Unicode
-      # equivalent for characters above U+FFFF as MySQL older than 5.5.3
-      # cannot store them, see Bugzilla::Comment::_check_thetext().
-      if ($is_mysql) {
-
-        # Perl 5.13.8 and older complain about non-characters.
-        no warnings 'utf8';
-        $comment->{thetext}
-          =~ s/\x{FDD0}\[U\+((?:[1-9A-F]|10)[0-9A-F]{4})\]\x{FDD1}/chr(hex $1)/eg;
-      }
     }
 
     # Some bugs may have no comments when upgrading old installations.
@@ -3995,7 +3984,7 @@ sub groups {
       . " THEN 1 ELSE 0 END,"
       . " CASE WHEN groups.id IN($grouplist) THEN 1 ELSE 0 END,"
       . " isactive, membercontrol, othercontrol"
-      . " FROM groups"
+      . " FROM " . $dbh->quote_identifier('groups')
       . " LEFT JOIN bug_group_map"
       . " ON bug_group_map.group_id = groups.id"
       . " AND bug_id = ?"
diff --git a/Bugzilla/BugMail.pm b/Bugzilla/BugMail.pm
index 18795d735..3fd00d5c4 100644
--- a/Bugzilla/BugMail.pm
+++ b/Bugzilla/BugMail.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::BugMail;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
@@ -21,6 +21,7 @@ use Bugzilla::Mailer;
 use Bugzilla::Hook;
 use Bugzilla::MIME;
 
+use Encode qw();
 use Date::Parse;
 use Date::Format;
 use Scalar::Util qw(blessed);
@@ -478,6 +479,7 @@ sub _generate_bugmail {
       encoding     => 'quoted-printable',
     },
     body_str => $msg_text,
+    encode_check => Encode::FB_DEFAULT
   ));
 
   if ($user->setting('email_format') eq 'html') {
@@ -491,14 +493,15 @@ sub _generate_bugmail {
         encoding     => 'quoted-printable',
       },
       body_str => $msg_html,
+      encode_check => Encode::FB_DEFAULT
       );
   }
 
   my $email = Bugzilla::MIME->new($msg_header);
-  if (scalar(@parts) == 1) {
-    $email->content_type_set($parts[0]->content_type);
-  }
-  else {
+
+  # If there's only one part, we don't need to set the overall content type
+  # because Email::MIME will automatically take it from that part (bug 1657496)
+  if (scalar(@parts) > 1) {
     $email->content_type_set('multipart/alternative');
 
     # Some mail clients need same encoding for each part, even empty ones.
diff --git a/Bugzilla/BugUrl.pm b/Bugzilla/BugUrl.pm
index c3e11e4c7..0369ec1e1 100644
--- a/Bugzilla/BugUrl.pm
+++ b/Bugzilla/BugUrl.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::BugUrl;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Bugzilla::Object);
+use base qw(Bugzilla::Object);
 
 use Bugzilla::Util;
 use Bugzilla::Error;
diff --git a/Bugzilla/BugUrl/Bugzilla.pm b/Bugzilla/BugUrl/Bugzilla.pm
index 9d036100f..c316d8fca 100644
--- a/Bugzilla/BugUrl/Bugzilla.pm
+++ b/Bugzilla/BugUrl/Bugzilla.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::BugUrl::Bugzilla;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Bugzilla::BugUrl);
+use base qw(Bugzilla::BugUrl);
 
 use Bugzilla::Error;
 use Bugzilla::Util;
diff --git a/Bugzilla/BugUrl/Bugzilla/Local.pm b/Bugzilla/BugUrl/Bugzilla/Local.pm
index 3fe0fcb5d..fcfb33b6e 100644
--- a/Bugzilla/BugUrl/Bugzilla/Local.pm
+++ b/Bugzilla/BugUrl/Bugzilla/Local.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::BugUrl::Bugzilla::Local;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Bugzilla::BugUrl::Bugzilla);
+use base qw(Bugzilla::BugUrl::Bugzilla);
 
 use Bugzilla::Error;
 use Bugzilla::Util;
diff --git a/Bugzilla/BugUrl/Debian.pm b/Bugzilla/BugUrl/Debian.pm
index f49f2b820..d1b263317 100644
--- a/Bugzilla/BugUrl/Debian.pm
+++ b/Bugzilla/BugUrl/Debian.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::BugUrl::Debian;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Bugzilla::BugUrl);
+use base qw(Bugzilla::BugUrl);
 
 ###############################
 ####        Methods        ####
diff --git a/Bugzilla/BugUrl/GitHub.pm b/Bugzilla/BugUrl/GitHub.pm
index 583837a60..795a8e317 100644
--- a/Bugzilla/BugUrl/GitHub.pm
+++ b/Bugzilla/BugUrl/GitHub.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::BugUrl::GitHub;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Bugzilla::BugUrl);
+use base qw(Bugzilla::BugUrl);
 
 ###############################
 ####        Methods        ####
diff --git a/Bugzilla/BugUrl/Google.pm b/Bugzilla/BugUrl/Google.pm
index 106425302..e59655d68 100644
--- a/Bugzilla/BugUrl/Google.pm
+++ b/Bugzilla/BugUrl/Google.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::BugUrl::Google;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Bugzilla::BugUrl);
+use base qw(Bugzilla::BugUrl);
 
 ###############################
 ####        Methods        ####
diff --git a/Bugzilla/BugUrl/JIRA.pm b/Bugzilla/BugUrl/JIRA.pm
index b42b1decc..251f05427 100644
--- a/Bugzilla/BugUrl/JIRA.pm
+++ b/Bugzilla/BugUrl/JIRA.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::BugUrl::JIRA;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Bugzilla::BugUrl);
+use base qw(Bugzilla::BugUrl);
 
 ###############################
 ####        Methods        ####
diff --git a/Bugzilla/BugUrl/Launchpad.pm b/Bugzilla/BugUrl/Launchpad.pm
index 5be8088d1..e27795773 100644
--- a/Bugzilla/BugUrl/Launchpad.pm
+++ b/Bugzilla/BugUrl/Launchpad.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::BugUrl::Launchpad;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Bugzilla::BugUrl);
+use base qw(Bugzilla::BugUrl);
 
 ###############################
 ####        Methods        ####
diff --git a/Bugzilla/BugUrl/MantisBT.pm b/Bugzilla/BugUrl/MantisBT.pm
index 742ae1a47..8b7e66669 100644
--- a/Bugzilla/BugUrl/MantisBT.pm
+++ b/Bugzilla/BugUrl/MantisBT.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::BugUrl::MantisBT;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Bugzilla::BugUrl);
+use base qw(Bugzilla::BugUrl);
 
 ###############################
 ####        Methods        ####
diff --git a/Bugzilla/BugUrl/SourceForge.pm b/Bugzilla/BugUrl/SourceForge.pm
index ffdde42f4..a0640e136 100644
--- a/Bugzilla/BugUrl/SourceForge.pm
+++ b/Bugzilla/BugUrl/SourceForge.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::BugUrl::SourceForge;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Bugzilla::BugUrl);
+use base qw(Bugzilla::BugUrl);
 
 ###############################
 ####        Methods        ####
diff --git a/Bugzilla/BugUrl/Trac.pm b/Bugzilla/BugUrl/Trac.pm
index 22418a1df..51dd414ff 100644
--- a/Bugzilla/BugUrl/Trac.pm
+++ b/Bugzilla/BugUrl/Trac.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::BugUrl::Trac;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Bugzilla::BugUrl);
+use base qw(Bugzilla::BugUrl);
 
 ###############################
 ####        Methods        ####
diff --git a/Bugzilla/BugUserLastVisit.pm b/Bugzilla/BugUserLastVisit.pm
index d1c351959..052574a2f 100644
--- a/Bugzilla/BugUserLastVisit.pm
+++ b/Bugzilla/BugUserLastVisit.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::BugUserLastVisit;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Bugzilla::Object);
+use base qw(Bugzilla::Object);
 
 #####################################################################
 # Overriden Constants that are used as methods
diff --git a/Bugzilla/CGI.pm b/Bugzilla/CGI.pm
index 019fb8d10..fa8b18b16 100644
--- a/Bugzilla/CGI.pm
+++ b/Bugzilla/CGI.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::CGI;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(CGI);
+use base qw(CGI);
 
 use Bugzilla::Constants;
 use Bugzilla::Error;
@@ -36,7 +36,7 @@ sub _init_bz_cgi_globals {
   # We don't precompile any functions here, that's done specially in
   # mod_perl code.
   $invocant->_setup_symbols(qw(:no_xhtml :oldstyle_urls :private_tempfiles
-      :unique_headers));
+  :unique_headers));
 }
 
 BEGIN { __PACKAGE__->_init_bz_cgi_globals() if i_am_cgi(); }
diff --git a/Bugzilla/Chart.pm b/Bugzilla/Chart.pm
index 1abf3dbe4..551f93c7e 100644
--- a/Bugzilla/Chart.pm
+++ b/Bugzilla/Chart.pm
@@ -13,7 +13,7 @@
 # the same points.
 package Bugzilla::Chart;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/Bugzilla/Classification.pm b/Bugzilla/Classification.pm
index 1ea86f592..9e7275d53 100644
--- a/Bugzilla/Classification.pm
+++ b/Bugzilla/Classification.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::Classification;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
@@ -17,7 +17,7 @@ use Bugzilla::Util;
 use Bugzilla::Error;
 use Bugzilla::Product;
 
-use parent qw(Bugzilla::Field::ChoiceInterface Bugzilla::Object Exporter);
+use base qw(Bugzilla::Field::ChoiceInterface Bugzilla::Object Exporter);
 @Bugzilla::Classification::EXPORT = qw(sort_products_by_classification);
 
 ###############################
diff --git a/Bugzilla/Comment.pm b/Bugzilla/Comment.pm
index 575703b4b..ac5341b1e 100644
--- a/Bugzilla/Comment.pm
+++ b/Bugzilla/Comment.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::Comment;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Bugzilla::Object);
+use base qw(Bugzilla::Object);
 
 use Bugzilla::Attachment;
 use Bugzilla::Comment::TagWeights;
@@ -440,20 +440,6 @@ sub _check_thetext {
   $thetext =~ s/\s*$//s;
   $thetext =~ s/\r\n?/\n/g;    # Get rid of \r.
 
-  # Characters above U+FFFF cannot be stored by MySQL older than 5.5.3 as they
-  # require the new utf8mb4 character set. Other DB servers are handling them
-  # without any problem. So we need to replace these characters if we use MySQL,
-  # else the comment is truncated.
-  # XXX - Once we use utf8mb4 for comments, this hack for MySQL can go away.
-  state $is_mysql = Bugzilla->dbh->isa('Bugzilla::DB::Mysql') ? 1 : 0;
-  if ($is_mysql) {
-
-    # Perl 5.13.8 and older complain about non-characters.
-    no warnings 'utf8';
-    $thetext
-      =~ s/([\x{10000}-\x{10FFFF}])/"\x{FDD0}[" . uc(sprintf('U+%04x', ord($1))) . "]\x{FDD1}"/eg;
-  }
-
   ThrowUserError('comment_too_long') if length($thetext) > MAX_COMMENT_LENGTH;
   return $thetext;
 }
diff --git a/Bugzilla/Comment/TagWeights.pm b/Bugzilla/Comment/TagWeights.pm
index 5355cad7f..210873c91 100644
--- a/Bugzilla/Comment/TagWeights.pm
+++ b/Bugzilla/Comment/TagWeights.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::Comment::TagWeights;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Bugzilla::Object);
+use base qw(Bugzilla::Object);
 
 use Bugzilla::Constants;
 
diff --git a/Bugzilla/Component.pm b/Bugzilla/Component.pm
index ef8180eb0..de1132a53 100644
--- a/Bugzilla/Component.pm
+++ b/Bugzilla/Component.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::Component;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Bugzilla::Field::ChoiceInterface Bugzilla::Object);
+use base qw(Bugzilla::Field::ChoiceInterface Bugzilla::Object);
 
 use Bugzilla::Constants;
 use Bugzilla::Util;
diff --git a/Bugzilla/Config.pm b/Bugzilla/Config.pm
index 1aa944985..fc604cf09 100644
--- a/Bugzilla/Config.pm
+++ b/Bugzilla/Config.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::Config;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Exporter);
+use base qw(Exporter);
 use autodie qw(:default);
 
 use Bugzilla::Constants;
@@ -341,6 +341,7 @@ sub read_param_file {
     die "The $file file does not exist."
       . ' You probably need to run checksetup.pl.',;
   }
+
   return \%params;
 }
 
diff --git a/Bugzilla/Config/Admin.pm b/Bugzilla/Config/Admin.pm
index fe19d7cf0..44173366f 100644
--- a/Bugzilla/Config/Admin.pm
+++ b/Bugzilla/Config/Admin.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::Config::Admin;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/Bugzilla/Config/Advanced.pm b/Bugzilla/Config/Advanced.pm
index 043a892d7..203a9f7d8 100644
--- a/Bugzilla/Config/Advanced.pm
+++ b/Bugzilla/Config/Advanced.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::Config::Advanced;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/Bugzilla/Config/Attachment.pm b/Bugzilla/Config/Attachment.pm
index 0cf4b768a..504eb01e7 100644
--- a/Bugzilla/Config/Attachment.pm
+++ b/Bugzilla/Config/Attachment.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::Config::Attachment;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/Bugzilla/Config/Auth.pm b/Bugzilla/Config/Auth.pm
index 09e81339f..43dcda8f4 100644
--- a/Bugzilla/Config/Auth.pm
+++ b/Bugzilla/Config/Auth.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::Config::Auth;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/Bugzilla/Config/BugChange.pm b/Bugzilla/Config/BugChange.pm
index ad1cafefc..e2ddf7b56 100644
--- a/Bugzilla/Config/BugChange.pm
+++ b/Bugzilla/Config/BugChange.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::Config::BugChange;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/Bugzilla/Config/BugFields.pm b/Bugzilla/Config/BugFields.pm
index 1659dc66a..adc6fe7cc 100644
--- a/Bugzilla/Config/BugFields.pm
+++ b/Bugzilla/Config/BugFields.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::Config::BugFields;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/Bugzilla/Config/Common.pm b/Bugzilla/Config/Common.pm
index a0077b5b2..86103c8f1 100644
--- a/Bugzilla/Config/Common.pm
+++ b/Bugzilla/Config/Common.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::Config::Common;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use Email::Address;
+use Email::Address::XS;
 use Socket;
 
 use Bugzilla::Util;
@@ -20,7 +20,7 @@ use Bugzilla::Field;
 use Bugzilla::Group;
 use Bugzilla::Status;
 
-use parent qw(Exporter);
+use base qw(Exporter);
 @Bugzilla::Config::Common::EXPORT
   = qw(check_multi check_numeric check_regexp check_url check_group
   check_sslbase check_priority check_severity check_platform
@@ -80,7 +80,8 @@ sub check_regexp {
 
 sub check_email {
   my ($value) = @_;
-  if ($value !~ $Email::Address::mailbox) {
+  my ($mbox) = Email::Address::XS->parse($value);
+  if (!$mbox->is_valid) {
     return "must be a valid email address.";
   }
   return "";
@@ -123,15 +124,19 @@ sub check_ip {
 }
 
 sub check_utf8 {
-  my $utf8 = shift;
-
-  # You cannot turn off the UTF-8 parameter if you've already converted
-  # your tables to utf-8.
-  my $dbh = Bugzilla->dbh;
-  if ($dbh->isa('Bugzilla::DB::Mysql') && $dbh->bz_db_is_utf8 && !$utf8) {
-    return "You cannot disable UTF-8 support, because your MySQL database"
-      . " is encoded in UTF-8";
+  my ($utf8, $entry) = @_;
+  # You cannot turn off the UTF-8 parameter.
+  my $current_utf8 = Bugzilla->params->{'utf8'};
+  if (!$utf8) {
+    return "You cannot disable UTF-8 support.";
+  }
+  elsif ($current_utf8 eq 'utf8mb3' && $utf8 ne 'utf8mb3' && $utf8 ne 'utf8mb4') {
+    return "You cannot downgrade from utf8mb3 support, only keep it or change to utf8mb4.";
   }
+  elsif ($current_utf8 eq 'utf8mb4' && $utf8 ne 'utf8mb4') {
+    return "You cannot disable UTF8-MB4 support.";
+  }
+
   return "";
 }
 
diff --git a/Bugzilla/Config/Core.pm b/Bugzilla/Config/Core.pm
index 50af9a077..db9e73163 100644
--- a/Bugzilla/Config/Core.pm
+++ b/Bugzilla/Config/Core.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::Config::Core;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/Bugzilla/Config/DependencyGraph.pm b/Bugzilla/Config/DependencyGraph.pm
index 27bc9938d..cded1e8d0 100644
--- a/Bugzilla/Config/DependencyGraph.pm
+++ b/Bugzilla/Config/DependencyGraph.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::Config::DependencyGraph;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/Bugzilla/Config/General.pm b/Bugzilla/Config/General.pm
index 322275aa0..d1acadbf3 100644
--- a/Bugzilla/Config/General.pm
+++ b/Bugzilla/Config/General.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::Config::General;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
@@ -24,7 +24,21 @@ use constant get_param_list => (
     checker  => \&check_email
   },
 
-  {name => 'utf8', type => 'b', default => '0', checker => \&check_utf8},
+  {
+    name    => 'utf8',
+    type    => 's',
+    choices => ['1', 'utf8', 'utf8mb3', 'utf8mb4'],
+    default => 'utf8mb4',
+    checker => \&check_utf8
+  },
+
+  {
+    name     => 'utf8_collate',
+    type     => 'r',
+    no_reset => '1',
+    default  => 'utf8mb4_unicode_520_ci',
+  },
+
 
   {name => 'shutdownhtml', type => 'l', default => ''},
 
diff --git a/Bugzilla/Config/GroupSecurity.pm b/Bugzilla/Config/GroupSecurity.pm
index 6602cdfea..01cb1803f 100644
--- a/Bugzilla/Config/GroupSecurity.pm
+++ b/Bugzilla/Config/GroupSecurity.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::Config::GroupSecurity;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/Bugzilla/Config/LDAP.pm b/Bugzilla/Config/LDAP.pm
index 75f58e141..1aac80dc8 100644
--- a/Bugzilla/Config/LDAP.pm
+++ b/Bugzilla/Config/LDAP.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::Config::LDAP;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/Bugzilla/Config/MTA.pm b/Bugzilla/Config/MTA.pm
index c7f8e5057..f3f85a7fe 100644
--- a/Bugzilla/Config/MTA.pm
+++ b/Bugzilla/Config/MTA.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::Config::MTA;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
@@ -50,7 +50,7 @@ sub get_param_list {
     {name => 'smtp_password', type => 'p', default => ''},
     {name => 'smtp_ssl',   type => 'b', default => 0, checker => \&check_smtp_ssl},
     {name => 'smtp_debug', type => 'b', default => 0},
-    {name => 'whinedays',  type => 't', default => 7, checker => \&check_numeric},
+    {name => 'whinedays', type => 't', default => 7, checker => \&check_numeric},
     {name => 'globalwatchers', type => 't', default => '',},
   );
   return @param_list;
diff --git a/Bugzilla/Config/Memcached.pm b/Bugzilla/Config/Memcached.pm
index 5ab3364f9..e3f800330 100644
--- a/Bugzilla/Config/Memcached.pm
+++ b/Bugzilla/Config/Memcached.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::Config::Memcached;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/Bugzilla/Config/Query.pm b/Bugzilla/Config/Query.pm
index adfb4eaf4..346551df1 100644
--- a/Bugzilla/Config/Query.pm
+++ b/Bugzilla/Config/Query.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::Config::Query;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/Bugzilla/Config/RADIUS.pm b/Bugzilla/Config/RADIUS.pm
index b0a5ddbf5..f952785e9 100644
--- a/Bugzilla/Config/RADIUS.pm
+++ b/Bugzilla/Config/RADIUS.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::Config::RADIUS;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/Bugzilla/Config/ShadowDB.pm b/Bugzilla/Config/ShadowDB.pm
index 101e4678f..3f0e0e021 100644
--- a/Bugzilla/Config/ShadowDB.pm
+++ b/Bugzilla/Config/ShadowDB.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::Config::ShadowDB;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/Bugzilla/Config/UserMatch.pm b/Bugzilla/Config/UserMatch.pm
index a1f8a3eb2..cbd8515fa 100644
--- a/Bugzilla/Config/UserMatch.pm
+++ b/Bugzilla/Config/UserMatch.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::Config::UserMatch;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/Bugzilla/Constants.pm b/Bugzilla/Constants.pm
index a9a5e0eaa..a0cc4fb0c 100644
--- a/Bugzilla/Constants.pm
+++ b/Bugzilla/Constants.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::Constants;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Exporter);
+use base qw(Exporter);
 
 # For bz_locations
 use File::Basename;
@@ -200,7 +200,7 @@ use Memoize;
 # CONSTANTS
 #
 # Bugzilla version
-use constant BUGZILLA_VERSION => "5.0.6";
+use constant BUGZILLA_VERSION => "5.2";
 
 # A base link to the current REST Documentation. We place it here
 # as it will need to be updated to whatever the current release is.
@@ -514,24 +514,38 @@ use constant INSTALLATION_MODE_NON_INTERACTIVE => 1;
 # Data about what we require for different databases.
 use constant DB_MODULE => {
 
-  # MySQL 5.0.15 was the first production 5.0.x release.
+  # Require MySQL 5.6.x for InnoDB's fulltext support
   'mysql' => {
     db         => 'Bugzilla::DB::Mysql',
-    db_version => '5.0.15',
+    db_version => '5.6.12',
     dbd        => {
       package => 'DBD-mysql',
       module  => 'DBD::mysql',
 
       # Disallow development versions
-      blacklist => ['_'],
+      blocklist => ['_'],
 
       # For UTF-8 support. 4.001 makes sure that blobs aren't
       # marked as UTF-8.
-      version => '4.001',
+      version => '4.032',
     },
     name => 'MySQL'
   },
 
+  # MariaDB needs version 10 for InnoDB fulltext support
+  'mariadb' => {
+    db         => 'Bugzilla::DB::MariaDB',
+    db_version => '10.0.5',
+    dbd        => {
+      package => 'DBD-MariaDB',
+      module  => 'DBD::MariaDB',
+
+      # Disallow development versions
+      blocklist => ['_'],
+    },
+    name => 'MariaDB'
+  },
+
   # Also see Bugzilla::DB::Pg::bz_check_server_version, which has special
   # code to require DBD::Pg 2.17.2 for PostgreSQL 9 and above.
   'pg' => {
diff --git a/Bugzilla/DB.pm b/Bugzilla/DB.pm
index c528e77d1..8ef3d36fa 100644
--- a/Bugzilla/DB.pm
+++ b/Bugzilla/DB.pm
@@ -7,14 +7,14 @@
 
 package Bugzilla::DB;
 
-use 5.10.1;
-use strict;
-use warnings;
+use 5.14.0;
+use Moo;
 
 use DBI;
+use DBIx::Connector;
+our %Connector;
 
-# Inherit the DB class from DBI::db.
-use parent -norequire, qw(DBI::db);
+has 'connector' => (is => 'lazy', handles => [qw( dbh )],);
 
 use Bugzilla::Constants;
 use Bugzilla::Mailer;
@@ -24,11 +24,49 @@ use Bugzilla::Install::Localconfig;
 use Bugzilla::Util;
 use Bugzilla::Error;
 use Bugzilla::DB::Schema;
+use Bugzilla::DB::QuoteIdentifier;
 use Bugzilla::Version;
 
+use Scalar::Util qw(blessed);
 use List::Util qw(max);
 use Storable qw(dclone);
 
+has [qw(dsn user pass attrs)] => (is => 'ro', required => 1,);
+
+
+has 'qi' => (is => 'lazy');
+
+sub _build_qi {
+  my ($self) = @_;
+  my %q;
+  tie %q, 'Bugzilla::DB::QuoteIdentifier', db => $self;
+
+  return \%q;
+}
+
+# Install proxy methods to the DBI object.
+# We can't use handles() as DBIx::Connector->dbh has to be called each
+# time we need a DBI handle to ensure the connection is alive.
+{
+  my @DBI_METHODS = qw(
+    begin_work column_info commit disconnect do errstr get_info last_insert_id
+    ping prepare prepare_cached primary_key quote_identifier rollback
+    selectall_arrayref selectall_hashref selectcol_arrayref selectrow_array
+    selectrow_arrayref selectrow_hashref table_info
+  );
+  my $stash = Package::Stash->new(__PACKAGE__);
+
+  foreach my $method (@DBI_METHODS) {
+    my $symbol = '&' . $method;
+    $stash->add_symbol(
+      $symbol => sub {
+        my $self = shift;
+        return $self->dbh->$method(@_);
+      }
+    );
+  }
+}
+
 #####################################################################
 # Constants
 #####################################################################
@@ -88,7 +126,7 @@ use constant INDEX_DROPS_REQUIRE_FK_DROPS => 1;
 
 sub quote {
   my $self   = shift;
-  my $retval = $self->SUPER::quote(@_);
+  my $retval = $self->dbh->quote(@_);
   trick_taint($retval) if defined $retval;
   return $retval;
 }
@@ -130,9 +168,7 @@ sub _connect {
     "'$driver' is not a valid choice for \$db_driver in " . " localconfig: " . $@);
 
   # instantiate the correct DB specific module
-  my $dbh = $pkg_module->new($params);
-
-  return $dbh;
+  return $pkg_module->new($params);
 }
 
 sub _handle_error {
@@ -200,10 +236,34 @@ sub bz_check_server_version {
   my ($self, $db, $output) = @_;
 
   my $sql_vers = $self->bz_server_version;
-  $self->disconnect;
+  if (((lc($db->{name}) eq 'mysql') || (lc($db->{name}) eq "mariadb"))
+    && ($sql_vers =~ s/^5\.5\.5-// || $sql_vers =~ /-MariaDB/)) {
+    # Version 5.5.5 of MySQL never existed. MariaDB = 10 always puts '5.5.5-'
+    # at the front of its version string to get around a limitation in the
+    # replication protocol it shares with MySQL.  So if the version starts with
+    # '5.5.5-' then we can assume this is MariaDB and the real version number
+    # will immediately follow that.  This was removed in MariaDB-11.0.  The
+    # version should always contain "MariaDB" if it is indeed MariaDB.
+    if (lc($db->{name}) eq 'mysql') {
+      if ($output) {
+        Bugzilla::Install::Requirements::_checking_for({
+          package => $db->{name},
+          wanted  => $db->{version},
+          ok      => 0,
+        });
+      }
+      die install_string('db_maria_on_mysql', {vers => $sql_vers});
+    }
 
+  }
+  my $sql_dontwant = exists $db->{db_blocklist} ? $db->{db_blocklist} : [];
   my $sql_want   = $db->{db_version};
   my $version_ok = vers_cmp($sql_vers, $sql_want) > -1 ? 1 : 0;
+  my $blocklisted;
+  if ($version_ok) {
+    $blocklisted = grep($sql_vers =~ /$_/, @$sql_dontwant);
+    $version_ok = 0 if $blocklisted;
+  }
 
   my $sql_server = $db->{name};
   if ($output) {
@@ -211,20 +271,22 @@ sub bz_check_server_version {
       package => $sql_server,
       wanted  => $sql_want,
       found   => $sql_vers,
-      ok      => $version_ok
+      ok      => $version_ok,
+      blocklisted => $blocklisted
     });
   }
 
   # Check what version of the database server is installed and let
   # the user know if the version is too old to be used with Bugzilla.
+  if ($blocklisted) {
+    die install_string('db_blocklisted', {server=>$sql_server, vers=>$sql_vers});
+  }
   if (!$version_ok) {
-    die <<EOT;
-
-Your $sql_server v$sql_vers is too old. Bugzilla requires version
-$sql_want or later of $sql_server. Please download and install a
-newer version.
-
-EOT
+    die install_string('db_too_old', {
+      server => $sql_server,
+      vers   => $sql_vers,
+      want   => $sql_want,
+    });
   }
 
   # This is used by subclasses.
@@ -237,7 +299,7 @@ sub bz_create_database {
   my $dbh;
 
   # See if we can connect to the actual Bugzilla database.
-  my $conn_success = eval { $dbh = connect_main() };
+  my $conn_success = eval { $dbh = connect_main(); $dbh->ping(); };
   my $db_name      = Bugzilla->localconfig->{db_name};
 
   if (!$conn_success) {
@@ -261,7 +323,6 @@ sub bz_create_database {
     }
   }
 
-  $dbh->disconnect;
 }
 
 # A helper for bz_create_database and bz_check_requirements.
@@ -270,6 +331,7 @@ sub _get_no_db_connection {
   my $dbh;
   my %connect_params = %{Bugzilla->localconfig};
   $connect_params{db_name} = '';
+  local %Connector = ();
   my $conn_success = eval { $dbh = _connect(\%connect_params); };
   if (!$conn_success) {
     my $driver     = $connect_params{db_driver};
@@ -546,7 +608,7 @@ sub bz_setup_foreign_keys {
   # so if it doesn't have them, then we're setting up FKs
   # for the first time, and should be quieter about it.
   my $activity_fk = $self->bz_fk_info('profiles_activity', 'userid');
-  my $any_fks = $activity_fk && $activity_fk->{created};
+  my $any_fks     = $activity_fk && $activity_fk->{created};
   if (!$any_fks) {
     say get_text('install_fk_setup');
   }
@@ -1132,12 +1194,21 @@ sub bz_set_next_serial_value {
 # Schema Information Methods
 #####################################################################
 
+sub _bz_schema_class {
+  my ($self) = @_;
+  my $class = blessed($self) // $self;
+  my @class_parts = split(/::/, $class);
+  splice(@class_parts, -1, 0, 'Schema');
+
+  return join('::', @class_parts);
+}
+
 sub _bz_schema {
   my ($self) = @_;
   return $self->{private_bz_schema} if exists $self->{private_bz_schema};
-  my @module_parts = split('::', ref $self);
-  my $module_name  = pop @module_parts;
-  $self->{private_bz_schema} = Bugzilla::DB::Schema->new($module_name);
+  my $schema_class = $self->_bz_schema_class;
+  eval "require $schema_class";
+  $self->{private_bz_schema} = $schema_class->new(db => $self);
   return $self->{private_bz_schema};
 }
 
@@ -1299,14 +1370,15 @@ sub bz_rollback_transaction {
 # Subclass Helpers
 #####################################################################
 
-sub db_new {
-  my ($class, $params) = @_;
-  my ($dsn, $user, $pass, $override_attrs) = @$params{qw(dsn user pass attrs)};
+sub _build_connector {
+  my ($self) = @_;
+  my ($dsn, $user, $pass, $override_attrs)
+    = map { $self->$_ } qw(dsn user pass attrs);
 
   # set up default attributes used to connect to the database
   # (may be overridden by DB driver implementations)
   my $attributes = {
-    RaiseError         => 0,
+    RaiseError         => 1,
     AutoCommit         => 1,
     PrintError         => 0,
     ShowErrorStatement => 1,
@@ -1323,20 +1395,14 @@ sub db_new {
       $attributes->{$key} = $override_attrs->{$key};
     }
   }
+  my $class = ref $self;
+  if ($class->can('on_dbi_connected')) {
+    $attributes->{Callbacks}
+      = {connected => sub { $class->on_dbi_connected(@_); return },};
+  }
 
-  # connect using our known info to the specified db
-  my $self = DBI->connect($dsn, $user, $pass, $attributes)
-    or die "\nCan't connect to the database.\nError: $DBI::errstr\n"
-    . "  Is your database installed and up and running?\n  Do you have"
-    . " the correct username and password selected in localconfig?\n\n";
-
-  # RaiseError was only set to 0 so that we could catch the
-  # above "die" condition.
-  $self->{RaiseError} = 1;
-
-  bless($self, $class);
-
-  return $self;
+  return $Connector{"$user.$dsn"}
+    //= DBIx::Connector->new($dsn, $user, $pass, $attributes);
 }
 
 #####################################################################
@@ -1520,7 +1586,11 @@ sub _check_references {
   # reserved words.
   my $bad_values = $self->selectcol_arrayref(
     "SELECT DISTINCT tabl.$column 
-           FROM $table AS tabl LEFT JOIN $foreign_table AS forn
+           FROM "
+      . $self->quote_identifier($table)
+      . " AS tabl LEFT JOIN "
+      . $self->quote_identifier($foreign_table)
+      . " AS forn
                 ON tabl.$column = forn.$foreign_column
           WHERE forn.$foreign_column IS NULL
                 AND tabl.$column IS NOT NULL"
@@ -1530,7 +1600,10 @@ sub _check_references {
     my $delete_action = $fk->{DELETE} || '';
     if ($delete_action eq 'CASCADE') {
       $self->do(
-        "DELETE FROM $table WHERE $column IN (" . join(',', ('?') x @$bad_values) . ")",
+        "DELETE FROM "
+          . $self->quote_identifier($table)
+          . " WHERE $column IN ("
+          . join(',', ('?') x @$bad_values) . ")",
         undef, @$bad_values
       );
       if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) {
@@ -1551,7 +1624,9 @@ sub _check_references {
     }
     elsif ($delete_action eq 'SET NULL') {
       $self->do(
-        "UPDATE $table SET $column = NULL
+            "UPDATE "
+          . $self->quote_identifier($table)
+          . " SET $column = NULL
                         WHERE $column IN ("
           . join(',', ('?') x @$bad_values) . ")", undef, @$bad_values
       );
@@ -2371,6 +2446,32 @@ Formatted SQL for the C<IN> operator.
 
 =back
 
+=head1 ATTRIBUTES
+
+=over 4
+
+=item C<dsn>
+
+The data source name for the database. This is a string that is passed to
+the DBI to connect to the database. It is usually of the form:
+
+  dbi:DriverName:database_name
+
+=item C<user>
+
+The user name to use when connecting to the database.
+
+=item C<pass>
+
+The password to use when connecting to the database.
+
+=item C<attrs>
+
+A hashref of attributes to pass to the DBI when connecting to the database.
+It is usually used to set the C<RaiseError> and C<PrintError> attributes,
+but can be used to set any attribute that the DBI supports.
+
+=back
 
 =head1 IMPLEMENTED METHODS
 
diff --git a/Bugzilla/DB/MariaDB.pm b/Bugzilla/DB/MariaDB.pm
new file mode 100644
index 000000000..c57ebd023
--- /dev/null
+++ b/Bugzilla/DB/MariaDB.pm
@@ -0,0 +1,1300 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+=head1 NAME
+
+Bugzilla::DB::MariaDB - Bugzilla database compatibility layer for MariaDB
+
+=head1 DESCRIPTION
+
+This module overrides methods of the Bugzilla::DB module with MariaDB specific
+implementation. It is instantiated by the Bugzilla::DB module and should never
+be used directly.
+
+For interface details see L<Bugzilla::DB> and L<DBI>.
+
+=cut
+
+package Bugzilla::DB::MariaDB;
+
+use 5.14.0;
+use Moo;
+
+extends qw(Bugzilla::DB);
+
+use Bugzilla::Constants;
+use Bugzilla::Install::Util qw(install_string);
+use Bugzilla::Config;
+use Bugzilla::Util;
+use Bugzilla::Error;
+use Bugzilla::DB::Schema::MariaDB;
+
+use List::Util qw(max any);
+use Text::ParseWords;
+use Carp;
+
+# This is how many comments of MAX_COMMENT_LENGTH we expect on a single bug.
+# In reality, you could have a LOT more comments than this, because
+# MAX_COMMENT_LENGTH is big.
+use constant MAX_COMMENTS => 50;
+
+use constant FULLTEXT_OR => '|';
+
+sub BUILDARGS {
+  my ($class, $params) = @_;
+  my ($user, $pass, $host, $dbname, $port, $sock)
+    = @$params{qw(db_user db_pass db_host db_name db_port db_sock)};
+
+  # construct the DSN from the parameters we got
+  my $dsn = "dbi:MariaDB:host=$host;database=$dbname";
+  $dsn .= ";port=$port"         if $port;
+  $dsn .= ";mariadb_socket=$sock" if $sock;
+
+  my %attrs = ();
+
+  # MariaDB SSL options
+  my ($ssl_ca_file, $ssl_ca_path, $ssl_cert, $ssl_key) = @$params{
+    qw(db_mysql_ssl_ca_file db_mysql_ssl_ca_path
+      db_mysql_ssl_client_cert db_mysql_ssl_client_key)
+  };
+  if ($ssl_ca_file || $ssl_ca_path || $ssl_cert || $ssl_key) {
+    $attrs{'mariadb_ssl'}             = 1;
+    $attrs{'mariadb_ssl_ca_file'}     = $ssl_ca_file if $ssl_ca_file;
+    $attrs{'mariadb_ssl_ca_path'}     = $ssl_ca_path if $ssl_ca_path;
+    $attrs{'mariadb_ssl_client_cert'} = $ssl_cert if $ssl_cert;
+    $attrs{'mariadb_ssl_client_key'}  = $ssl_key if $ssl_key;
+  }
+
+  return {dsn => $dsn, user => $user, pass => $pass, attrs => \%attrs};
+}
+
+sub on_dbi_connected {
+  my ($class, $dbh) = @_;
+
+  # This makes sure that if the tables are encoded as UTF-8, we
+  # return their data correctly.
+  my $charset = $class->utf8_charset;
+  my $collate = $class->utf8_collate;
+  $dbh->do("SET NAMES $charset COLLATE $collate");
+
+  # Check for MySQL modes.
+  my ($var, $sql_mode)
+    = $dbh->selectrow_array("SHOW VARIABLES LIKE 'sql\\_mode'");
+
+  # Disable ANSI and strict modes, else Bugzilla will crash.
+  if ($sql_mode) {
+
+    # STRICT_TRANS_TABLE or STRICT_ALL_TABLES enable MySQL strict mode,
+    # causing bug 321645. TRADITIONAL sets these modes (among others) as
+    # well, so it has to be stipped as well
+    my $new_sql_mode = join(",",
+      grep { $_ !~ /^(?:ANSI|STRICT_(?:TRANS|ALL)_TABLES|TRADITIONAL)$/ }
+        split(/,/, $sql_mode));
+
+    if ($sql_mode ne $new_sql_mode) {
+      $dbh->do("SET SESSION sql_mode = ?", undef, $new_sql_mode);
+    }
+  }
+
+  # Allow large GROUP_CONCATs (largely for inserting comments
+  # into bugs_fulltext).
+  $dbh->do('SET SESSION group_concat_max_len = 128000000');
+
+  # MySQL 5.5.2 and older have this variable set to true, which causes
+  # trouble, see bug 870369.
+  $dbh->do('SET SESSION sql_auto_is_null = 0');
+}
+
+# when last_insert_id() is supported on MySQL by lowest DBI/DBD version
+# required by Bugzilla, this implementation can be removed.
+sub bz_last_key {
+  my ($self) = @_;
+
+  my ($last_insert_id) = $self->selectrow_array('SELECT LAST_INSERT_ID()');
+
+  return $last_insert_id;
+}
+
+sub sql_group_concat {
+  my ($self, $column, $separator, $sort, $order_by) = @_;
+  $separator = $self->quote(', ') if !defined $separator;
+  $sort = 1 if !defined $sort;
+  if ($order_by) {
+    $column .= " ORDER BY $order_by";
+  }
+  elsif ($sort) {
+    my $sort_order = $column;
+    $sort_order =~ s/^DISTINCT\s+//i;
+    $column = "$column ORDER BY $sort_order";
+  }
+  return "GROUP_CONCAT($column SEPARATOR $separator)";
+}
+
+sub sql_regexp {
+  my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_;
+  $real_pattern ||= $pattern;
+
+  $self->bz_check_regexp($real_pattern) if !$nocheck;
+
+  return "$expr REGEXP $pattern";
+}
+
+sub sql_not_regexp {
+  my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_;
+  $real_pattern ||= $pattern;
+
+  $self->bz_check_regexp($real_pattern) if !$nocheck;
+
+  return "$expr NOT REGEXP $pattern";
+}
+
+sub sql_limit {
+  my ($self, $limit, $offset) = @_;
+
+  if (defined($offset)) {
+    return "LIMIT $offset, $limit";
+  }
+  else {
+    return "LIMIT $limit";
+  }
+}
+
+sub sql_string_concat {
+  my ($self, @params) = @_;
+
+  return 'CONCAT(' . join(', ', @params) . ')';
+}
+
+sub sql_fulltext_search {
+  my ($self, $column, $text) = @_;
+
+  # Add the boolean mode modifier if the search string contains
+  # boolean operators at the start or end of a word.
+  my $mode = '';
+  if ($text =~ /(?:^|\W)[+\-<>~"()]/ || $text =~ /[()"*](?:$|\W)/) {
+    $mode = 'IN BOOLEAN MODE';
+
+    my @terms = split(quotemeta(FULLTEXT_OR), $text);
+    foreach my $term (@terms) {
+
+      # quote un-quoted compound words
+      my @words = quotewords('[\s()]+', 'delimiters', $term);
+      foreach my $word (@words) {
+
+        # match words that have non-word chars in the middle of them
+        if ($word =~ /\w\W+\w/ && $word !~ m/"/) {
+          $word = '"' . $word . '"';
+        }
+      }
+      $term = join('', @words);
+    }
+    $text = join(FULLTEXT_OR, @terms);
+  }
+
+  # quote the text for use in the MATCH AGAINST expression
+  $text = $self->quote($text);
+
+  # untaint the text, since it's safe to use now that we've quoted it
+  trick_taint($text);
+
+  return "MATCH($column) AGAINST($text $mode)";
+}
+
+sub sql_istring {
+  my ($self, $string) = @_;
+
+  return $string;
+}
+
+sub sql_from_days {
+  my ($self, $days) = @_;
+
+  return "FROM_DAYS($days)";
+}
+
+sub sql_to_days {
+  my ($self, $date) = @_;
+
+  return "TO_DAYS($date)";
+}
+
+sub sql_date_format {
+  my ($self, $date, $format) = @_;
+
+  $format = "%Y.%m.%d %H:%i:%s" if !$format;
+
+  return "DATE_FORMAT($date, " . $self->quote($format) . ")";
+}
+
+sub sql_date_math {
+  my ($self, $date, $operator, $interval, $units) = @_;
+
+  return "$date $operator INTERVAL $interval $units";
+}
+
+sub sql_iposition {
+  my ($self, $fragment, $text) = @_;
+  return "INSTR($text, $fragment)";
+}
+
+sub sql_position {
+  my ($self, $fragment, $text) = @_;
+
+  return "INSTR(CAST($text AS BINARY), CAST($fragment AS BINARY))";
+}
+
+sub sql_group_by {
+  my ($self, $needed_columns, $optional_columns) = @_;
+
+  # MySQL allows you to specify the minimal subset of columns to get
+  # a unique result. While it does allow specifying all columns as
+  # ANSI SQL requires, according to MySQL documentation, the fewer
+  # columns you specify, the faster the query runs.
+  return "GROUP BY $needed_columns";
+}
+
+sub bz_explain {
+  my ($self, $sql) = @_;
+  my $sth = $self->prepare("EXPLAIN $sql");
+  $sth->execute();
+  my $columns       = $sth->{'NAME'};
+  my $lengths       = $sth->{'mysql_max_length'};
+  my $format_string = '|';
+  my $i             = 0;
+  foreach my $column (@$columns) {
+
+    # Sometimes the column name is longer than the contents.
+    my $length = max($lengths->[$i], length($column));
+    $format_string .= ' %-' . $length . 's |';
+    $i++;
+  }
+
+  my $first_row    = sprintf($format_string, @$columns);
+  my @explain_rows = ($first_row, '-' x length($first_row));
+  while (my $row = $sth->fetchrow_arrayref) {
+    my @fixed = map { defined $_ ? $_ : 'NULL' } @$row;
+    push(@explain_rows, sprintf($format_string, @fixed));
+  }
+
+  return join("\n", @explain_rows);
+}
+
+sub _bz_get_initial_schema {
+  my ($self) = @_;
+  return $self->_bz_build_schema_from_disk();
+}
+
+#####################################################################
+# Database Setup
+#####################################################################
+
+sub bz_check_server_version {
+  my $self = shift;
+
+  my $lc = Bugzilla->localconfig;
+  if (lc(Bugzilla->localconfig->{db_name}) eq 'mysql') {
+    die "It is not safe to run Bugzilla inside a database named 'mysql'.\n"
+      . " Please pick a different value for \$db_name in localconfig.\n";
+  }
+
+  $self->SUPER::bz_check_server_version(@_);
+}
+
+sub bz_setup_database {
+  my ($self) = @_;
+
+  # Before touching anything else, find out whether this database server does
+  # any aliasing of the character set we plan to use so we can check for
+  # already converted tables properly. We do this by creating a table as our
+  # intended charset and then test how it reads back.
+  my $db_name = Bugzilla->localconfig->{db_name};
+  my $charset = $self->utf8_charset;
+  my $collate = $self->utf8_collate;
+  $self->do("CREATE TABLE `utf8_test` (id tinyint) CHARACTER SET ? COLLATE ?", undef, $charset, $collate);
+  my ($found_collate) = $self->selectrow_array("SELECT TABLE_COLLATION FROM information_schema.TABLES WHERE TABLE_SCHEMA=? AND TABLE_NAME='utf8_test'", undef, $db_name);
+  $self->do("DROP TABLE `utf8_test`");
+  my ($found_charset) = ($found_collate =~ m/^([a-z0-9]+)_/);
+  Bugzilla->params->{'utf8'} = $found_charset;
+  Bugzilla->params->{'utf8_collate'} = $found_collate;
+  Bugzilla::Config::write_params();
+  # reload these because they get used later.
+  $charset = $self->utf8_charset;
+  $collate = $self->utf8_collate;
+
+  # The "comments" field of the bugs_fulltext table could easily exceed
+  # MySQL's default max_allowed_packet. Also, MySQL should never have
+  # a max_allowed_packet smaller than our max_attachment_size. So, we
+  # warn the user here if max_allowed_packet is too small.
+  my $min_max_allowed = MAX_COMMENTS * MAX_COMMENT_LENGTH;
+  my (undef, $current_max_allowed)
+    = $self->selectrow_array(q{SHOW VARIABLES LIKE 'max\_allowed\_packet'});
+
+  # This parameter is not yet defined when the DB is being built for
+  # the very first time. The code below still works properly, however,
+  # because the default maxattachmentsize is smaller than $min_max_allowed.
+  my $max_attachment     = (Bugzilla->params->{'maxattachmentsize'} || 0) * 1024;
+  my $needed_max_allowed = max($min_max_allowed, $max_attachment);
+  if ($current_max_allowed < $needed_max_allowed) {
+    warn install_string('max_allowed_packet',
+      {current => $current_max_allowed, needed => $needed_max_allowed})
+      . "\n";
+  }
+
+  # Make sure the installation has InnoDB turned on, or we're going to be
+  # doing silly things like making foreign keys on MyISAM tables, which is
+  # hard to fix later. We do this up here because none of the code below
+  # works if InnoDB is off. (Particularly if we've already converted the
+  # tables to InnoDB.)
+  my %engines = @{$self->selectcol_arrayref('SHOW ENGINES', {Columns => [1, 2]})};
+  if (!$engines{InnoDB} || $engines{InnoDB} !~ /^(YES|DEFAULT)$/) {
+    die install_string('mysql_innodb_disabled');
+  }
+
+  if ($self->utf8_charset eq 'utf8mb3') {
+    my %global = map {@$_}
+      @{$self->selectall_arrayref(q(SHOW GLOBAL VARIABLES LIKE 'innodb_%'))};
+
+    # In versions of MySQL > 8, the default value for innodb_file_format is Barracuda
+    # and the setting was deprecated. Also innodb_file_per_table also now defaults
+    # to ON. innodb_large_prefix has also been removed in newer MySQL versions.
+    my $utf8mb4_supported
+      = (!exists $global{innodb_file_format}
+        || $global{innodb_file_format} eq 'Barracuda')
+      && (!exists $global{innodb_file_per_table}
+      || $global{innodb_file_per_table} eq 'ON')
+      && (!exists $global{innodb_large_prefix}
+      || $global{innodb_large_prefix} eq 'ON');
+
+    die install_string('mysql_innodb_settings') unless $utf8mb4_supported;
+
+    my $tables = $self->selectall_arrayref('SHOW TABLE STATUS');
+    foreach my $table_status (@$tables) {
+      my ($table, undef, undef, $row_format) = @$table_status;
+      my $table_type     = $table_status->[-1];
+      my $new_row_format = $self->default_row_format($table);
+      next if lc($table_type) eq 'view';
+      next if lc($new_row_format) eq 'compact';
+      next if lc($row_format) eq 'dynamic';
+      next if lc($row_format) eq 'compressed';
+      if (lc($new_row_format) ne lc($row_format)) {
+        print install_string(
+          'mysql_row_format_conversion', {table => $table, format => $new_row_format}
+          ),
+          "\n";
+        $self->do(
+          sprintf 'ALTER TABLE %s ROW_FORMAT=%s',
+          $self->quote_identifier($table),
+          $new_row_format
+        );
+      }
+    }
+  }
+
+  my ($sd_index_deleted, $longdescs_index_deleted);
+  my @tables = $self->bz_table_list_real();
+
+  # We want to convert tables to InnoDB, but it's possible that they have
+  # fulltext indexes on them, and conversion will fail unless we remove
+  # the indexes.
+  if (grep($_ eq 'bugs', @tables) and !grep($_ eq 'bugs_fulltext', @tables)) {
+    if ($self->bz_index_info_real('bugs', 'short_desc')) {
+      $self->bz_drop_index_raw('bugs', 'short_desc');
+    }
+    if ($self->bz_index_info_real('bugs', 'bugs_short_desc_idx')) {
+      $self->bz_drop_index_raw('bugs', 'bugs_short_desc_idx');
+      $sd_index_deleted = 1;    # Used for later schema cleanup.
+    }
+  }
+  if (grep($_ eq 'longdescs', @tables) and !grep($_ eq 'bugs_fulltext', @tables))
+  {
+    if ($self->bz_index_info_real('longdescs', 'thetext')) {
+      $self->bz_drop_index_raw('longdescs', 'thetext');
+    }
+    if ($self->bz_index_info_real('longdescs', 'longdescs_thetext_idx')) {
+      $self->bz_drop_index_raw('longdescs', 'longdescs_thetext_idx');
+      $longdescs_index_deleted = 1;    # For later schema cleanup.
+    }
+  }
+
+  # Upgrade tables from MyISAM to InnoDB
+  my $myisam_tables = $self->selectcol_arrayref(
+    'SELECT TABLE_NAME FROM information_schema.TABLES 
+          WHERE TABLE_SCHEMA = ? AND ENGINE = ?', undef, $db_name, 'MyISAM'
+  );
+
+  if (scalar @$myisam_tables) {
+    print "Bugzilla now uses the InnoDB storage engine in MySQL for",
+      " most tables.\nConverting tables to InnoDB:\n";
+    foreach my $table (@$myisam_tables) {
+      print "Converting table $table... ";
+      $self->do('ALTER TABLE ' . $self->quote_identifier($table) . ' ENGINE = InnoDB');
+      print "done.\n";
+    }
+  }
+
+  # Versions of Bugzilla before the existence of Bugzilla::DB::Schema did
+  # not provide explicit names for the table indexes. This means
+  # that our upgrades will not be reliable, because we look for the name
+  # of the index, not what fields it is on, when doing upgrades.
+  # (using the name is much better for cross-database compatibility
+  # and general reliability). It's also very important that our
+  # Schema object be consistent with what is on the disk.
+  #
+  # While we're at it, we also fix some inconsistent index naming
+  # from the original checkin of Bugzilla::DB::Schema.
+
+  # We check for the existence of a particular "short name" index that
+  # has existed at least since Bugzilla 2.8, and probably earlier.
+  # For fixing the inconsistent naming of Schema indexes,
+  # we also check for one of those inconsistently-named indexes.
+  if (
+    grep($_ eq 'bugs', @tables)
+    && ( $self->bz_index_info_real('bugs', 'assigned_to')
+      || $self->bz_index_info_real('flags', 'flags_bidattid_idx'))
+    )
+  {
+
+    # This is a check unrelated to the indexes, to see if people are
+    # upgrading from 2.18 or below, but somehow have a bz_schema table
+    # already. This only happens if they have done a mysqldump into
+    # a database without doing a DROP DATABASE first.
+    # We just do the check here since this check is a reliable way
+    # of telling that we are upgrading from a version pre-2.20.
+    if (grep($_ eq 'bz_schema', $self->bz_table_list_real())) {
+      die install_string('bz_schema_exists_before_220');
+    }
+
+    my $bug_count = $self->selectrow_array("SELECT COUNT(*) FROM bugs");
+
+    # We estimate one minute for each 3000 bugs, plus 3 minutes just
+    # to handle basic MySQL stuff.
+    my $rename_time = int($bug_count / 3000) + 3;
+
+    # And 45 minutes for every 15,000 attachments, per some experiments.
+    my ($attachment_count)
+      = $self->selectrow_array("SELECT COUNT(*) FROM attachments");
+    $rename_time += int(($attachment_count * 45) / 15000);
+
+    # If we're going to take longer than 5 minutes, we let the user know
+    # and allow them to abort.
+    if ($rename_time > 5) {
+      print "\n", install_string('mysql_index_renaming', {minutes => $rename_time});
+
+      # Wait 45 seconds for them to respond.
+      sleep(45) unless Bugzilla->installation_answers->{NO_PAUSE};
+    }
+    print "Renaming indexes...\n";
+
+    # We can't be interrupted, because of how the "if"
+    # works above.
+    local $SIG{INT}  = 'IGNORE';
+    local $SIG{TERM} = 'IGNORE';
+    local $SIG{PIPE} = 'IGNORE';
+
+    # Certain indexes had names in Schema that did not easily conform
+    # to a standard. We store those names here, so that they
+    # can be properly renamed.
+    # Also, sometimes an old mysqldump would incorrectly rename
+    # unique indexes to "PRIMARY", so we address that here, also.
+    my $bad_names = {
+
+      # 'when' is a possible leftover from Bugzillas before 2.8
+      bugs_activity =>
+        ['when', 'bugs_activity_bugid_idx', 'bugs_activity_bugwhen_idx'],
+      cc                 => ['PRIMARY'],
+      longdescs          => ['longdescs_bugid_idx', 'longdescs_bugwhen_idx'],
+      flags              => ['flags_bidattid_idx'],
+      flaginclusions     => ['flaginclusions_tpcid_idx'],
+      flagexclusions     => ['flagexclusions_tpc_id_idx'],
+      keywords           => ['PRIMARY'],
+      milestones         => ['PRIMARY'],
+      profiles_activity  => ['profiles_activity_when_idx'],
+      group_control_map  => ['group_control_map_gid_idx', 'PRIMARY'],
+      user_group_map     => ['PRIMARY'],
+      group_group_map    => ['PRIMARY'],
+      email_setting      => ['PRIMARY'],
+      bug_group_map      => ['PRIMARY'],
+      category_group_map => ['PRIMARY'],
+      watch              => ['PRIMARY'],
+      namedqueries       => ['PRIMARY'],
+      series_data        => ['PRIMARY'],
+
+      # series_categories is dealt with below, not here.
+    };
+
+    # The series table is broken and needs to have one index
+    # dropped before we begin the renaming, because it had a
+    # useless index on it that would cause a naming conflict here.
+    if (grep($_ eq 'series', @tables)) {
+      my $dropname;
+
+      # This is what the bad index was called before Schema.
+      if ($self->bz_index_info_real('series', 'creator_2')) {
+        $dropname = 'creator_2';
+      }
+
+      # This is what the bad index is called in Schema.
+      elsif ($self->bz_index_info_real('series', 'series_creator_idx')) {
+        $dropname = 'series_creator_idx';
+      }
+      $self->bz_drop_index_raw('series', $dropname) if $dropname;
+    }
+
+    # The email_setting table also had the same problem.
+    if (grep($_ eq 'email_setting', @tables)
+      && $self->bz_index_info_real('email_setting', 'email_settings_user_id_idx'))
+    {
+      $self->bz_drop_index_raw('email_setting', 'email_settings_user_id_idx');
+    }
+
+    # Go through all the tables.
+    foreach my $table (@tables) {
+
+      # Will contain the names of old indexes as keys, and the
+      # definition of the new indexes as a value. The values
+      # include an extra hash key, NAME, with the new name of
+      # the index.
+      my %rename_indexes;
+
+      # And go through all the columns on each table.
+      my @columns = $self->bz_table_columns_real($table);
+
+      # We also want to fix the silly naming of unique indexes
+      # that happened when we first checked-in Bugzilla::DB::Schema.
+      if ($table eq 'series_categories') {
+
+        # The series_categories index had a nonstandard name.
+        push(@columns, 'series_cats_unique_idx');
+      }
+      elsif ($table eq 'email_setting') {
+
+        # The email_setting table had a similar problem.
+        push(@columns, 'email_settings_unique_idx');
+      }
+      else {
+        push(@columns, "${table}_unique_idx");
+      }
+
+      # And this is how we fix the other inconsistent Schema naming.
+      push(@columns, @{$bad_names->{$table}}) if (exists $bad_names->{$table});
+      foreach my $column (@columns) {
+
+        # If we have an index named after this column, it's an
+        # old-style-name index.
+        if (my $index = $self->bz_index_info_real($table, $column)) {
+
+          # Fix the name to fit in with the new naming scheme.
+          $index->{NAME} = $table . "_" . $index->{FIELDS}->[0] . "_idx";
+          print "Renaming index $column to " . $index->{NAME} . "...\n";
+          $rename_indexes{$column} = $index;
+        }    # if
+      }    # foreach column
+
+      my @rename_sql
+        = $self->_bz_schema->get_rename_indexes_ddl($table, %rename_indexes);
+      $self->do($_) foreach (@rename_sql);
+
+    }    # foreach table
+  }    # if old-name indexes
+
+  # If there are no tables, but the DB isn't utf8 and it should be,
+  # then we should alter the database to be utf8. We know it should be
+  # if the utf8 parameter is true or there are no params at all.
+  # This kind of situation happens when people create the database
+  # themselves, and if we don't do this they will get the big
+  # scary WARNING statement about conversion to UTF8.
+  unless ($self->bz_db_is_utf8) {
+    $self->_alter_db_charset_to_utf8();
+  }
+
+  # And now we create the tables and the Schema object.
+  $self->SUPER::bz_setup_database();
+
+  if ($sd_index_deleted) {
+    $self->_bz_real_schema->delete_index('bugs', 'bugs_short_desc_idx');
+    $self->_bz_store_real_schema;
+  }
+  if ($longdescs_index_deleted) {
+    $self->_bz_real_schema->delete_index('longdescs', 'longdescs_thetext_idx');
+    $self->_bz_store_real_schema;
+  }
+
+  # The old timestamp fields need to be adjusted here instead of in
+  # checksetup. Otherwise the UPDATE statements inside of bz_add_column
+  # will cause accidental timestamp updates.
+  # The code that does this was moved here from checksetup.
+
+  # 2002-08-14 - bbaetz@student.usyd.edu.au - bug 153578
+  # attachments creation time needs to be a datetime, not a timestamp
+  my $attach_creation = $self->bz_column_info("attachments", "creation_ts");
+  if ($attach_creation && $attach_creation->{TYPE} =~ /^TIMESTAMP/i) {
+    print "Fixing creation time on attachments...\n";
+
+    my $sth = $self->prepare("SELECT COUNT(attach_id) FROM attachments");
+    $sth->execute();
+    my ($attach_count) = $sth->fetchrow_array();
+
+    if ($attach_count > 1000) {
+      print "This may take a while...\n";
+    }
+    my $i = 0;
+
+    # This isn't just as simple as changing the field type, because
+    # the creation_ts was previously updated when an attachment was made
+    # obsolete from the attachment creation screen. So we have to go
+    # and recreate these times from the comments..
+    $sth = $self->prepare(
+      "SELECT bug_id, attach_id, submitter_id " . "FROM attachments");
+    $sth->execute();
+
+    # Restrict this as much as possible in order to avoid false
+    # positives, and keep the db search time down
+    my $sth2 = $self->prepare(
+      "SELECT bug_when FROM longdescs 
+                                    WHERE bug_id=? AND who=? 
+                                          AND thetext LIKE ?
+                                 ORDER BY bug_when " . $self->sql_limit(1)
+    );
+    while (my ($bug_id, $attach_id, $submitter_id) = $sth->fetchrow_array()) {
+      $sth2->execute($bug_id, $submitter_id,
+        "Created an attachment (id=$attach_id)%");
+      my ($when) = $sth2->fetchrow_array();
+      if ($when) {
+        $self->do("UPDATE attachments "
+            . "SET creation_ts='$when' "
+            . "WHERE attach_id=$attach_id");
+      }
+      else {
+        print "Warning - could not determine correct creation"
+          . " time for attachment $attach_id on bug $bug_id\n";
+      }
+      ++$i;
+      print "Converted $i of $attach_count attachments\n" if !($i % 1000);
+    }
+    print "Done - converted $i attachments\n";
+
+    $self->bz_alter_column("attachments", "creation_ts",
+      {TYPE => 'DATETIME', NOTNULL => 1});
+  }
+
+  # 2004-08-29 - Tomas.Kopal@altap.cz, bug 257303
+  # Change logincookies.lastused type from timestamp to datetime
+  my $login_lastused = $self->bz_column_info("logincookies", "lastused");
+  if ($login_lastused && $login_lastused->{TYPE} =~ /^TIMESTAMP/i) {
+    $self->bz_alter_column('logincookies', 'lastused',
+      {TYPE => 'DATETIME', NOTNULL => 1});
+  }
+
+  # 2005-01-17 - Tomas.Kopal@altap.cz, bug 257315
+  # Change bugs.delta_ts type from timestamp to datetime
+  my $bugs_deltats = $self->bz_column_info("bugs", "delta_ts");
+  if ($bugs_deltats && $bugs_deltats->{TYPE} =~ /^TIMESTAMP/i) {
+    $self->bz_alter_column('bugs', 'delta_ts', {TYPE => 'DATETIME', NOTNULL => 1});
+  }
+
+  # 2005-09-24 - bugreport@peshkin.net, bug 307602
+  # Make sure that default 4G table limit is overridden
+  my $attach_data_create = $self->selectrow_array(
+    'SELECT CREATE_OPTIONS FROM information_schema.TABLES 
+          WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?', undef, $db_name, 'attach_data'
+  );
+  if ($attach_data_create !~ /MAX_ROWS/i) {
+    print "Converting attach_data maximum size to 100G...\n";
+    $self->do(
+      "ALTER TABLE attach_data
+                   AVG_ROW_LENGTH=1000000,
+                   MAX_ROWS=100000"
+    );
+  }
+
+  # Convert the database to UTF-8 if the utf8 parameter is on.
+  # We check if any table isn't utf8, because lots of crazy
+  # partial-conversion situations can happen, and this handles anything
+  # that could come up (including having the DB charset be utf8 but not
+  # the table charsets.
+  #
+  # TABLE_COLLATION IS NOT NULL prevents us from trying to convert views.
+  my $non_utf8_tables = $self->selectrow_array(
+    "SELECT 1 FROM information_schema.TABLES 
+          WHERE TABLE_SCHEMA = ? AND TABLE_COLLATION IS NOT NULL 
+                AND TABLE_COLLATION != ?
+          LIMIT 1", undef, $db_name, $collate
+  );
+
+  if (Bugzilla->params->{'utf8'} && $non_utf8_tables) {
+    print "\n", install_string('mysql_utf8_conversion');
+
+    if (!Bugzilla->installation_answers->{NO_PAUSE}) {
+      if (Bugzilla->installation_mode == INSTALLATION_MODE_NON_INTERACTIVE) {
+        die install_string('continue_without_answers'), "\n";
+      }
+      else {
+        print "\n         " . install_string('enter_or_ctrl_c');
+        getc;
+      }
+    }
+
+    print
+      "Converting table storage format to $charset (collate $collate). This may take a while.\n";
+    foreach my $table ($self->bz_table_list_real) {
+      my $info_sth = $self->prepare("SHOW FULL COLUMNS FROM " . $self->quote_identifier($table));
+      $info_sth->execute();
+      my (@binary_sql, @utf8_sql);
+      while (my $column = $info_sth->fetchrow_hashref) {
+
+        # Our conversion code doesn't work on enum fields, but they
+        # all go away later in checksetup anyway.
+        next if $column->{Type} =~ /enum/i;
+
+        # If this particular column isn't stored in utf-8
+        if ( $column->{Collation}
+          && $column->{Collation} ne 'NULL'
+          && $column->{Collation} ne $collate)
+        {
+          my $name = $column->{Field};
+
+          print "$table.$name needs to be converted to $charset (collate $collate)...\n";
+
+          # These will be automatically re-created at the end
+          # of checksetup.
+          $self->bz_drop_related_fks($table, $name);
+
+          my $col_info = $self->bz_column_info_real($table, $name);
+
+          # CHANGE COLUMN doesn't take PRIMARY KEY
+          delete $col_info->{PRIMARYKEY};
+          my $sql_def = $self->_bz_schema->get_type_ddl($col_info);
+
+          # We don't want MySQL to actually try to *convert*
+          # from our current charset to UTF-8, we just want to
+          # transfer the bytes directly. This is how we do that.
+
+          # The CHARACTER SET part of the definition has to come
+          # right after the type, which will always come first.
+          my ($binary, $utf8) = ($sql_def, $sql_def);
+          my $type = $self->_bz_schema->convert_type($col_info->{TYPE});
+          $binary =~ s/(\Q$type\E)/$1 CHARACTER SET binary/;
+          $utf8   =~ s/(\Q$type\E)/$1 CHARACTER SET $charset COLLATE $collate/;
+          push(@binary_sql, "MODIFY COLUMN $name $binary");
+          push(@utf8_sql,   "MODIFY COLUMN $name $utf8");
+        }
+      }    # foreach column
+
+      if (@binary_sql) {
+        my %indexes = %{$self->bz_table_indexes($table)};
+        foreach my $index_name (keys %indexes) {
+          my $index = $indexes{$index_name};
+          if ($index->{TYPE} and $index->{TYPE} eq 'FULLTEXT') {
+            $self->bz_drop_index($table, $index_name);
+          }
+          else {
+            delete $indexes{$index_name};
+          }
+        }
+
+        print "Converting the $table table to UTF-8...\n";
+        my $bin
+          = 'ALTER TABLE '
+          . $self->quote_identifier($table) . ' '
+          . join(', ', @binary_sql);
+        my $utf
+          = 'ALTER TABLE '
+          . $self->quote_identifier($table) . ' '
+          . join(', ', @utf8_sql, "DEFAULT CHARACTER SET $charset COLLATE $collate");
+        $self->do($bin);
+        $self->do($utf);
+
+        # Re-add any removed FULLTEXT indexes.
+        foreach my $index (keys %indexes) {
+          $self->bz_add_index($table, $index, $indexes{$index});
+        }
+      }
+      else {
+        $self->do('ALTER TABLE '
+            . $self->quote_identifier($table)
+            . " DEFAULT CHARACTER SET $charset COLLATE $collate");
+      }
+
+    }    # foreach my $table (@tables)
+  }
+
+  # Sometimes you can have a situation where all the tables are utf8,
+  # but the database isn't. (This tends to happen when you've done
+  # a mysqldump.) So we have this change outside of the above block,
+  # so that it just happens silently if no actual *table* conversion
+  # needs to happen.
+  unless ($self->bz_db_is_utf8) {
+    $self->_alter_db_charset_to_utf8();
+  }
+
+  $self->_fix_defaults();
+
+  # Bug 451735 highlighted a bug in bz_drop_index() which didn't
+  # check for FKs before trying to delete an index. Consequently,
+  # the series_creator_idx index was considered to be deleted
+  # despite it was still present in the DB. That's why we have to
+  # force the deletion, bypassing the DB schema.
+  if (!$self->bz_index_info('series', 'series_category_idx')) {
+    if (!$self->bz_index_info('series', 'series_creator_idx')
+      && $self->bz_index_info_real('series', 'series_creator_idx'))
+    {
+      foreach my $column (qw(creator category subcategory name)) {
+        $self->bz_drop_related_fks('series', $column);
+      }
+      $self->bz_drop_index_raw('series', 'series_creator_idx');
+    }
+  }
+}
+
+# When you import a MySQL 3/4 mysqldump into MySQL 5, columns that
+# aren't supposed to have defaults will have defaults. This is only
+# a minor issue, but it makes our tests fail, and it's good to keep
+# the DB actually consistent with what DB::Schema thinks the database
+# looks like. So we remove defaults from columns that aren't supposed
+# to have them
+sub _fix_defaults {
+  my $self        = shift;
+  my $maj_version = substr($self->bz_server_version, 0, 1);
+  return if $maj_version < 5;
+
+  # The oldest column that could have this problem is bugs.assigned_to,
+  # so if it doesn't have the problem, we just skip doing this entirely.
+  my $assi_def     = $self->_bz_raw_column_info('bugs', 'assigned_to');
+  my $assi_default = $assi_def->{COLUMN_DEF};
+
+  # This "ne ''" thing is necessary because _raw_column_info seems to
+  # return COLUMN_DEF as an empty string for columns that don't have
+  # a default.
+  return unless (defined $assi_default && $assi_default ne '');
+
+  my %fix_columns;
+  foreach my $table ($self->_bz_real_schema->get_table_list()) {
+    foreach my $column ($self->bz_table_columns($table)) {
+      my $abs_def = $self->bz_column_info($table, $column);
+
+      # BLOB/TEXT columns never have defaults
+      next if $abs_def->{TYPE} =~ /BLOB|TEXT/i;
+      if (!defined $abs_def->{DEFAULT}) {
+
+        # Get the exact default from the database without any
+        # "fixing" by bz_column_info_real.
+        my $raw_info    = $self->_bz_raw_column_info($table, $column);
+        my $raw_default = $raw_info->{COLUMN_DEF};
+        if (defined $raw_default) {
+          if ($raw_default eq '') {
+
+            # Only (var)char columns can have empty strings as
+            # defaults, so if we got an empty string for some
+            # other default type, then it's bogus.
+            next unless $abs_def->{TYPE} =~ /char/i;
+            $raw_default = "''";
+          }
+          $fix_columns{$table} ||= [];
+          push(@{$fix_columns{$table}}, $column);
+          print "$table.$column has incorrect DB default: $raw_default\n";
+        }
+      }
+    }    # foreach $column
+  }    # foreach $table
+
+  print "Fixing defaults...\n";
+  foreach my $table (reverse sort keys %fix_columns) {
+    my @alters = map("ALTER COLUMN $_ DROP DEFAULT", @{$fix_columns{$table}});
+    my $sql
+        = 'ALTER TABLE ' . $self->quote_identifier($table) . ' ' . join(',', @alters);
+    $self->do($sql);
+  }
+}
+
+sub utf8_charset {
+  return 'utf8mb4' unless Bugzilla->params->{'utf8'};
+  return 'utf8mb4' if Bugzilla->params->{'utf8'} eq '1';
+  return Bugzilla->params->{'utf8'};
+}
+
+sub utf8_collate {
+  my $charset = utf8_charset();
+  return $charset . '_unicode_520_ci' unless Bugzilla->params->{'utf8_collate'};
+  return $charset . '_unicode_520_ci' unless (Bugzilla->params->{'utf8_collate'} =~ /^${charset}_/);
+  return Bugzilla->params->{'utf8_collate'};
+}
+
+sub default_row_format {
+  my ($class, $table) = @_;
+  my $charset = utf8_charset();
+  if ($charset eq 'utf8') {
+    return 'Compact';
+  }
+  elsif (($charset eq 'utf8mb4') || ($charset eq 'utf8mb3')) {
+    my @no_compress = qw(
+      bug_user_last_visit
+      cc
+      email_rates
+      logincookies
+      token_data
+      tokens
+      ts_error
+      ts_exitstatus
+      ts_funcmap
+      ts_job
+      ts_note
+      user_request_log
+      votes
+    );
+    return 'Dynamic' if any { $table eq $_ } @no_compress;
+    return 'Compressed';
+  }
+  else {
+    croak "invalid charset: $charset";
+  }
+}
+
+sub _alter_db_charset_to_utf8 {
+  my $self    = shift;
+  my $db_name = Bugzilla->localconfig->{db_name};
+  my $charset = $self->utf8_charset;
+  my $collate = $self->utf8_collate;
+  $self->do("ALTER DATABASE $db_name CHARACTER SET $charset COLLATE $collate");
+}
+
+sub bz_db_is_utf8 {
+  my $self = shift;
+  my $db_charset
+    = $self->selectrow_arrayref("SHOW VARIABLES LIKE 'character_set_database'");
+
+  # First column holds the variable name, second column holds the value.
+  my $charset = $self->utf8_charset;
+  return $db_charset->[1] eq $charset ? 1 : 0;
+}
+
+
+sub bz_enum_initial_values {
+  my ($self) = @_;
+  my %enum_values = %{$self->ENUM_DEFAULTS};
+
+  # Get a complete description of the 'bugs' table; with DBD::MySQL
+  # there isn't a column-by-column way of doing this.  Could use
+  # $dbh->column_info, but it would go slower and we would have to
+  # use the undocumented mysql_type_name accessor to get the type
+  # of each row.
+  my $sth = $self->prepare("DESCRIBE bugs");
+  $sth->execute();
+
+  # Look for the particular columns we are interested in.
+  while (my ($thiscol, $thistype) = $sth->fetchrow_array()) {
+    if (defined $enum_values{$thiscol}) {
+
+      # this is a column of interest.
+      my @value_list;
+      if ($thistype and ($thistype =~ /^enum\(/)) {
+
+        # it has an enum type; get the set of values.
+        while ($thistype =~ /'([^']*)'(.*)/) {
+          push(@value_list, $1);
+          $thistype = $2;
+        }
+      }
+      if (@value_list) {
+
+        # record the enum values found.
+        $enum_values{$thiscol} = \@value_list;
+      }
+    }
+  }
+
+  return \%enum_values;
+}
+
+#####################################################################
+# MySQL-specific Database-Reading Methods
+#####################################################################
+
+=begin private
+
+=head1 MYSQL-SPECIFIC DATABASE-READING METHODS
+
+These methods read information about the database from the disk,
+instead of from a Schema object. They are only reliable for MySQL 
+(see bug 285111 for the reasons why not all DBs use/have functions
+like this), but that's OK because we only need them for 
+backwards-compatibility anyway, for versions of Bugzilla before 2.20.
+
+=over 4
+
+=item C<bz_column_info_real($table, $column)>
+
+ Description: Returns an abstract column definition for a column
+              as it actually exists on disk in the database.
+ Params:      $table - The name of the table the column is on.
+              $column - The name of the column you want info about.
+ Returns:     An abstract column definition.
+              If the column does not exist, returns undef.
+
+=cut
+
+sub bz_column_info_real {
+  my ($self, $table, $column) = @_;
+  my $col_data = $self->_bz_raw_column_info($table, $column);
+  return $self->_bz_schema->column_info_to_column($col_data);
+}
+
+sub _bz_raw_column_info {
+  my ($self, $table, $column) = @_;
+
+  # DBD::mysql does not support selecting a specific column,
+  # so we have to get all the columns on the table and find
+  # the one we want.
+  my $info_sth = $self->column_info(undef, undef, $table, '%');
+
+  # Don't use fetchall_hashref as there's a Win32 DBI bug (292821)
+  my $col_data;
+  while ($col_data = $info_sth->fetchrow_hashref) {
+    last if $col_data->{'COLUMN_NAME'} eq $column;
+  }
+
+  if (!defined $col_data) {
+    return undef;
+  }
+  return $col_data;
+}
+
+=item C<bz_index_info_real($table, $index)>
+
+ Description: Returns information about an index on a table in the database.
+ Params:      $table = name of table containing the index
+              $index = name of an index
+ Returns:     An abstract index definition, always in hashref format.
+              If the index does not exist, the function returns undef.
+
+=cut
+
+sub bz_index_info_real {
+  my ($self, $table, $index) = @_;
+
+  my $sth = $self->prepare("SHOW INDEX FROM $table");
+  $sth->execute;
+
+  my @fields;
+  my $index_type;
+
+  # $raw_def will be an arrayref containing the following information:
+  # 0 = name of the table that the index is on
+  # 1 = 0 if unique, 1 if not unique
+  # 2 = name of the index
+  # 3 = seq_in_index (The order of the current field in the index).
+  # 4 = Name of ONE column that the index is on
+  # 5 = 'Collation' of the index. Usually 'A'.
+  # 6 = Cardinality. Either a number or undef.
+  # 7 = sub_part. Usually undef. Sometimes 1.
+  # 8 = "packed". Usually undef.
+  # 9 = Null. Sometimes undef, sometimes 'YES'.
+  # 10 = Index_type. The type of the index. Usually either 'BTREE' or 'FULLTEXT'
+  # 11 = 'Comment.' Usually undef.
+  while (my $raw_def = $sth->fetchrow_arrayref) {
+    if ($raw_def->[2] eq $index) {
+      push(@fields, $raw_def->[4]);
+
+      # No index can be both UNIQUE and FULLTEXT, that's why
+      # this is written this way.
+      $index_type = $raw_def->[1] ? '' : 'UNIQUE';
+      $index_type = $raw_def->[10] eq 'FULLTEXT' ? 'FULLTEXT' : $index_type;
+    }
+  }
+
+  my $retval;
+  if (scalar(@fields)) {
+    $retval = {FIELDS => \@fields, TYPE => $index_type};
+  }
+  return $retval;
+}
+
+=item C<bz_index_list_real($table)>
+
+ Description: Returns a list of index names on a table in 
+              the database, as it actually exists on disk.
+ Params:      $table - The name of the table you want info about.
+ Returns:     An array of index names.
+
+=cut
+
+sub bz_index_list_real {
+  my ($self, $table) = @_;
+  my $sth = $self->prepare("SHOW INDEX FROM $table");
+
+  # Column 3 of a SHOW INDEX statement contains the name of the index.
+  return @{$self->selectcol_arrayref($sth, {Columns => [3]})};
+}
+
+#####################################################################
+# MySQL-Specific "Schema Builder"
+#####################################################################
+
+=back
+
+=head1 MYSQL-SPECIFIC "SCHEMA BUILDER"
+
+MySQL needs to be able to read in a legacy database (from before 
+Schema existed) and create a Schema object out of it. That's what
+this code does.
+
+=end private
+
+=cut
+
+# This sub itself is actually written generically, but the subroutines
+# that it depends on are database-specific. In particular, the
+# bz_column_info_real function would be very difficult to create
+# properly for any other DB besides MySQL.
+sub _bz_build_schema_from_disk {
+  my ($self) = @_;
+
+  my $schema = $self->_bz_schema->get_empty_schema();
+
+  my @tables = $self->bz_table_list_real();
+  if (@tables) {
+    print "Building Schema object from database...\n";
+  }
+  foreach my $table (@tables) {
+    $schema->add_table($table);
+    my @columns = $self->bz_table_columns_real($table);
+    foreach my $column (@columns) {
+      my $type_info = $self->bz_column_info_real($table, $column);
+      $schema->set_column($table, $column, $type_info);
+    }
+
+    my @indexes = $self->bz_index_list_real($table);
+    foreach my $index (@indexes) {
+      unless ($index eq 'PRIMARY') {
+        my $index_info = $self->bz_index_info_real($table, $index);
+        ($index_info = $index_info->{FIELDS}) if (!$index_info->{TYPE});
+        $schema->set_index($table, $index, $index_info);
+      }
+    }
+  }
+
+  return $schema;
+}
+
+1;
+
+=head1 METHODS
+
+=head2 BUILDARGS
+
+This method is called by L<Moo> when creating a new object. It turns the flat
+named arguments from C<new()> and puts them into the C<dsn>, C<user>, C<pass>,
+and C<attr> attributes.
+
+=head2 on_dbi_connected
+
+This method is called by L<DBI> when a connection is established. It sets various connection-specific attributes
+which are nessessary for the database to function correctly. Because the database can be reconnected to 
+any required session variables must be set here.
+
+Undocumented methods: utf8_charset, utf8_collate, default_row_format'
+
+=head2 utf8_charset
+
+Returns the name of the charset to use for utf8 columns.
+This comes from the C<Bugzilla-E<gt>params-E<gt>{utf8}> parameter.
+It can be either true, false, or utf8mb4
+
+=head2 utf8_collate
+
+Returns the name of the collation to use for utf8 columns.
+When C<utf8_charset> is C<utf8mb4> this is C<utf8mb4_unicode_520_ci>.
+Otherwise it is C<utf8_general_ci>.
+
+=head2 default_row_format
+
+Returns the default row format to use for tables.
+When C<utf8_charset> is C<utf8mb4> this is C<DYNAMIC> for most tables,
+and C<COMPRESSED> for several table that benefit from compression.
+
+When C<utf8_charset> is C<utf8> this is C<COMPACT> for all tables.
+
+=head2 sql_date_format
+
+Returns the SQL date format string for the current database.
+
+=head2 bz_explain
+
+Returns the EXPLAIN output for the given SQL statement.
+
+=head2 bz_last_key
+
+Returns the last auto-increment key generated by the database.
+
+=head2 sql_position
+
+Returns the SQL position function for the current database.
+
+=head2 sql_fulltext_search
+
+Returns the SQL fulltext search function for the current database.
+
+=head2 sql_iposition
+
+Returns the SQL position function for the current database, but
+case-insensitive.
+
+=head2 bz_enum_initial_values
+
+Returns the initial values for an ENUM column.
+
+=head2 sql_group_by
+
+Returns the SQL GROUP BY clause for the current database.
+
+=head2 sql_limit
+
+Returns the SQL LIMIT clause for the current database.
+
+=head2 sql_not_regexp
+
+Returns the SQL NOT REGEXP operator for the current database.
+
+=head2 sql_string_concat
+
+Returns the SQL string concatenation operator for the current database.
+
+=head2 sql_date_math
+
+Returns the SQL date math operator for the current database.
+
+=head2 sql_to_days
+
+Returns the SQL to_days function for the current database.
+
+=head2 bz_check_server_version
+
+Returns true if the database server version is at least the given
+
+=head2 sql_from_days
+
+Returns the SQL from_days function for the current database.
+
+=head2 sql_regexp
+
+Returns the SQL REGEXP operator for the current database.
+
+=head2 sql_istring
+
+Returns the SQL string case-insensitive operator for the current database.
+
+=head2 sql_group_concat
+
+Returns the SQL GROUP_CONCAT function for the current database.
+
+=head2 bz_setup_database
+
+Sets up the database for use with Bugzilla.
+
+=head2 bz_db_is_utf8
+
+Returns true if the database is using UTF-8.
+
diff --git a/Bugzilla/DB/Mysql.pm b/Bugzilla/DB/Mysql.pm
index a58d88df4..826cd6272 100644
--- a/Bugzilla/DB/Mysql.pm
+++ b/Bugzilla/DB/Mysql.pm
@@ -21,20 +21,21 @@ For interface details see L<Bugzilla::DB> and L<DBI>.
 
 package Bugzilla::DB::Mysql;
 
-use 5.10.1;
-use strict;
-use warnings;
+use 5.14.0;
+use Moo;
 
-use parent qw(Bugzilla::DB);
+extends qw(Bugzilla::DB);
 
 use Bugzilla::Constants;
 use Bugzilla::Install::Util qw(install_string);
+use Bugzilla::Config;
 use Bugzilla::Util;
 use Bugzilla::Error;
 use Bugzilla::DB::Schema::Mysql;
 
-use List::Util qw(max);
+use List::Util qw(max any);
 use Text::ParseWords;
+use Carp;
 
 # This is how many comments of MAX_COMMENT_LENGTH we expect on a single bug.
 # In reality, you could have a LOT more comments than this, because
@@ -43,7 +44,7 @@ use constant MAX_COMMENTS => 50;
 
 use constant FULLTEXT_OR => '|';
 
-sub new {
+sub BUILDARGS {
   my ($class, $params) = @_;
   my ($user, $pass, $host, $dbname, $port, $sock)
     = @$params{qw(db_user db_pass db_host db_name db_port db_sock)};
@@ -53,12 +54,7 @@ sub new {
   $dsn .= ";port=$port"         if $port;
   $dsn .= ";mysql_socket=$sock" if $sock;
 
-  my %attrs = (
-    mysql_enable_utf8 => Bugzilla->params->{'utf8'},
-
-    # Needs to be explicitly specified for command-line processes.
-    mysql_auto_reconnect => 1,
-  );
+  my %attrs = (mysql_enable_utf8 => 1);
 
   # MySQL SSL options
   my ($ssl_ca_file, $ssl_ca_path, $ssl_cert, $ssl_key) = @$params{
@@ -73,25 +69,21 @@ sub new {
     $attrs{'mysql_ssl_client_key'}  = $ssl_key if $ssl_key;
   }
 
-  my $self = $class->db_new(
-    {dsn => $dsn, user => $user, pass => $pass, attrs => \%attrs});
+  return {dsn => $dsn, user => $user, pass => $pass, attrs => \%attrs};
+}
+
+sub on_dbi_connected {
+  my ($class, $dbh) = @_;
 
   # This makes sure that if the tables are encoded as UTF-8, we
   # return their data correctly.
-  $self->do("SET NAMES utf8") if Bugzilla->params->{'utf8'};
-
-  # all class local variables stored in DBI derived class needs to have
-  # a prefix 'private_'. See DBI documentation.
-  $self->{private_bz_tables_locked} = "";
-
-  # Needed by TheSchwartz
-  $self->{private_bz_dsn} = $dsn;
-
-  bless($self, $class);
+  my $charset = $class->utf8_charset;
+  my $collate = $class->utf8_collate;
+  $dbh->do("SET NAMES $charset COLLATE $collate");
 
   # Check for MySQL modes.
   my ($var, $sql_mode)
-    = $self->selectrow_array("SHOW VARIABLES LIKE 'sql\\_mode'");
+    = $dbh->selectrow_array("SHOW VARIABLES LIKE 'sql\\_mode'");
 
   # Disable ANSI and strict modes, else Bugzilla will crash.
   if ($sql_mode) {
@@ -104,19 +96,17 @@ sub new {
         split(/,/, $sql_mode));
 
     if ($sql_mode ne $new_sql_mode) {
-      $self->do("SET SESSION sql_mode = ?", undef, $new_sql_mode);
+      $dbh->do("SET SESSION sql_mode = ?", undef, $new_sql_mode);
     }
   }
 
   # Allow large GROUP_CONCATs (largely for inserting comments
   # into bugs_fulltext).
-  $self->do('SET SESSION group_concat_max_len = 128000000');
+  $dbh->do('SET SESSION group_concat_max_len = 128000000');
 
   # MySQL 5.5.2 and older have this variable set to true, which causes
   # trouble, see bug 870369.
-  $self->do('SET SESSION sql_auto_is_null = 0');
-
-  return $self;
+  $dbh->do('SET SESSION sql_auto_is_null = 0');
 }
 
 # when last_insert_id() is supported on MySQL by lowest DBI/DBD version
@@ -317,6 +307,24 @@ sub bz_check_server_version {
 sub bz_setup_database {
   my ($self) = @_;
 
+  # Before touching anything else, find out whether this database server does
+  # any aliasing of the character set we plan to use so we can check for
+  # already converted tables properly. We do this by creating a table as our
+  # intended charset and then test how it reads back.
+  my $db_name = Bugzilla->localconfig->{db_name};
+  my $charset = $self->utf8_charset;
+  my $collate = $self->utf8_collate;
+  $self->do("CREATE TABLE `utf8_test` (id tinyint) CHARACTER SET ? COLLATE ?", undef, $charset, $collate);
+  my ($found_collate) = $self->selectrow_array("SELECT TABLE_COLLATION FROM information_schema.TABLES WHERE TABLE_SCHEMA=? AND TABLE_NAME='utf8_test'", undef, $db_name);
+  $self->do("DROP TABLE `utf8_test`");
+  my ($found_charset) = ($found_collate =~ m/^([a-z0-9]+)_/);
+  Bugzilla->params->{'utf8'} = $found_charset;
+  Bugzilla->params->{'utf8_collate'} = $found_collate;
+  Bugzilla::Config::write_params();
+  # reload these because they get used later.
+  $charset = $self->utf8_charset;
+  $collate = $self->utf8_collate;
+
   # The "comments" field of the bugs_fulltext table could easily exceed
   # MySQL's default max_allowed_packet. Also, MySQL should never have
   # a max_allowed_packet smaller than our max_attachment_size. So, we
@@ -346,6 +354,45 @@ sub bz_setup_database {
     die install_string('mysql_innodb_disabled');
   }
 
+  if ($self->utf8_charset eq 'utf8mb4') {
+    my %global = map {@$_}
+      @{$self->selectall_arrayref(q(SHOW GLOBAL VARIABLES LIKE 'innodb_%'))};
+
+    # In versions of MySQL > 8, the default value for innodb_file_format is Barracuda
+    # and the setting was deprecated. Also innodb_file_per_table also now defaults
+    # to ON. innodb_large_prefix has also been removed in newer MySQL versions.
+    my $utf8mb4_supported
+      = (!exists $global{innodb_file_format}
+        || $global{innodb_file_format} eq 'Barracuda')
+      && (!exists $global{innodb_file_per_table}
+      || $global{innodb_file_per_table} eq 'ON')
+      && (!exists $global{innodb_large_prefix}
+      || $global{innodb_large_prefix} eq 'ON');
+
+    die install_string('mysql_innodb_settings') unless $utf8mb4_supported;
+
+    my $tables = $self->selectall_arrayref('SHOW TABLE STATUS');
+    foreach my $table_status (@$tables) {
+      my ($table, undef, undef, $row_format) = @$table_status;
+      my $table_type     = $table_status->[-1];
+      my $new_row_format = $self->default_row_format($table);
+      next if lc($table_type) eq 'view';
+      next if lc($new_row_format) eq 'compact';
+      next if lc($row_format) eq 'dynamic';
+      next if lc($row_format) eq 'compressed';
+      if (lc($new_row_format) ne lc($row_format)) {
+        print install_string(
+          'mysql_row_format_conversion', {table => $table, format => $new_row_format}
+          ),
+          "\n";
+        $self->do(
+          sprintf 'ALTER TABLE %s ROW_FORMAT=%s',
+          $self->quote_identifier($table),
+          $new_row_format
+        );
+      }
+    }
+  }
 
   my ($sd_index_deleted, $longdescs_index_deleted);
   my @tables = $self->bz_table_list_real();
@@ -374,21 +421,17 @@ sub bz_setup_database {
   }
 
   # Upgrade tables from MyISAM to InnoDB
-  my $db_name       = Bugzilla->localconfig->{db_name};
   my $myisam_tables = $self->selectcol_arrayref(
     'SELECT TABLE_NAME FROM information_schema.TABLES 
           WHERE TABLE_SCHEMA = ? AND ENGINE = ?', undef, $db_name, 'MyISAM'
   );
-  foreach my $should_be_myisam (Bugzilla::DB::Schema::Mysql::MYISAM_TABLES) {
-    @$myisam_tables = grep { $_ ne $should_be_myisam } @$myisam_tables;
-  }
 
   if (scalar @$myisam_tables) {
     print "Bugzilla now uses the InnoDB storage engine in MySQL for",
       " most tables.\nConverting tables to InnoDB:\n";
     foreach my $table (@$myisam_tables) {
       print "Converting table $table... ";
-      $self->do("ALTER TABLE $table ENGINE = InnoDB");
+      $self->do('ALTER TABLE ' . $self->quote_identifier($table) . ' ENGINE = InnoDB');
       print "done.\n";
     }
   }
@@ -564,10 +607,7 @@ sub bz_setup_database {
   # This kind of situation happens when people create the database
   # themselves, and if we don't do this they will get the big
   # scary WARNING statement about conversion to UTF8.
-  if ( !$self->bz_db_is_utf8
-    && !@tables
-    && (Bugzilla->params->{'utf8'} || !scalar keys %{Bugzilla->params}))
-  {
+  unless ($self->bz_db_is_utf8) {
     $self->_alter_db_charset_to_utf8();
   }
 
@@ -681,8 +721,8 @@ sub bz_setup_database {
   my $non_utf8_tables = $self->selectrow_array(
     "SELECT 1 FROM information_schema.TABLES 
           WHERE TABLE_SCHEMA = ? AND TABLE_COLLATION IS NOT NULL 
-                AND TABLE_COLLATION NOT LIKE 'utf8%' 
-          LIMIT 1", undef, $db_name
+                AND TABLE_COLLATION != ?
+          LIMIT 1", undef, $db_name, $collate
   );
 
   if (Bugzilla->params->{'utf8'} && $non_utf8_tables) {
@@ -698,9 +738,10 @@ sub bz_setup_database {
       }
     }
 
-    print "Converting table storage format to UTF-8. This may take a", " while.\n";
+    print
+      "Converting table storage format to $charset (collate $collate). This may take a while.\n";
     foreach my $table ($self->bz_table_list_real) {
-      my $info_sth = $self->prepare("SHOW FULL COLUMNS FROM $table");
+      my $info_sth = $self->prepare("SHOW FULL COLUMNS FROM " . $self->quote_identifier($table));
       $info_sth->execute();
       my (@binary_sql, @utf8_sql);
       while (my $column = $info_sth->fetchrow_hashref) {
@@ -712,11 +753,11 @@ sub bz_setup_database {
         # If this particular column isn't stored in utf-8
         if ( $column->{Collation}
           && $column->{Collation} ne 'NULL'
-          && $column->{Collation} !~ /utf8/)
+          && $column->{Collation} ne $collate)
         {
           my $name = $column->{Field};
 
-          print "$table.$name needs to be converted to UTF-8...\n";
+          print "$table.$name needs to be converted to $charset (collate $collate)...\n";
 
           # These will be automatically re-created at the end
           # of checksetup.
@@ -737,7 +778,7 @@ sub bz_setup_database {
           my ($binary, $utf8) = ($sql_def, $sql_def);
           my $type = $self->_bz_schema->convert_type($col_info->{TYPE});
           $binary =~ s/(\Q$type\E)/$1 CHARACTER SET binary/;
-          $utf8   =~ s/(\Q$type\E)/$1 CHARACTER SET utf8/;
+          $utf8   =~ s/(\Q$type\E)/$1 CHARACTER SET $charset COLLATE $collate/;
           push(@binary_sql, "MODIFY COLUMN $name $binary");
           push(@utf8_sql,   "MODIFY COLUMN $name $utf8");
         }
@@ -756,9 +797,14 @@ sub bz_setup_database {
         }
 
         print "Converting the $table table to UTF-8...\n";
-        my $bin = "ALTER TABLE $table " . join(', ', @binary_sql);
+        my $bin
+          = 'ALTER TABLE '
+          . $self->quote_identifier($table) . ' '
+          . join(', ', @binary_sql);
         my $utf
-          = "ALTER TABLE $table " . join(', ', @utf8_sql, 'DEFAULT CHARACTER SET utf8');
+          = 'ALTER TABLE '
+          . $self->quote_identifier($table) . ' '
+          . join(', ', @utf8_sql, "DEFAULT CHARACTER SET $charset COLLATE $collate");
         $self->do($bin);
         $self->do($utf);
 
@@ -768,7 +814,9 @@ sub bz_setup_database {
         }
       }
       else {
-        $self->do("ALTER TABLE $table DEFAULT CHARACTER SET utf8");
+        $self->do('ALTER TABLE '
+            . $self->quote_identifier($table)
+            . " DEFAULT CHARACTER SET $charset COLLATE $collate");
       }
 
     }    # foreach my $table (@tables)
@@ -779,7 +827,7 @@ sub bz_setup_database {
   # a mysqldump.) So we have this change outside of the above block,
   # so that it just happens silently if no actual *table* conversion
   # needs to happen.
-  if (Bugzilla->params->{'utf8'} && !$self->bz_db_is_utf8) {
+  unless ($self->bz_db_is_utf8) {
     $self->_alter_db_charset_to_utf8();
   }
 
@@ -856,24 +904,71 @@ sub _fix_defaults {
   print "Fixing defaults...\n";
   foreach my $table (reverse sort keys %fix_columns) {
     my @alters = map("ALTER COLUMN $_ DROP DEFAULT", @{$fix_columns{$table}});
-    my $sql    = "ALTER TABLE $table " . join(',', @alters);
+    my $sql
+        = 'ALTER TABLE ' . $self->quote_identifier($table) . ' ' . join(',', @alters);
     $self->do($sql);
   }
 }
 
+sub utf8_charset {
+  return 'utf8mb4' unless Bugzilla->params->{'utf8'};
+  return 'utf8mb4' if Bugzilla->params->{'utf8'} eq '1';
+  return Bugzilla->params->{'utf8'};
+}
+
+sub utf8_collate {
+  my $charset = utf8_charset();
+  return $charset . '_unicode_520_ci' unless Bugzilla->params->{'utf8_collate'};
+  return $charset . '_unicode_520_ci' unless (Bugzilla->params->{'utf8_collate'} =~ /^${charset}_/);
+  return Bugzilla->params->{'utf8_collate'};
+}
+
+sub default_row_format {
+  my ($class, $table) = @_;
+  my $charset = utf8_charset();
+  if ($charset eq 'utf8') {
+    return 'Compact';
+  }
+  elsif ($charset eq 'utf8mb4') {
+    my @no_compress = qw(
+      bug_user_last_visit
+      cc
+      email_rates
+      logincookies
+      token_data
+      tokens
+      ts_error
+      ts_exitstatus
+      ts_funcmap
+      ts_job
+      ts_note
+      user_request_log
+      votes
+    );
+    return 'Dynamic' if any { $table eq $_ } @no_compress;
+    return 'Compressed';
+  }
+  else {
+    croak "invalid charset: $charset";
+  }
+}
+
 sub _alter_db_charset_to_utf8 {
   my $self    = shift;
   my $db_name = Bugzilla->localconfig->{db_name};
-  $self->do("ALTER DATABASE $db_name CHARACTER SET utf8");
+  my $charset = $self->utf8_charset;
+  my $collate = $self->utf8_collate;
+  $self->do("ALTER DATABASE $db_name CHARACTER SET $charset COLLATE $collate");
 }
 
 sub bz_db_is_utf8 {
   my $self = shift;
-  my $db_collation
+  my $db_charset
     = $self->selectrow_arrayref("SHOW VARIABLES LIKE 'character_set_database'");
 
   # First column holds the variable name, second column holds the value.
-  return $db_collation->[1] =~ /utf8/ ? 1 : 0;
+  my $charset = $self->utf8_charset;
+  return $db_charset->[1] eq $charset ? 1 : 0;
 }
 
 
@@ -1086,48 +1181,120 @@ sub _bz_build_schema_from_disk {
 
 1;
 
-=head1 B<Methods in need of POD>
+=head1 METHODS
 
-=over
+=head2 BUILDARGS
 
-=item sql_date_format
+This method is called by L<Moo> when creating a new object. It turns the flat
+named arguments from C<new()> and puts them into the C<dsn>, C<user>, C<pass>,
+and C<attr> attributes.
 
-=item bz_explain
+=head2 on_dbi_connected
 
-=item bz_last_key
+This method is called by L<DBI> when a connection is established. It sets various connection-specific attributes
+which are nessessary for the database to function correctly. Because the database can be reconnected to 
+any required session variables must be set here.
 
-=item sql_position
+Undocumented methods: utf8_charset, utf8_collate, default_row_format'
 
-=item sql_fulltext_search
+=head2 utf8_charset
 
-=item sql_iposition
+Returns the name of the charset to use for utf8 columns.
+This comes from the C<Bugzilla-E<gt>params-E<gt>{utf8}> parameter.
+It can be either true, false, or utf8mb4
 
-=item bz_enum_initial_values
+=head2 utf8_collate
 
-=item sql_group_by
+Returns the name of the collation to use for utf8 columns.
+When C<utf8_charset> is C<utf8mb4> this is C<utf8mb4_unicode_520_ci>.
+Otherwise it is C<utf8_general_ci>.
 
-=item sql_limit
+=head2 default_row_format
 
-=item sql_not_regexp
+Returns the default row format to use for tables.
+When C<utf8_charset> is C<utf8mb4> this is C<DYNAMIC> for most tables,
+and C<COMPRESSED> for several table that benefit from compression.
 
-=item sql_string_concat
+When C<utf8_charset> is C<utf8> this is C<COMPACT> for all tables.
 
-=item sql_date_math
+=head2 sql_date_format
 
-=item sql_to_days
+Returns the SQL date format string for the current database.
 
-=item bz_check_server_version
+=head2 bz_explain
 
-=item sql_from_days
+Returns the EXPLAIN output for the given SQL statement.
 
-=item sql_regexp
+=head2 bz_last_key
 
-=item sql_istring
+Returns the last auto-increment key generated by the database.
 
-=item sql_group_concat
+=head2 sql_position
 
-=item bz_setup_database
+Returns the SQL position function for the current database.
 
-=item bz_db_is_utf8
+=head2 sql_fulltext_search
+
+Returns the SQL fulltext search function for the current database.
+
+=head2 sql_iposition
+
+Returns the SQL position function for the current database, but
+case-insensitive.
+
+=head2 bz_enum_initial_values
+
+Returns the initial values for an ENUM column.
+
+=head2 sql_group_by
+
+Returns the SQL GROUP BY clause for the current database.
+
+=head2 sql_limit
+
+Returns the SQL LIMIT clause for the current database.
+
+=head2 sql_not_regexp
+
+Returns the SQL NOT REGEXP operator for the current database.
+
+=head2 sql_string_concat
+
+Returns the SQL string concatenation operator for the current database.
+
+=head2 sql_date_math
+
+Returns the SQL date math operator for the current database.
+
+=head2 sql_to_days
+
+Returns the SQL to_days function for the current database.
+
+=head2 bz_check_server_version
+
+Returns true if the database server version is at least the given
+
+=head2 sql_from_days
+
+Returns the SQL from_days function for the current database.
+
+=head2 sql_regexp
+
+Returns the SQL REGEXP operator for the current database.
+
+=head2 sql_istring
+
+Returns the SQL string case-insensitive operator for the current database.
+
+=head2 sql_group_concat
+
+Returns the SQL GROUP_CONCAT function for the current database.
+
+=head2 bz_setup_database
+
+Sets up the database for use with Bugzilla.
+
+=head2 bz_db_is_utf8
+
+Returns true if the database is using UTF-8.
 
-=back
diff --git a/Bugzilla/DB/Oracle.pm b/Bugzilla/DB/Oracle.pm
index 930270ccc..ac2f14a63 100644
--- a/Bugzilla/DB/Oracle.pm
+++ b/Bugzilla/DB/Oracle.pm
@@ -21,11 +21,10 @@ For interface details see L<Bugzilla::DB> and L<DBI>.
 
 package Bugzilla::DB::Oracle;
 
-use 5.10.1;
-use strict;
-use warnings;
+use 5.14.0;
+use Moo;
 
-use parent qw(Bugzilla::DB);
+extends qw(Bugzilla::DB);
 
 use DBD::Oracle;
 use DBD::Oracle qw(:ora_types);
@@ -46,7 +45,7 @@ use constant BLOB_TYPE       => {ora_type => ORA_BLOB};
 use constant MIN_LONG_READ_LEN => 32 * 1024;
 use constant FULLTEXT_OR       => ' OR ';
 
-sub new {
+sub BUILDARGS {
   my ($class, $params) = @_;
   my ($user, $pass, $host, $dbname, $port)
     = @$params{qw(db_user db_pass db_host db_name db_port)};
@@ -66,24 +65,21 @@ sub new {
     LongReadLen =>
       max(Bugzilla->params->{'maxattachmentsize'} || 0, MIN_LONG_READ_LEN) * 1024,
   };
-  my $self = $class->db_new(
-    {dsn => $dsn, user => $user, pass => $pass, attrs => $attrs});
-
-  # Needed by TheSchwartz
-  $self->{private_bz_dsn} = $dsn;
+  return {dsn => $dsn, user => $user, pass => $pass, attrs => $attrs};
+}
 
-  bless($self, $class);
+sub on_dbi_connected {
+  my ($class, $dbh) = @_;
 
   # Set the session's default date format to match MySQL
-  $self->do("ALTER SESSION SET NLS_DATE_FORMAT='YYYY-MM-DD HH24:MI:SS'");
-  $self->do("ALTER SESSION SET NLS_TIMESTAMP_FORMAT='YYYY-MM-DD HH24:MI:SS'");
-  $self->do("ALTER SESSION SET NLS_LENGTH_SEMANTICS='CHAR'")
+  $dbh->do("ALTER SESSION SET NLS_DATE_FORMAT='YYYY-MM-DD HH24:MI:SS'");
+  $dbh->do("ALTER SESSION SET NLS_TIMESTAMP_FORMAT='YYYY-MM-DD HH24:MI:SS'");
+  $dbh->do("ALTER SESSION SET NLS_LENGTH_SEMANTICS='CHAR'")
     if Bugzilla->params->{'utf8'};
 
   # To allow case insensitive query.
-  $self->do("ALTER SESSION SET NLS_COMP='ANSI'");
-  $self->do("ALTER SESSION SET NLS_SORT='BINARY_AI'");
-  return $self;
+  $dbh->do("ALTER SESSION SET NLS_COMP='ANSI'");
+  $dbh->do("ALTER SESSION SET NLS_SORT='BINARY_AI'");
 }
 
 sub bz_last_key {
@@ -748,11 +744,11 @@ sub _get_create_trigger_ddl {
 
 package Bugzilla::DB::Oracle::st;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent -norequire, qw(DBI::st);
+use base -norequire, qw(DBI::st);
 
 sub fetchrow_arrayref {
   my $self = shift;
diff --git a/Bugzilla/DB/Pg.pm b/Bugzilla/DB/Pg.pm
index 15a268381..319b7b6c4 100644
--- a/Bugzilla/DB/Pg.pm
+++ b/Bugzilla/DB/Pg.pm
@@ -21,20 +21,19 @@ For interface details see L<Bugzilla::DB> and L<DBI>.
 
 package Bugzilla::DB::Pg;
 
-use 5.10.1;
-use strict;
-use warnings;
+use 5.14.0;
+use Moo;
 
 use Bugzilla::Error;
 use Bugzilla::Version;
 use DBD::Pg;
 
 # This module extends the DB interface via inheritance
-use parent qw(Bugzilla::DB);
+extends qw(Bugzilla::DB);
 
 use constant BLOB_TYPE => {pg_type => DBD::Pg::PG_BYTEA};
 
-sub new {
+sub BUILDARGS {
   my ($class, $params) = @_;
   my ($user, $pass, $host, $dbname, $port)
     = @$params{qw(db_user db_pass db_host db_name db_port)};
@@ -55,19 +54,7 @@ sub new {
 
   my $attrs = {pg_enable_utf8 => Bugzilla->params->{'utf8'}};
 
-  my $self = $class->db_new(
-    {dsn => $dsn, user => $user, pass => $pass, attrs => $attrs});
-
-  # all class local variables stored in DBI derived class needs to have
-  # a prefix 'private_'. See DBI documentation.
-  $self->{private_bz_tables_locked} = "";
-
-  # Needed by TheSchwartz
-  $self->{private_bz_dsn} = $dsn;
-
-  bless($self, $class);
-
-  return $self;
+  return {dsn => $dsn, user => $user, pass => $pass, attrs => $attrs};
 }
 
 # if last_insert_id is supported on PostgreSQL by lowest DBI/DBD version
diff --git a/Bugzilla/DB/QuoteIdentifier.pm b/Bugzilla/DB/QuoteIdentifier.pm
new file mode 100644
index 000000000..40baab682
--- /dev/null
+++ b/Bugzilla/DB/QuoteIdentifier.pm
@@ -0,0 +1,110 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::DB::QuoteIdentifier;
+
+use 5.14.0;
+use Moo;
+
+has 'db' => (
+  is       => 'ro',
+  weak_ref => 1,
+  required => 1,
+);
+
+sub TIEHASH {
+  my ($class, @args) = @_;
+
+  return $class->new(@args);
+}
+
+sub FETCH {
+  my ($self, $key) = @_;
+
+  return $self->db->quote_identifier($key);
+}
+
+sub FIRSTKEY {
+  return;
+}
+
+sub FIRSTVALUE {
+  return;
+}
+
+sub EXISTS {
+  return 1
+}
+
+sub DELETE {
+  return 1
+}
+
+1;
+
+__END__
+
+=head1 NAME
+
+Bugzilla::DB::QuoteIdentifier
+
+=head1 SYNOPSIS
+
+  my %q;
+  tie %q, 'Bugzilla::DB::QuoteIdentifier', db => Bugzilla->dbh;
+
+  is("this is $q{something}", 'this is ' . Bugzilla->dbh->quote_identifier('something'));
+
+=head1 DESCRIPTION
+
+Bugzilla has many strings with bare sql column names or table names. Sometimes,
+as in the case of MySQL 8, formerly unreserved keywords can become reserved.
+
+This module provides a shortcut for quoting identifiers in strings by way of overloading a hash
+so that we can easily call C<quote_identifier> inside double-quoted strings.
+
+=head1 METHODS
+
+=head2 TIEHASH
+
+This class can be used as a tied hash, which is only done to allow quoting identifiers inside double-quoted strings.
+
+Exmaple:
+
+    my $qi = Bugzilla->dbh->qi;
+    my $sql = "SELECT $qi->{bug_id} FROM $qi->{bugs}";
+
+=head2 FETCH
+
+Returns the quoted identifier for the given key, this just calls C<quote_identifier> on the database handle.
+
+=head2 FIRSTKEY
+
+This returns nothing, as this tied hash has no keys or values.
+
+=head2 FIRSTVALUE
+
+This returns nothing, as this tied hash has no keys or values.
+
+=head2 EXISTS
+
+This always returns true, as this tied hash has no keys or values but technically every key exists.
+
+=head2 DELETE
+
+This always returns true, as this tied hash has no keys or values but technically every key can be deleted.
+
+=head2 db
+
+This is a weak reference to the database handle that is used to quote identifiers.
+
+=head1 SEE ALSO
+
+L<Bugzilla::DB::Schema>
+
+=cut
+
diff --git a/Bugzilla/DB/Schema.pm b/Bugzilla/DB/Schema.pm
index 94b8734f3..a88793d94 100644
--- a/Bugzilla/DB/Schema.pm
+++ b/Bugzilla/DB/Schema.pm
@@ -15,9 +15,8 @@ package Bugzilla::DB::Schema;
 #
 ###########################################################################
 
-use 5.10.1;
-use strict;
-use warnings;
+use 5.14.0;
+use Moo;
 
 use Bugzilla::Error;
 use Bugzilla::Hook;
@@ -221,7 +220,7 @@ use constant FIELD_TABLE_SCHEMA => {
   # to these index names.
   INDEXES => [
     value_idx               => {FIELDS => ['value'], TYPE => 'UNIQUE'},
-    sortkey_idx             => ['sortkey',           'value'],
+    sortkey_idx             => ['sortkey', 'value'],
     visibility_value_id_idx => ['visibility_value_id'],
   ],
 };
@@ -663,15 +662,15 @@ use constant ABSTRACT_SCHEMA => {
   flagtypes => {
     FIELDS => [
       id               => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1},
-      name             => {TYPE => 'varchar(50)', NOTNULL => 1},
-      description      => {TYPE => 'MEDIUMTEXT',  NOTNULL => 1},
+      name             => {TYPE => 'varchar(50)',  NOTNULL => 1},
+      description      => {TYPE => 'MEDIUMTEXT',   NOTNULL => 1},
       cc_list          => {TYPE => 'varchar(200)'},
-      target_type      => {TYPE => 'char(1)',     NOTNULL => 1, DEFAULT => "'b'"},
-      is_active        => {TYPE => 'BOOLEAN',     NOTNULL => 1, DEFAULT => 'TRUE'},
-      is_requestable   => {TYPE => 'BOOLEAN',     NOTNULL => 1, DEFAULT => 'FALSE'},
-      is_requesteeble  => {TYPE => 'BOOLEAN',     NOTNULL => 1, DEFAULT => 'FALSE'},
-      is_multiplicable => {TYPE => 'BOOLEAN',     NOTNULL => 1, DEFAULT => 'FALSE'},
-      sortkey          => {TYPE => 'INT2',        NOTNULL => 1, DEFAULT => '0'},
+      target_type      => {TYPE => 'char(1)',      NOTNULL => 1, DEFAULT => "'b'"},
+      is_active        => {TYPE => 'BOOLEAN',      NOTNULL => 1, DEFAULT => 'TRUE'},
+      is_requestable   => {TYPE => 'BOOLEAN',      NOTNULL => 1, DEFAULT => 'FALSE'},
+      is_requesteeble  => {TYPE => 'BOOLEAN',      NOTNULL => 1, DEFAULT => 'FALSE'},
+      is_multiplicable => {TYPE => 'BOOLEAN',      NOTNULL => 1, DEFAULT => 'FALSE'},
+      sortkey          => {TYPE => 'INT2',         NOTNULL => 1, DEFAULT => '0'},
       grant_group_id   => {
         TYPE       => 'INT3',
         REFERENCES => {TABLE => 'groups', COLUMN => 'id', DELETE => 'SET NULL'}
@@ -827,7 +826,7 @@ use constant ABSTRACT_SCHEMA => {
     ],
     INDEXES => [
       bug_status_value_idx               => {FIELDS => ['value'], TYPE => 'UNIQUE'},
-      bug_status_sortkey_idx             => ['sortkey',           'value'],
+      bug_status_sortkey_idx             => ['sortkey', 'value'],
       bug_status_visibility_value_id_idx => ['visibility_value_id'],
     ],
   },
@@ -836,7 +835,7 @@ use constant ABSTRACT_SCHEMA => {
     FIELDS  => dclone(FIELD_TABLE_SCHEMA->{FIELDS}),
     INDEXES => [
       resolution_value_idx               => {FIELDS => ['value'], TYPE => 'UNIQUE'},
-      resolution_sortkey_idx             => ['sortkey',           'value'],
+      resolution_sortkey_idx             => ['sortkey', 'value'],
       resolution_visibility_value_id_idx => ['visibility_value_id'],
     ],
   },
@@ -845,7 +844,7 @@ use constant ABSTRACT_SCHEMA => {
     FIELDS  => dclone(FIELD_TABLE_SCHEMA->{FIELDS}),
     INDEXES => [
       bug_severity_value_idx               => {FIELDS => ['value'], TYPE => 'UNIQUE'},
-      bug_severity_sortkey_idx             => ['sortkey',           'value'],
+      bug_severity_sortkey_idx             => ['sortkey', 'value'],
       bug_severity_visibility_value_id_idx => ['visibility_value_id'],
     ],
   },
@@ -854,7 +853,7 @@ use constant ABSTRACT_SCHEMA => {
     FIELDS  => dclone(FIELD_TABLE_SCHEMA->{FIELDS}),
     INDEXES => [
       priority_value_idx               => {FIELDS => ['value'], TYPE => 'UNIQUE'},
-      priority_sortkey_idx             => ['sortkey',           'value'],
+      priority_sortkey_idx             => ['sortkey', 'value'],
       priority_visibility_value_id_idx => ['visibility_value_id'],
     ],
   },
@@ -863,7 +862,7 @@ use constant ABSTRACT_SCHEMA => {
     FIELDS  => dclone(FIELD_TABLE_SCHEMA->{FIELDS}),
     INDEXES => [
       rep_platform_value_idx               => {FIELDS => ['value'], TYPE => 'UNIQUE'},
-      rep_platform_sortkey_idx             => ['sortkey',           'value'],
+      rep_platform_sortkey_idx             => ['sortkey', 'value'],
       rep_platform_visibility_value_id_idx => ['visibility_value_id'],
     ],
   },
@@ -872,7 +871,7 @@ use constant ABSTRACT_SCHEMA => {
     FIELDS  => dclone(FIELD_TABLE_SCHEMA->{FIELDS}),
     INDEXES => [
       op_sys_value_idx               => {FIELDS => ['value'], TYPE => 'UNIQUE'},
-      op_sys_sortkey_idx             => ['sortkey',           'value'],
+      op_sys_sortkey_idx             => ['sortkey', 'value'],
       op_sys_visibility_value_id_idx => ['visibility_value_id'],
     ],
   },
@@ -1748,49 +1747,72 @@ DB-specific code in a subclass. Methods which are prefixed with C<_>
 are considered protected. Subclasses may override these methods, but
 other modules should not invoke these methods directly.
 
+=over 4
+
 =cut
 
 #--------------------------------------------------------------------------
-sub new {
+sub BUILD {
 
-=over
+=item C<BUILD>
 
-=item C<new>
-
- Description: Public constructor method used to instantiate objects of this
-              class. However, it also can be used as a factory method to
-              instantiate database-specific subclasses when an optional
-              driver argument is supplied.
- Parameters:  $driver (optional) - Used to specify the type of database.
-              This routine C<die>s if no subclass is found for the specified
-              driver.
-              $schema (optional) - A reference to a hash. Callers external
-                  to this package should never use this parameter.
- Returns:     new instance of the Schema class or a database-specific subclass
+This method exists to bridge between the old way of doing things (calling C<_initialize>)
+and the Moo way of doing things. Moo will call this method when you call C<new> on a class,
+and it will throw an error if called on the base class. This is because the base class is abstract.
 
 =cut
 
-  my $this   = shift;
-  my $class  = ref($this) || $this;
-  my $driver = shift;
+  my $self   = shift;
+  my $class  = ref($self) || $self;
 
-  if ($driver) {
-    (my $subclass = $driver) =~ s/^(\S)/\U$1/;
-    $class .= '::' . $subclass;
-    eval "require $class;";
-    die "The $class class could not be found ($subclass " . "not supported?): $@"
-      if ($@);
-  }
   die "$class is an abstract base class. Instantiate a subclass instead."
     if ($class eq __PACKAGE__);
 
-  my $self = {};
-  bless $self, $class;
-  $self = $self->_initialize(@_);
+  $self->_initialize();
+}    #eosub--BUILD
+
+# we declare attributes below, even though we access their slots directly.
+# This is because this code is evolving from the pre-Moo days of perl OO.
+
+# the init_arg begins with an underscore as this should only be passed in internally.
+# This should be a 'lazy' attribute, but to maintain the smallest diff we're
+# instead setting it in _initialize() if it isn't already passed in.
+has 'abstract_schema' => ( init_arg => '_abstract_schema', is => 'rw' );
+
+=item C<abstract_schema>
+
+Returns the abstract schema for this database. It is initialized in C<_initialize>.
 
-  return ($self);
+=cut
+
+# this could also be lazy, but it is also set in _initialize()
+has 'schema' => (init_arg =>undef, is => 'rw');
+
+=item C<schema>
 
-}    #eosub--new
+Returns the schema for this database. It is initialized in C<_initialize>
+and cannot be passed in to C<new>.
+
+=cut
+
+has 'db_specific' => (init_arg => undef, is => 'rw');
+
+=item C<db_specific>
+
+Returns the DB-specific schema for this database. It is initialized in C<_initialize>
+and cannot be passed in to C<new>.
+
+=cut
+
+has 'db' => (is => 'ro', weak_ref => 1, required => 1);
+
+=item C<db>
+
+Returns the L<Bugzilla::DB> object that this schema is associated with.
+This is a weak reference, so it will be C<undef> if the L<Bugzilla::DB>
+object has been destroyed.
+
+=cut
 
 #--------------------------------------------------------------------------
 sub _initialize {
@@ -1805,17 +1827,12 @@ sub _initialize {
               define the database-specific implementation of the all
               abstract data types), and then call the C<_adjust_schema>
               method.
- Parameters:  $abstract_schema (optional) - A reference to a hash. If 
-                  provided, this hash will be used as the internal
-                  representation of the abstract schema instead of our
-                  default abstract schema. This is intended for internal 
-                  use only by deserialize_abstract.
  Returns:     the instance of the Schema class
 
 =cut
 
   my $self            = shift;
-  my $abstract_schema = shift;
+  my $abstract_schema = $self->abstract_schema;
 
   if (!$abstract_schema) {
 
@@ -1997,7 +2014,9 @@ is undefined.
 
   return
       "\n     CONSTRAINT $fk_name FOREIGN KEY ($column)\n"
-    . "     REFERENCES $to_table($to_column)\n"
+    . "     REFERENCES "
+    . Bugzilla->dbh->quote_identifier($to_table)
+    . "($to_column)\n"
     . "      ON UPDATE $update ON DELETE $delete";
 }
 
@@ -2032,13 +2051,18 @@ sub get_add_fks_sql {
   my @add = $self->_column_fks_to_ddl($table, $column_fks);
 
   my @sql;
+  my $dbh = Bugzilla->dbh;
   if ($self->MULTIPLE_FKS_IN_ALTER) {
-    my $alter = "ALTER TABLE $table ADD " . join(', ADD ', @add);
+    my $alter
+      = "ALTER TABLE "
+      . $dbh->quote_identifier($table) . " ADD "
+      . join(', ADD ', @add);
     push(@sql, $alter);
   }
   else {
     foreach my $fk_string (@add) {
-      push(@sql, "ALTER TABLE $table ADD $fk_string");
+      push(@sql,
+        "ALTER TABLE " . $dbh->quote_identifier($table) . " ADD $fk_string");
     }
   }
   return @sql;
@@ -2059,7 +2083,8 @@ sub get_drop_fk_sql {
   my ($self, $table, $column, $references) = @_;
   my $fk_name = $self->_get_fk_name($table, $column, $references);
 
-  return ("ALTER TABLE $table DROP CONSTRAINT $fk_name");
+  return (
+    "ALTER TABLE " . Bugzilla->dbh->quote_identifier($table) . " DROP CONSTRAINT $fk_name");
 }
 
 sub convert_type {
@@ -2224,7 +2249,9 @@ sub _get_create_table_ddl {
   }
 
   my $sql
-    = "CREATE TABLE $table (\n" . join(",\n", @col_lines, @fk_lines) . "\n)";
+    = "CREATE TABLE "
+    . Bugzilla->dbh->quote_identifier($table) . " (\n"
+    . join(",\n", @col_lines, @fk_lines) . "\n)";
   return $sql;
 
 }
@@ -2248,7 +2275,9 @@ sub _get_create_index_ddl {
   my $sql = "CREATE ";
   $sql .= "$index_type " if ($index_type && $index_type eq 'UNIQUE');
   $sql
-    .= "INDEX $index_name ON $table_name \(" . join(", ", @$index_fields) . "\)";
+    .= "INDEX $index_name ON "
+    . Bugzilla->dbh->quote_identifier($table_name) . ' ('
+    . join(', ', @$index_fields) . ')';
 
   return ($sql);
 
@@ -2274,16 +2303,20 @@ sub get_add_column_ddl {
 
   my ($self, $table, $column, $definition, $init_value) = @_;
   my @statements;
+  my $dbh = Bugzilla->dbh;
   push(@statements,
-        "ALTER TABLE $table "
+        'ALTER TABLE '
+      . $dbh->quote_identifier($table) . ' '
       . $self->ADD_COLUMN
       . " $column "
       . $self->get_type_ddl($definition));
 
   # XXX - Note that although this works for MySQL, most databases will fail
   # before this point, if we haven't set a default.
-  (push(@statements, "UPDATE $table SET $column = $init_value"))
-    if defined $init_value;
+  (
+    push(@statements,
+      'UPDATE ' . $dbh->quote_identifier($table) . " SET $column = $init_value")
+  ) if defined $init_value;
 
   if (defined $definition->{REFERENCES}) {
     push(@statements,
@@ -2350,6 +2383,7 @@ sub get_alter_column_ddl {
 
   my $self = shift;
   my ($table, $column, $new_def, $set_nulls_to) = @_;
+  my $dbh = Bugzilla->dbh;
 
   my @statements;
   my $old_def  = $self->get_column_abstract($table, $column);
@@ -2376,7 +2410,10 @@ sub get_alter_column_ddl {
 
   # If we went from having a default to not having one
   elsif (!defined $default && defined $default_old) {
-    push(@statements, "ALTER TABLE $table ALTER COLUMN $column" . " DROP DEFAULT");
+    push(@statements,
+          "ALTER TABLE "
+        . $dbh->quote_identifier($table)
+        . " ALTER COLUMN $column DROP DEFAULT");
   }
 
   # If we went from no default to a default, or we changed the default.
@@ -2384,28 +2421,40 @@ sub get_alter_column_ddl {
     || ($default ne $default_old))
   {
     push(@statements,
-      "ALTER TABLE $table ALTER COLUMN $column " . " SET DEFAULT $default");
+          "ALTER TABLE "
+        . $dbh->quote_identifier($table)
+        . " ALTER COLUMN $column SET DEFAULT $default");
   }
 
   # If we went from NULL to NOT NULL.
   if (!$old_def->{NOTNULL} && $new_def->{NOTNULL}) {
     push(@statements, $self->_set_nulls_sql(@_));
-    push(@statements, "ALTER TABLE $table ALTER COLUMN $column" . " SET NOT NULL");
+    push(@statements,
+          "ALTER TABLE "
+        . $dbh->quote_identifier($table)
+        . " ALTER COLUMN $column SET NOT NULL");
   }
 
   # If we went from NOT NULL to NULL
   elsif ($old_def->{NOTNULL} && !$new_def->{NOTNULL}) {
-    push(@statements, "ALTER TABLE $table ALTER COLUMN $column" . " DROP NOT NULL");
+    push(@statements,
+          "ALTER TABLE "
+        . $dbh->quote_identifier($table)
+        . " ALTER COLUMN $column DROP NOT NULL");
   }
 
   # If we went from not being a PRIMARY KEY to being a PRIMARY KEY.
   if (!$old_def->{PRIMARYKEY} && $new_def->{PRIMARYKEY}) {
-    push(@statements, "ALTER TABLE $table ADD PRIMARY KEY ($column)");
+    push(@statements,
+          "ALTER TABLE "
+        . $dbh->quote_identifier($table)
+        . " ADD PRIMARY KEY ($column)");
   }
 
   # If we went from being a PK to not being a PK
   elsif ($old_def->{PRIMARYKEY} && !$new_def->{PRIMARYKEY}) {
-    push(@statements, "ALTER TABLE $table DROP PRIMARY KEY");
+    push(@statements,
+      "ALTER TABLE " . $dbh->quote_identifier($table) . " DROP PRIMARY KEY");
   }
 
   return @statements;
@@ -2427,7 +2476,10 @@ sub _set_nulls_sql {
   }
   my @sql;
   if (defined $default) {
-    push(@sql, "UPDATE $table SET $column = $default" . "  WHERE $column IS NULL");
+    push(@sql,
+          "UPDATE "
+        . Bugzilla->dbh->quote_identifier($table)
+        . " SET $column = $default WHERE $column IS NULL");
   }
   return @sql;
 }
@@ -2462,7 +2514,9 @@ sub get_drop_column_ddl {
 =cut
 
   my ($self, $table, $column) = @_;
-  return ("ALTER TABLE $table DROP COLUMN $column");
+  return ("ALTER TABLE "
+      . Bugzilla->dbh->quote_identifier($table)
+      . " DROP COLUMN $column");
 }
 
 =item C<get_drop_table_ddl($table)>
@@ -2475,7 +2529,7 @@ sub get_drop_column_ddl {
 
 sub get_drop_table_ddl {
   my ($self, $table) = @_;
-  return ("DROP TABLE $table");
+  return ('DROP TABLE ' . Bugzilla->dbh->quote_identifier($table));
 }
 
 sub get_rename_column_ddl {
@@ -2525,7 +2579,11 @@ Gets SQL to rename a table in the database.
 =cut
 
   my ($self, $old_name, $new_name) = @_;
-  return ("ALTER TABLE $old_name RENAME TO $new_name");
+  my $dbh = Bugzilla->dbh;
+  return ('ALTER TABLE '
+      . $dbh->quote_identifier($old_name)
+      . ' RENAME TO '
+      . $dbh->quote_identifier($new_name));
 }
 
 =item C<delete_table($name)>
@@ -2956,7 +3014,7 @@ sub serialize_abstract {
 =cut
 
 sub deserialize_abstract {
-  my ($class, $serialized, $version) = @_;
+  my ($self, $serialized, $version) = @_;
 
   my $thawed_hash;
   if ($version < 2) {
@@ -2970,7 +3028,7 @@ sub deserialize_abstract {
 
   # Version 2 didn't have the "created" key for REFERENCES items.
   if ($version < 3) {
-    my $standard = $class->new()->{abstract_schema};
+    my $standard = $self->new(db => $self->db)->{abstract_schema};
     foreach my $table_name (keys %$thawed_hash) {
       my %standard_fields = @{$standard->{$table_name}->{FIELDS} || []};
       my $table           = $thawed_hash->{$table_name};
@@ -2983,7 +3041,7 @@ sub deserialize_abstract {
     }
   }
 
-  return $class->new(undef, $thawed_hash);
+  return $self->new(db => $self->db, _abstract_schema => $thawed_hash);
 }
 
 #####################################################################
@@ -3011,8 +3069,8 @@ object.
 =cut
 
 sub get_empty_schema {
-  my ($class) = @_;
-  return $class->deserialize_abstract(Dumper({}), SCHEMA_VERSION);
+  my ($self) = @_;
+  return $self->deserialize_abstract(Dumper({}), SCHEMA_VERSION);
 }
 
 1;
diff --git a/Bugzilla/DB/Schema/MariaDB.pm b/Bugzilla/DB/Schema/MariaDB.pm
new file mode 100644
index 000000000..6438aea45
--- /dev/null
+++ b/Bugzilla/DB/Schema/MariaDB.pm
@@ -0,0 +1,459 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::DB::Schema::MariaDB;
+
+###############################################################################
+#
+# DB::Schema implementation for MariaDB
+#
+###############################################################################
+
+use 5.14.0;
+use Moo;
+
+use Bugzilla::Error;
+
+extends qw(Bugzilla::DB::Schema);
+
+# This is for column_info_to_column, to know when a tinyint is a
+# boolean and when it's really a tinyint. This only has to be accurate
+# up to and through 2.19.3, because that's the only time we need
+# column_info_to_column.
+#
+# This is basically a hash of tables/columns, with one entry for each column
+# that should be interpreted as a BOOLEAN instead of as an INT1 when
+# reading in the Schema from the disk. The values are discarded; I just
+# used "1" for simplicity.
+#
+# THIS CONSTANT IS ONLY USED FOR UPGRADES FROM 2.18 OR EARLIER. DON'T
+# UPDATE IT TO MODERN COLUMN NAMES OR DEFINITIONS.
+use constant BOOLEAN_MAP => {
+  bugs => {
+    everconfirmed        => 1,
+    reporter_accessible  => 1,
+    cclist_accessible    => 1,
+    qacontact_accessible => 1,
+    assignee_accessible  => 1
+  },
+  longdescs   => {isprivate => 1, already_wrapped => 1},
+  attachments => {ispatch   => 1, isobsolete      => 1, isprivate => 1},
+  flags     => {is_active => 1},
+  flagtypes => {
+    is_active        => 1,
+    is_requestable   => 1,
+    is_requesteeble  => 1,
+    is_multiplicable => 1
+  },
+  fielddefs    => {mailhead     => 1, obsolete => 1},
+  bug_status   => {isactive     => 1},
+  resolution   => {isactive     => 1},
+  bug_severity => {isactive     => 1},
+  priority     => {isactive     => 1},
+  rep_platform => {isactive     => 1},
+  op_sys       => {isactive     => 1},
+  profiles     => {mybugslink   => 1, newemailtech => 1},
+  namedqueries => {linkinfooter => 1, watchfordiffs => 1},
+  groups       => {isbuggroup   => 1, isactive => 1},
+  group_control_map =>
+    {entry => 1, membercontrol => 1, othercontrol => 1, canedit => 1},
+  group_group_map => {isbless       => 1},
+  user_group_map  => {isbless       => 1, isderived => 1},
+  products        => {disallownew   => 1},
+  series          => {public        => 1},
+  whine_queries   => {onemailperbug => 1},
+  quips           => {approved      => 1},
+  setting         => {is_enabled    => 1}
+};
+
+# Maps the db_specific hash backwards, for use in column_info_to_column.
+use constant REVERSE_MAPPING => {
+
+  # Boolean and the SERIAL fields are handled in column_info_to_column,
+  # and so don't have an entry here.
+  TINYINT   => 'INT1',
+  SMALLINT  => 'INT2',
+  MEDIUMINT => 'INT3',
+  INTEGER   => 'INT4',
+
+  # All the other types have the same name in their abstract version
+  # as in their db-specific version, so no reverse mapping is needed.
+};
+
+#------------------------------------------------------------------------------
+sub _initialize {
+
+  my $self = shift;
+
+  $self = $self->SUPER::_initialize();
+
+  $self->{db_specific} = {
+
+    BOOLEAN => 'tinyint',
+    FALSE   => '0',
+    TRUE    => '1',
+
+    INT1 => 'tinyint',
+    INT2 => 'smallint',
+    INT3 => 'mediumint',
+    INT4 => 'integer',
+
+    SMALLSERIAL  => 'smallint auto_increment',
+    MEDIUMSERIAL => 'mediumint auto_increment',
+    INTSERIAL    => 'integer auto_increment',
+
+    TINYTEXT   => 'tinytext',
+    MEDIUMTEXT => 'mediumtext',
+    LONGTEXT   => 'mediumtext',
+
+    LONGBLOB => 'longblob',
+
+    DATETIME => 'datetime',
+    DATE     => 'date',
+  };
+
+  $self->_adjust_schema;
+
+  return $self;
+
+}    #eosub--_initialize
+
+#------------------------------------------------------------------------------
+sub _get_create_table_ddl {
+
+  # Returns a "create table" SQL statement.
+  my ($self, $table) = @_;
+  my $charset    = Bugzilla::DB::MariaDB->utf8_charset;
+  my $collate    = Bugzilla::DB::MariaDB->utf8_collate;
+  my $row_format = Bugzilla::DB::MariaDB->default_row_format($table);
+  my @parts      = (
+    $self->SUPER::_get_create_table_ddl($table), 'ENGINE = InnoDB',
+    "CHARACTER SET $charset COLLATE $collate",   "ROW_FORMAT=$row_format",
+  );
+  return join(' ', @parts);
+}    #eosub--_get_create_table_ddl
+
+#------------------------------------------------------------------------------
+sub _get_create_index_ddl {
+
+  # Extend superclass method to create FULLTEXT indexes on text fields.
+  # Returns a "create index" SQL statement.
+
+  my ($self, $table_name, $index_name, $index_fields, $index_type) = @_;
+  my $dbh = Bugzilla->dbh;
+
+  my $sql = "CREATE ";
+  $sql .= "$index_type "
+    if ($index_type eq 'UNIQUE' || $index_type eq 'FULLTEXT');
+  $sql
+    .= "INDEX "
+    . $dbh->quote_identifier($index_name) . " ON "
+    . $dbh->quote_identifier($table_name) . " \("
+    . join(", ", @$index_fields) . "\)";
+
+  return ($sql);
+
+}    #eosub--_get_create_index_ddl
+
+#--------------------------------------------------------------------
+
+sub get_create_database_sql {
+  my ($self, $name) = @_;
+
+  # We only create as utf8 if we have no params (meaning we're doing
+  # a new installation) or if the utf8 param is on.
+  my $charset = Bugzilla::DB::MariaDB->utf8_charset;
+  my $collate = Bugzilla::DB::MariaDB->utf8_collate;
+  return ("CREATE DATABASE $name CHARACTER SET $charset COLLATE $collate");
+}
+
+# MariaDB has a simpler ALTER TABLE syntax than ANSI.
+sub get_alter_column_ddl {
+  my ($self, $table, $column, $new_def, $set_nulls_to) = @_;
+  my $old_def      = $self->get_column($table, $column);
+  my %new_def_copy = %$new_def;
+  if ($old_def->{PRIMARYKEY} && $new_def->{PRIMARYKEY}) {
+
+    # If a column stays a primary key do NOT specify PRIMARY KEY in the
+    # ALTER TABLE statement. This avoids a MariaDB error that two primary
+    # keys are not allowed.
+    delete $new_def_copy{PRIMARYKEY};
+  }
+
+  my @statements;
+
+  my $dbh = Bugzilla->dbh;
+  push(@statements,
+        "UPDATE "
+      . $dbh->quote_identifier($table)
+      . " SET $column = $set_nulls_to WHERE $column IS NULL")
+    if defined $set_nulls_to;
+
+  # Calling SET DEFAULT or DROP DEFAULT is *way* faster than calling
+  # CHANGE COLUMN, so just do that if we're just changing the default.
+  my %old_defaultless = %$old_def;
+  my %new_defaultless = %$new_def;
+  delete $old_defaultless{DEFAULT};
+  delete $new_defaultless{DEFAULT};
+  if (!$self->columns_equal($old_def, $new_def)
+    && $self->columns_equal(\%new_defaultless, \%old_defaultless))
+  {
+    if (!defined $new_def->{DEFAULT}) {
+      push(@statements,
+           "ALTER TABLE "
+           . $dbh->quote_identifier($table)
+           . " ALTER COLUMN $column DROP DEFAULT");
+    }
+    else {
+      push(@statements,
+           "ALTER TABLE "
+           . $dbh->quote_identifier($table)
+           . " ALTER COLUMN $column SET DEFAULT "
+           . $new_def->{DEFAULT});
+    }
+  }
+  else {
+    my $new_ddl = $self->get_type_ddl(\%new_def_copy);
+    push(@statements,
+         "ALTER TABLE "
+         . $dbh->quote_identifier($table)
+         . " CHANGE COLUMN $column $column $new_ddl");
+  }
+
+  if ($old_def->{PRIMARYKEY} && !$new_def->{PRIMARYKEY}) {
+
+    # Dropping a PRIMARY KEY needs an explicit DROP PRIMARY KEY
+    push(@statements,
+         'ALTER TABLE ' . $dbh->quote_identifier($table) . ' DROP PRIMARY KEY');
+  }
+
+  return @statements;
+}
+
+sub get_drop_fk_sql {
+  my ($self, $table, $column, $references) = @_;
+  my $fk_name = $self->_get_fk_name($table, $column, $references);
+  my $dbh     = Bugzilla->dbh;
+  my @sql     = (
+    "ALTER TABLE " . $dbh->quote_identifier($table) . " DROP FOREIGN KEY $fk_name");
+
+  # MySQL requires, and will create, an index on any column with
+  # an FK. It will name it after the fk, which we never do.
+  # So if there's an index named after the fk, we also have to delete it.
+  if ($dbh->bz_index_info_real($table, $fk_name)) {
+    push(@sql, $self->get_drop_index_ddl($table, $fk_name));
+  }
+
+  return @sql;
+}
+
+sub get_drop_index_ddl {
+  my ($self, $table, $name) = @_;
+  return ("DROP INDEX \`$name\` ON $table");
+}
+
+# A special function for MySQL, for renaming a lot of indexes.
+# Index renames is a hash, where the key is a string - the
+# old names of the index, and the value is a hash - the index
+# definition that we're renaming to, with an extra key of "NAME"
+# that contains the new index name.
+# The indexes in %indexes must be in hashref format.
+sub get_rename_indexes_ddl {
+  my ($self, $table, %indexes) = @_;
+  my @keys = keys %indexes or return ();
+
+  my $sql = 'ALTER TABLE' . Bugzilla->dbh->quote_identifier($table) . ' ';
+
+  foreach my $old_name (@keys) {
+    my $name = $indexes{$old_name}->{NAME};
+    my $type = $indexes{$old_name}->{TYPE};
+    $type ||= 'INDEX';
+    my $fields = join(',', @{$indexes{$old_name}->{FIELDS}});
+
+    # $old_name needs to be escaped, sometimes, because it was
+    # a reserved word.
+    $old_name = '`' . $old_name . '`';
+    $sql .= " ADD $type $name ($fields), DROP INDEX $old_name,";
+  }
+
+  # Remove the last comma.
+  chop($sql);
+  return ($sql);
+}
+
+sub get_set_serial_sql {
+  my ($self, $table, $column, $value) = @_;
+  return ("ALTER TABLE "
+      . Bugzilla->dbh->quote_identifier($table)
+      . " AUTO_INCREMENT = $value");
+}
+
+# Converts a DBI column_info output to an abstract column definition.
+# Expects to only be called by Bugzila::DB::MariaDB::_bz_build_schema_from_disk,
+# although there's a chance that it will also work properly if called
+# elsewhere.
+sub column_info_to_column {
+  my ($self, $column_info) = @_;
+
+  # Unfortunately, we have to break Schema's normal "no database"
+  # barrier a few times in this function.
+  my $dbh = Bugzilla->dbh;
+
+  my $table    = $column_info->{TABLE_NAME};
+  my $col_name = $column_info->{COLUMN_NAME};
+
+  my $column = {};
+
+  ($column->{NOTNULL} = 1) if $column_info->{NULLABLE} == 0;
+
+  if ($column_info->{mariadb_is_pri_key}) {
+
+    # In MySQL, if a table has no PK, but it has a UNIQUE index,
+    # that index will show up as the PK. So we have to eliminate
+    # that possibility.
+    # Unfortunately, the only way to definitely solve this is
+    # to break Schema's standard of not touching the live database
+    # and check if the index called PRIMARY is on that field.
+    my $pri_index = $dbh->bz_index_info_real($table, 'PRIMARY');
+    if ($pri_index && grep($_ eq $col_name, @{$pri_index->{FIELDS}})) {
+      $column->{PRIMARYKEY} = 1;
+    }
+  }
+
+  # MySQL frequently defines a default for a field even when we
+  # didn't explicitly set one. So we have to have some special
+  # hacks to determine whether or not we should actually put
+  # a default in the abstract schema for this field.
+  if (defined $column_info->{COLUMN_DEF}) {
+
+    # The defaults that MySQL inputs automatically are usually
+    # something that would be considered "false" by perl, either
+    # a 0 or an empty string. (Except for datetime and decimal
+    # fields, which have their own special auto-defaults.)
+    #
+    # Here's how we handle this: If it exists in the schema
+    # without a default, then we don't use the default. If it
+    # doesn't exist in the schema, then we're either going to
+    # be dropping it soon, or it's a custom end-user column, in which
+    # case having a bogus default won't harm anything.
+    my $schema_column = $self->get_column($table, $col_name);
+    unless (
+      (
+          !$column_info->{COLUMN_DEF}
+        || $column_info->{COLUMN_DEF} eq '0000-00-00 00:00:00'
+        || $column_info->{COLUMN_DEF} eq '0.00'
+      )
+      && $schema_column
+      && !exists $schema_column->{DEFAULT}
+      )
+    {
+
+      my $default = $column_info->{COLUMN_DEF};
+
+      # Schema uses '0' for the defaults for decimal fields.
+      $default = 0 if $default =~ /^0\.0+$/;
+
+      # If we're not a number, we're a string and need to be
+      # quoted.
+      $default = $dbh->quote($default) if !($default =~ /^(-)?([0-9]+)(\.[0-9]+)?$/);
+      $column->{DEFAULT} = $default;
+    }
+  }
+
+  my $type = $column_info->{TYPE_NAME};
+
+  # Certain types of columns need the size/precision appended.
+  if ($type =~ /CHAR$/ || $type eq 'DECIMAL') {
+
+    # This is nicely lowercase and has the size/precision appended.
+    $type = $column_info->{mariadb_type_name};
+  }
+
+  # If we're a tinyint, we could be either a BOOLEAN or an INT1.
+  # Only the BOOLEAN_MAP knows the difference.
+  elsif ($type eq 'TINYINT'
+    && exists BOOLEAN_MAP->{$table}
+    && exists BOOLEAN_MAP->{$table}->{$col_name})
+  {
+    $type = 'BOOLEAN';
+    if (exists $column->{DEFAULT}) {
+      $column->{DEFAULT} = $column->{DEFAULT} ? 'TRUE' : 'FALSE';
+    }
+  }
+
+  # We also need to check if we're an auto_increment field.
+  elsif ($type =~ /INT/) {
+
+    # Unfortunately, the only way to do this in DBI is to query the
+    # database, so we have to break the rule here that Schema normally
+    # doesn't touch the live DB.
+    my $ref_sth = $dbh->prepare("SELECT $col_name FROM $table LIMIT 1");
+    $ref_sth->execute;
+    if ($ref_sth->{mariadb_is_auto_increment}->[0]) {
+      if ($type eq 'MEDIUMINT') {
+        $type = 'MEDIUMSERIAL';
+      }
+      elsif ($type eq 'SMALLINT') {
+        $type = 'SMALLSERIAL';
+      }
+      else {
+        $type = 'INTSERIAL';
+      }
+    }
+    $ref_sth->finish;
+
+  }
+
+  # For all other db-specific types, check if they exist in
+  # REVERSE_MAPPING and use the type found there.
+  if (exists REVERSE_MAPPING->{$type}) {
+    $type = REVERSE_MAPPING->{$type};
+  }
+
+  $column->{TYPE} = $type;
+
+  #print "$table.$col_name: " . Data::Dumper->Dump([$column]) . "\n";
+
+  return $column;
+}
+
+sub get_rename_column_ddl {
+  my ($self, $table, $old_name, $new_name) = @_;
+  my $def = $self->get_type_ddl($self->get_column($table, $old_name));
+
+  # MySQL doesn't like having the PRIMARY KEY statement in a rename.
+  $def =~ s/PRIMARY KEY//i;
+  return ("ALTER TABLE "
+      . Bugzilla->dbh->quote_identifier($table)
+      . " CHANGE COLUMN $old_name $new_name $def");
+}
+
+1;
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item get_rename_column_ddl
+
+=item get_create_database_sql
+
+=item get_drop_index_ddl
+
+=item get_set_serial_sql
+
+=item get_rename_indexes_ddl
+
+=item get_drop_fk_sql
+
+=item MYISAM_TABLES
+
+=item column_info_to_column
+
+=item get_alter_column_ddl
+
+=back
diff --git a/Bugzilla/DB/Schema/Mysql.pm b/Bugzilla/DB/Schema/Mysql.pm
index 4c0d43523..76a648eb6 100644
--- a/Bugzilla/DB/Schema/Mysql.pm
+++ b/Bugzilla/DB/Schema/Mysql.pm
@@ -13,13 +13,12 @@ package Bugzilla::DB::Schema::Mysql;
 #
 ###############################################################################
 
-use 5.10.1;
-use strict;
-use warnings;
+use 5.14.0;
+use Moo;
 
 use Bugzilla::Error;
 
-use parent qw(Bugzilla::DB::Schema);
+extends qw(Bugzilla::DB::Schema);
 
 # This is for column_info_to_column, to know when a tinyint is a
 # boolean and when it's really a tinyint. This only has to be accurate
@@ -85,14 +84,12 @@ use constant REVERSE_MAPPING => {
   # as in their db-specific version, so no reverse mapping is needed.
 };
 
-use constant MYISAM_TABLES => qw();
-
 #------------------------------------------------------------------------------
 sub _initialize {
 
   my $self = shift;
 
-  $self = $self->SUPER::_initialize(@_);
+  $self = $self->SUPER::_initialize();
 
   $self->{db_specific} = {
 
@@ -128,20 +125,16 @@ sub _initialize {
 #------------------------------------------------------------------------------
 sub _get_create_table_ddl {
 
-  # Extend superclass method to specify the MYISAM storage engine.
   # Returns a "create table" SQL statement.
-
   my ($self, $table) = @_;
-
-  my $charset = Bugzilla->dbh->bz_db_is_utf8 ? "CHARACTER SET utf8" : '';
-  my $type = grep($_ eq $table, MYISAM_TABLES) ? 'MYISAM' : 'InnoDB';
-
-  my $ddl = $self->SUPER::_get_create_table_ddl($table);
-  $ddl =~ s/CREATE TABLE (.*) \(/CREATE TABLE `$1` (/;
-  $ddl .= " ENGINE = $type $charset";
-
-  return $ddl;
-
+  my $charset    = Bugzilla::DB::Mysql->utf8_charset;
+  my $collate    = Bugzilla::DB::Mysql->utf8_collate;
+  my $row_format = Bugzilla::DB::Mysql->default_row_format($table);
+  my @parts      = (
+    $self->SUPER::_get_create_table_ddl($table), 'ENGINE = InnoDB',
+    "CHARACTER SET $charset COLLATE $collate",   "ROW_FORMAT=$row_format",
+  );
+  return join(' ', @parts);
 }    #eosub--_get_create_table_ddl
 
 #------------------------------------------------------------------------------
@@ -151,11 +144,15 @@ sub _get_create_index_ddl {
   # Returns a "create index" SQL statement.
 
   my ($self, $table_name, $index_name, $index_fields, $index_type) = @_;
+  my $dbh = Bugzilla->dbh;
 
   my $sql = "CREATE ";
   $sql .= "$index_type "
     if ($index_type eq 'UNIQUE' || $index_type eq 'FULLTEXT');
-  $sql .= "INDEX \`$index_name\` ON \`$table_name\` \("
+  $sql
+    .= "INDEX "
+    . $dbh->quote_identifier($index_name) . " ON "
+    . $dbh->quote_identifier($table_name) . " \("
     . join(", ", @$index_fields) . "\)";
 
   return ($sql);
@@ -169,10 +166,9 @@ sub get_create_database_sql {
 
   # We only create as utf8 if we have no params (meaning we're doing
   # a new installation) or if the utf8 param is on.
-  my $create_utf8
-    = Bugzilla->params->{'utf8'} || !defined Bugzilla->params->{'utf8'};
-  my $charset = $create_utf8 ? "CHARACTER SET utf8" : '';
-  return ("CREATE DATABASE $name $charset");
+  my $charset = Bugzilla::DB::Mysql->utf8_charset;
+  my $collate = Bugzilla::DB::Mysql->utf8_collate;
+  return ("CREATE DATABASE $name CHARACTER SET $charset COLLATE $collate");
 }
 
 # MySQL has a simpler ALTER TABLE syntax than ANSI.
@@ -190,10 +186,12 @@ sub get_alter_column_ddl {
 
   my @statements;
 
-  push(
-    @statements, "UPDATE $table SET $column = $set_nulls_to
-                        WHERE $column IS NULL"
-  ) if defined $set_nulls_to;
+  my $dbh = Bugzilla->dbh;
+  push(@statements,
+        "UPDATE "
+      . $dbh->quote_identifier($table)
+      . " SET $column = $set_nulls_to WHERE $column IS NULL")
+    if defined $set_nulls_to;
 
   # Calling SET DEFAULT or DROP DEFAULT is *way* faster than calling
   # CHANGE COLUMN, so just do that if we're just changing the default.
@@ -205,27 +203,32 @@ sub get_alter_column_ddl {
     && $self->columns_equal(\%new_defaultless, \%old_defaultless))
   {
     if (!defined $new_def->{DEFAULT}) {
-      push(@statements, "ALTER TABLE $table ALTER COLUMN $column DROP DEFAULT");
+      push(@statements,
+           "ALTER TABLE "
+           . $dbh->quote_identifier($table)
+           . " ALTER COLUMN $column DROP DEFAULT");
     }
     else {
-      push(
-        @statements, "ALTER TABLE $table ALTER COLUMN $column
-                               SET DEFAULT " . $new_def->{DEFAULT}
-      );
+      push(@statements,
+           "ALTER TABLE "
+           . $dbh->quote_identifier($table)
+           . " ALTER COLUMN $column SET DEFAULT "
+           . $new_def->{DEFAULT});
     }
   }
   else {
     my $new_ddl = $self->get_type_ddl(\%new_def_copy);
-    push(
-      @statements, "ALTER TABLE $table CHANGE COLUMN 
-                       $column $column $new_ddl"
-    );
+    push(@statements,
+         "ALTER TABLE "
+         . $dbh->quote_identifier($table)
+         . " CHANGE COLUMN $column $column $new_ddl");
   }
 
   if ($old_def->{PRIMARYKEY} && !$new_def->{PRIMARYKEY}) {
 
     # Dropping a PRIMARY KEY needs an explicit DROP PRIMARY KEY
-    push(@statements, "ALTER TABLE $table DROP PRIMARY KEY");
+    push(@statements,
+         'ALTER TABLE ' . $dbh->quote_identifier($table) . ' DROP PRIMARY KEY');
   }
 
   return @statements;
@@ -234,8 +237,9 @@ sub get_alter_column_ddl {
 sub get_drop_fk_sql {
   my ($self, $table, $column, $references) = @_;
   my $fk_name = $self->_get_fk_name($table, $column, $references);
-  my @sql     = ("ALTER TABLE $table DROP FOREIGN KEY $fk_name");
   my $dbh     = Bugzilla->dbh;
+  my @sql     = (
+    "ALTER TABLE " . $dbh->quote_identifier($table) . " DROP FOREIGN KEY $fk_name");
 
   # MySQL requires, and will create, an index on any column with
   # an FK. It will name it after the fk, which we never do.
@@ -262,7 +266,7 @@ sub get_rename_indexes_ddl {
   my ($self, $table, %indexes) = @_;
   my @keys = keys %indexes or return ();
 
-  my $sql = "ALTER TABLE $table ";
+  my $sql = 'ALTER TABLE' . Bugzilla->dbh->quote_identifier($table) . ' ';
 
   foreach my $old_name (@keys) {
     my $name = $indexes{$old_name}->{NAME};
@@ -283,7 +287,9 @@ sub get_rename_indexes_ddl {
 
 sub get_set_serial_sql {
   my ($self, $table, $column, $value) = @_;
-  return ("ALTER TABLE $table AUTO_INCREMENT = $value");
+  return ("ALTER TABLE "
+      . Bugzilla->dbh->quote_identifier($table)
+      . " AUTO_INCREMENT = $value");
 }
 
 # Converts a DBI column_info output to an abstract column definition.
@@ -421,7 +427,9 @@ sub get_rename_column_ddl {
 
   # MySQL doesn't like having the PRIMARY KEY statement in a rename.
   $def =~ s/PRIMARY KEY//i;
-  return ("ALTER TABLE $table CHANGE COLUMN $old_name $new_name $def");
+  return ("ALTER TABLE "
+      . Bugzilla->dbh->quote_identifier($table)
+      . " CHANGE COLUMN $old_name $new_name $def");
 }
 
 1;
diff --git a/Bugzilla/DB/Schema/Oracle.pm b/Bugzilla/DB/Schema/Oracle.pm
index 416e9204b..6e7c54b6d 100644
--- a/Bugzilla/DB/Schema/Oracle.pm
+++ b/Bugzilla/DB/Schema/Oracle.pm
@@ -13,11 +13,10 @@ package Bugzilla::DB::Schema::Oracle;
 #
 ###############################################################################
 
-use 5.10.1;
-use strict;
-use warnings;
+use 5.14.0;
+use Moo;
 
-use parent qw(Bugzilla::DB::Schema);
+extends qw(Bugzilla::DB::Schema);
 use Carp qw(confess);
 use Bugzilla::Util;
 
@@ -34,7 +33,7 @@ sub _initialize {
 
   my $self = shift;
 
-  $self = $self->SUPER::_initialize(@_);
+  $self = $self->SUPER::_initialize();
 
   $self->{db_specific} = {
 
diff --git a/Bugzilla/DB/Schema/Pg.pm b/Bugzilla/DB/Schema/Pg.pm
index cf28a02d9..f81c0a46d 100644
--- a/Bugzilla/DB/Schema/Pg.pm
+++ b/Bugzilla/DB/Schema/Pg.pm
@@ -13,11 +13,10 @@ package Bugzilla::DB::Schema::Pg;
 #
 ###############################################################################
 
-use 5.10.1;
-use strict;
-use warnings;
+use 5.14.0;
+use Moo;
 
-use parent qw(Bugzilla::DB::Schema);
+extends qw(Bugzilla::DB::Schema);
 use Storable qw(dclone);
 
 #------------------------------------------------------------------------------
@@ -25,7 +24,7 @@ sub _initialize {
 
   my $self = shift;
 
-  $self = $self->SUPER::_initialize(@_);
+  $self = $self->SUPER::_initialize();
 
   # Remove FULLTEXT index types from the schemas.
   foreach my $table (keys %{$self->{schema}}) {
diff --git a/Bugzilla/DB/Schema/Sqlite.pm b/Bugzilla/DB/Schema/Sqlite.pm
index 57361d2bb..5814cf0aa 100644
--- a/Bugzilla/DB/Schema/Sqlite.pm
+++ b/Bugzilla/DB/Schema/Sqlite.pm
@@ -7,11 +7,10 @@
 
 package Bugzilla::DB::Schema::Sqlite;
 
-use 5.10.1;
-use strict;
-use warnings;
+use 5.14.0;
+use Moo;
 
-use parent qw(Bugzilla::DB::Schema);
+use base qw(Bugzilla::DB::Schema);
 
 use Bugzilla::Error;
 use Bugzilla::Util qw(generate_random_password);
@@ -24,7 +23,7 @@ sub _initialize {
 
   my $self = shift;
 
-  $self = $self->SUPER::_initialize(@_);
+  $self = $self->SUPER::_initialize();
 
   $self->{db_specific} = {
     BOOLEAN => 'integer',
@@ -110,7 +109,7 @@ sub _sqlite_alter_schema {
   my $insert_str = join(',', @insert_cols);
   my $select_str = join(',', @select_cols);
   my $copy_sql
-    = "INSERT INTO $table ($insert_str)" . " SELECT $select_str FROM $rename_to";
+    = "INSERT INTO " . $dbh->quote_identifier($table) . " ($insert_str)" . " SELECT $select_str FROM " . $dbh->quote_identifier($rename_to);
 
   # We have to turn FKs off before doing this. Otherwise, when we rename
   # the table, all of the FKs in the other tables will be automatically
@@ -123,7 +122,10 @@ sub _sqlite_alter_schema {
     'PRAGMA foreign_keys = OFF',
     'BEGIN EXCLUSIVE TRANSACTION',
     @{$options->{pre_sql} || []},
-    "ALTER TABLE $table RENAME TO $rename_to",
+    'ALTER TABLE '
+      . Bugzilla->dbh->quote_identifier($table)
+      . ' RENAME TO '
+      . Bugzilla->dbh->quote_identifier($rename_to),
     $create_table,
     $copy_sql,
     "DROP TABLE $rename_to",
diff --git a/Bugzilla/DB/Sqlite.pm b/Bugzilla/DB/Sqlite.pm
index c180fd0d7..55117eb9b 100644
--- a/Bugzilla/DB/Sqlite.pm
+++ b/Bugzilla/DB/Sqlite.pm
@@ -7,11 +7,10 @@
 
 package Bugzilla::DB::Sqlite;
 
-use 5.10.1;
-use strict;
-use warnings;
+use 5.14.0;
+use Moo;
 
-use parent qw(Bugzilla::DB);
+extends qw(Bugzilla::DB);
 
 use Bugzilla::Constants;
 use Bugzilla::Error;
@@ -69,7 +68,7 @@ sub _sqlite_position_ci {
 # Constructor #
 ###############
 
-sub new {
+sub BUILDARGS {
   my ($class, $params) = @_;
   my $db_name = $params->{db_name};
 
@@ -99,11 +98,11 @@ sub new {
     sqlite_unicode => Bugzilla->params->{'utf8'},
   };
 
-  my $self
-    = $class->db_new({dsn => $dsn, user => '', pass => '', attrs => $attrs});
+  return {dsn => $dsn, user => '', pass => '', attrs => $attrs};
+}
 
-  # Needed by TheSchwartz
-  $self->{private_bz_dsn} = $dsn;
+sub on_dbi_connected {
+  my ($class, $dbh) = @_;
 
   my %pragmas = (
 
@@ -129,25 +128,22 @@ sub new {
   );
 
   while (my ($name, $value) = each %pragmas) {
-    $self->do("PRAGMA $name = $value");
+    $dbh->do("PRAGMA $name = $value");
   }
 
-  $self->sqlite_create_collation('bugzilla', \&_sqlite_collate_ci);
-  $self->sqlite_create_function('position',  2, \&_sqlite_position);
-  $self->sqlite_create_function('iposition', 2, \&_sqlite_position_ci);
+  $dbh->sqlite_create_collation('bugzilla', \&_sqlite_collate_ci);
+  $dbh->sqlite_create_function('position',  2, \&_sqlite_position);
+  $dbh->sqlite_create_function('iposition', 2, \&_sqlite_position_ci);
 
   # SQLite has a "substr" function, but other DBs call it "SUBSTRING"
   # so that's what we use, and I don't know of any way in SQLite to
   # alias the SQL "substr" function to be called "SUBSTRING".
-  $self->sqlite_create_function('substring',      3, \&CORE::substr);
-  $self->sqlite_create_function('char_length',    1, sub { length($_[0]) });
-  $self->sqlite_create_function('mod',            2, \&_sqlite_mod);
-  $self->sqlite_create_function('now',            0, \&_sqlite_now);
-  $self->sqlite_create_function('localtimestamp', 1, \&_sqlite_now);
-  $self->sqlite_create_function('floor',          1, \&POSIX::floor);
-
-  bless($self, $class);
-  return $self;
+  $dbh->sqlite_create_function('substring',      3, \&CORE::substr);
+  $dbh->sqlite_create_function('char_length',    1, sub { length($_[0]) });
+  $dbh->sqlite_create_function('mod',            2, \&_sqlite_mod);
+  $dbh->sqlite_create_function('now',            0, \&_sqlite_now);
+  $dbh->sqlite_create_function('localtimestamp', 1, \&_sqlite_now);
+  $dbh->sqlite_create_function('floor',          1, \&POSIX::floor);
 }
 
 ###############
@@ -348,4 +344,8 @@ For interface details see L<Bugzilla::DB> and L<DBI>.
 
 =item bz_setup_database
 
+=item BUILDARGS
+
+=item on_dbi_connected
+
 =back
diff --git a/Bugzilla/Error.pm b/Bugzilla/Error.pm
index ac56b9b02..4a5fee295 100644
--- a/Bugzilla/Error.pm
+++ b/Bugzilla/Error.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::Error;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Exporter);
+use base qw(Exporter);
 
 @Bugzilla::Error::EXPORT = qw(ThrowCodeError ThrowTemplateError ThrowUserError);
 
diff --git a/Bugzilla/Extension.pm b/Bugzilla/Extension.pm
index a5522583e..b955fa638 100644
--- a/Bugzilla/Extension.pm
+++ b/Bugzilla/Extension.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::Extension;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
@@ -245,7 +245,7 @@ F<extensions/Foo.pm>:
 
  package Bugzilla::Extension::Foo
  use strict;
- use parent qw(Bugzilla::Extension);
+ use base qw(Bugzilla::Extension);
 
  our $VERSION = '0.02';
  use constant NAME => 'Foo';
diff --git a/Bugzilla/Field.pm b/Bugzilla/Field.pm
index 97013af51..1656f917a 100644
--- a/Bugzilla/Field.pm
+++ b/Bugzilla/Field.pm
@@ -57,11 +57,11 @@ in addition to what is documented here.
 
 package Bugzilla::Field;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Exporter Bugzilla::Object);
+use base qw(Exporter Bugzilla::Object);
 @Bugzilla::Field::EXPORT = qw(check_field get_field_id get_legal_field_values);
 
 use Bugzilla::Constants;
@@ -153,7 +153,7 @@ use constant SQL_DEFINITIONS => {
   {TYPE => 'varchar(64)', NOTNULL => 1, DEFAULT => "'---'"}, FIELD_TYPE_TEXTAREA,
   {TYPE => 'MEDIUMTEXT',  NOTNULL => 1, DEFAULT => "''"},    FIELD_TYPE_DATETIME,
   {TYPE => 'DATETIME'}, FIELD_TYPE_DATE, {TYPE => 'DATE'}, FIELD_TYPE_BUG_ID,
-  {TYPE => 'INT3'},     FIELD_TYPE_INTEGER,
+  {TYPE => 'INT3'}, FIELD_TYPE_INTEGER,
   {TYPE => 'INT4', NOTNULL => 1, DEFAULT => 0},
 };
 
@@ -1471,7 +1471,7 @@ sub check_field {
     return 0 if $no_warn;    # We don't want an error to be thrown; return.
     trick_taint($name);
 
-    my $field = new Bugzilla::Field({name => $name});
+    my $field      = new Bugzilla::Field({name => $name});
     my $field_desc = $field ? $field->description : $name;
     ThrowCodeError('illegal_field', {field => $field_desc});
   }
diff --git a/Bugzilla/Field/Choice.pm b/Bugzilla/Field/Choice.pm
index bd82a8e3f..34699fc7f 100644
--- a/Bugzilla/Field/Choice.pm
+++ b/Bugzilla/Field/Choice.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::Field::Choice;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Bugzilla::Field::ChoiceInterface Bugzilla::Object);
+use base qw(Bugzilla::Field::ChoiceInterface Bugzilla::Object);
 
 use Bugzilla::Config qw(SetParam write_params);
 use Bugzilla::Constants;
@@ -99,7 +99,7 @@ sub type {
   if (!defined *{"${package}::DB_TABLE"}) {
     eval <<EOC;
             package $package;
-            use parent qw(Bugzilla::Field::Choice);
+            use base qw(Bugzilla::Field::Choice);
             use constant DB_TABLE => '$field_name';
 EOC
   }
diff --git a/Bugzilla/Field/ChoiceInterface.pm b/Bugzilla/Field/ChoiceInterface.pm
index eeedfca83..91c49fb0a 100644
--- a/Bugzilla/Field/ChoiceInterface.pm
+++ b/Bugzilla/Field/ChoiceInterface.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::Field::ChoiceInterface;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/Bugzilla/Flag.pm b/Bugzilla/Flag.pm
index fb43d306b..8bbc47747 100644
--- a/Bugzilla/Flag.pm
+++ b/Bugzilla/Flag.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::Flag;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
@@ -51,7 +51,7 @@ use Bugzilla::Mailer;
 use Bugzilla::Constants;
 use Bugzilla::Field;
 
-use parent qw(Bugzilla::Object Exporter);
+use base qw(Bugzilla::Object Exporter);
 @Bugzilla::Flag::EXPORT = qw(SKIP_REQUESTEE_ON_ERROR);
 
 ###############################
diff --git a/Bugzilla/FlagType.pm b/Bugzilla/FlagType.pm
index a08ee83b9..0494f7d28 100644
--- a/Bugzilla/FlagType.pm
+++ b/Bugzilla/FlagType.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::FlagType;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
@@ -40,10 +40,10 @@ use Bugzilla::Error;
 use Bugzilla::Util;
 use Bugzilla::Group;
 
-use Email::Address;
+use Email::Address::XS;
 use List::MoreUtils qw(uniq);
 
-use parent qw(Bugzilla::Object);
+use base qw(Bugzilla::Object);
 
 ###############################
 ####    Initialization     ####
@@ -306,12 +306,12 @@ sub _check_cc_list {
     || ThrowUserError('flag_type_cc_list_invalid', {cc_list => $cc_list});
 
   my @addresses = split(/[,\s]+/, $cc_list);
-  my $addr_spec = $Email::Address::addr_spec;
 
   # We do not call check_email_syntax() because these addresses do not
   # require to match 'emailregexp' and do not depend on 'emailsuffix'.
   foreach my $address (@addresses) {
-    ($address !~ /\P{ASCII}/ && $address =~ /^$addr_spec$/)
+    my $email = Email::Address::XS->parse_bare_address($address); 
+    ($address !~ /\P{ASCII}/ && $email->is_valid)
       || ThrowUserError('illegal_email_address', {addr => $address, default => 1});
   }
   return $cc_list;
diff --git a/Bugzilla/Group.pm b/Bugzilla/Group.pm
index 8715c4cfe..2d7c72e85 100644
--- a/Bugzilla/Group.pm
+++ b/Bugzilla/Group.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::Group;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Bugzilla::Object);
+use base qw(Bugzilla::Object);
 
 use Bugzilla::Constants;
 use Bugzilla::Util;
@@ -440,7 +440,8 @@ sub create {
 sub ValidateGroupName {
   my ($name, @users) = (@_);
   my $dbh   = Bugzilla->dbh;
-  my $query = "SELECT id FROM groups " . "WHERE name = ?";
+  my $query
+    = 'SELECT id FROM ' . $dbh->quote_identifier('groups') . ' WHERE name = ?';
   if (Bugzilla->params->{'usevisibilitygroups'}) {
     my @visible = (-1);
     foreach my $user (@users) {
diff --git a/Bugzilla/Hook.pm b/Bugzilla/Hook.pm
index f5c67c692..0718d4d19 100644
--- a/Bugzilla/Hook.pm
+++ b/Bugzilla/Hook.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::Hook;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/Bugzilla/Install.pm b/Bugzilla/Install.pm
index cb05c44e9..c9bbd42ec 100644
--- a/Bugzilla/Install.pm
+++ b/Bugzilla/Install.pm
@@ -15,7 +15,7 @@ package Bugzilla::Install;
 # make those assumptions, then it should go into one of the
 # packages under the Bugzilla::Install namespace.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/Bugzilla/Install/CPAN.pm b/Bugzilla/Install/CPAN.pm
index 4321cac61..c2df627f0 100644
--- a/Bugzilla/Install/CPAN.pm
+++ b/Bugzilla/Install/CPAN.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::Install::CPAN;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Exporter);
+use base qw(Exporter);
 our @EXPORT = qw(
   BZ_LIB
 
diff --git a/Bugzilla/Install/DB.pm b/Bugzilla/Install/DB.pm
index ab71a5334..1570a1b4d 100644
--- a/Bugzilla/Install/DB.pm
+++ b/Bugzilla/Install/DB.pm
@@ -10,7 +10,7 @@ package Bugzilla::Install::DB;
 # NOTE: This package may "use" any modules that it likes,
 # localconfig is available, and params are up to date.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
@@ -837,7 +837,7 @@ sub _update_flagtypes_id {
     }
   }
 
-  if ($flagtypes_def->{TYPE} eq 'SMALLSERIAL') {
+  if ($flagtypes_def->{TYPE} ne 'MEDIUMSERIAL') {
     $flagtypes_def->{TYPE} = 'MEDIUMSERIAL';
     $dbh->bz_alter_column('flagtypes', 'id', $flagtypes_def);
   }
@@ -1640,7 +1640,8 @@ sub _convert_groups_system_from_groupset {
     $dbh->bz_drop_index('groups', 'groups_name_idx');
     my @primary_key = $dbh->primary_key(undef, undef, 'groups');
     if (@primary_key) {
-      $dbh->do("ALTER TABLE groups DROP PRIMARY KEY");
+      $dbh->do(
+        'ALTER TABLE ' . $dbh->quote_identifier('groups') . ' DROP PRIMARY KEY');
     }
 
     $dbh->bz_add_column('groups', 'id',
@@ -1651,7 +1652,8 @@ sub _convert_groups_system_from_groupset {
 
     # Convert all existing groupset records to map entries before removing
     # groupset fields or removing "bit" from groups.
-    my $sth = $dbh->prepare("SELECT bit, id FROM groups WHERE bit > 0");
+    my $sth = $dbh->prepare(
+      'SELECT bit, id FROM ' . $dbh->quote_identifier('groups') . ' WHERE bit > 0');
     $sth->execute();
     while (my ($bit, $gid) = $sth->fetchrow_array) {
 
@@ -1746,7 +1748,7 @@ sub _convert_groups_system_from_groupset {
 
         # Get names of groups added.
         my $sth2 = $dbh->prepare(
-          "SELECT name FROM groups
+          "SELECT name FROM " . $dbh->quote_identifier('groups') . "
                                            WHERE (bit & $added) != 0
                                                  AND (bit & $removed) = 0"
         );
@@ -1758,7 +1760,7 @@ sub _convert_groups_system_from_groupset {
 
         # Get names of groups removed.
         $sth2 = $dbh->prepare(
-          "SELECT name FROM groups
+          "SELECT name FROM " . $dbh->quote_identifier('groups') . "
                                         WHERE (bit & $removed) != 0
                                               AND (bit & $added) = 0"
         );
@@ -1771,9 +1773,7 @@ sub _convert_groups_system_from_groupset {
         # Get list of group bits added that correspond to
         # missing groups.
         $sth2 = $dbh->prepare(
-          "SELECT ($added & ~BIT_OR(bit)) 
-                                         FROM groups"
-        );
+          "SELECT ($added & ~BIT_OR(bit)) FROM " . $dbh->quote_identifier('groups'));
         $sth2->execute();
         my ($miss) = $sth2->fetchrow_array;
         if ($miss) {
@@ -1785,9 +1785,7 @@ sub _convert_groups_system_from_groupset {
         # Get list of group bits deleted that correspond to
         # missing groups.
         $sth2 = $dbh->prepare(
-          "SELECT ($removed & ~BIT_OR(bit)) 
-                                         FROM groups"
-        );
+          "SELECT ($removed & ~BIT_OR(bit)) FROM " . $dbh->quote_identifier('groups'));
         $sth2->execute();
         ($miss) = $sth2->fetchrow_array;
         if ($miss) {
@@ -1823,7 +1821,7 @@ sub _convert_groups_system_from_groupset {
 
         # Get names of groups added.
         my $sth2 = $dbh->prepare(
-          "SELECT name FROM groups
+          "SELECT name FROM " . $dbh->quote_identifier('groups') . "
                                            WHERE (bit & $added) != 0
                                                  AND (bit & $removed) = 0"
         );
@@ -1835,7 +1833,7 @@ sub _convert_groups_system_from_groupset {
 
         # Get names of groups removed.
         $sth2 = $dbh->prepare(
-          "SELECT name FROM groups
+          "SELECT name FROM " . $dbh->quote_identifier('groups') . "
                                         WHERE (bit & $removed) != 0
                                               AND (bit & $added) = 0"
         );
@@ -1864,11 +1862,12 @@ sub _convert_groups_system_from_groupset {
 
     # Identify admin group.
     my ($admin_gid)
-      = $dbh->selectrow_array("SELECT id FROM groups WHERE name = 'admin'");
+      = $dbh->selectrow_array(
+        "SELECT id FROM " . $dbh->quote_identifier('groups') . " WHERE name = 'admin'");
     if (!$admin_gid) {
-      $dbh->do(q{INSERT INTO groups (name, description)
-                                   VALUES ('admin', 'Administrators')}
-      );
+      $dbh->do("INSERT INTO "
+               . $dbh->quote_identifier('groups')
+               . " (name, description) VALUES ('admin', 'Administrators')");
       $admin_gid = $dbh->bz_last_key('groups', 'id');
     }
 
@@ -2495,7 +2494,8 @@ sub _fix_group_with_empty_name {
   # Note that there can be at most one such group (because of
   # the SQL index on the name column).
   my ($emptygroupid)
-    = $dbh->selectrow_array("SELECT id FROM groups where name = ''");
+    = $dbh->selectrow_array(
+    "SELECT id FROM " . $dbh->quote_identifier('groups') . " where name = ''");
   if ($emptygroupid) {
 
     # There is a group with an empty name; find a name to rename it
@@ -2503,7 +2503,8 @@ sub _fix_group_with_empty_name {
     # group_$gid and add _<n> if necessary.
     my $trycount = 0;
     my $trygroupname;
-    my $sth         = $dbh->prepare("SELECT 1 FROM groups where name = ?");
+    my $sth = $dbh->prepare(
+      'SELECT 1 FROM ' . $dbh->quote_identifier('groups') . ' where name = ?');
     my $name_exists = 1;
 
     while ($name_exists) {
@@ -2514,7 +2515,8 @@ sub _fix_group_with_empty_name {
       $name_exists = $dbh->selectrow_array($sth, undef, $trygroupname);
       $trycount++;
     }
-    $dbh->do("UPDATE groups SET name = ? WHERE id = ?",
+    $dbh->do(
+      'UPDATE ' . $dbh->quote_identifier('groups') . ' SET name = ? WHERE id = ?',
       undef, $trygroupname, $emptygroupid);
     print "Group $emptygroupid had an empty name; renamed as",
       " '$trygroupname'.\n";
@@ -2685,7 +2687,8 @@ sub _change_all_mysql_booleans_to_tinyint {
     my $quip_info_sth = $dbh->column_info(undef, undef, 'quips', '%');
     my $quips_cols    = $quip_info_sth->fetchall_hashref("COLUMN_NAME");
     my $approved_col  = $quips_cols->{'approved'};
-    if (  $approved_col->{TYPE_NAME} eq 'TINYINT'
+    if ($approved_col->{TYPE_NAME} eq 'TINYINT'
+      and defined $approved_col->{COLUMN_SIZE}
       and $approved_col->{COLUMN_SIZE} == 1)
     {
       # series.public could have been renamed to series.is_public,
@@ -2947,8 +2950,11 @@ EOT
 sub _rederive_regex_groups {
   my $dbh = Bugzilla->dbh;
 
-  my $regex_groups_exist = $dbh->selectrow_array(
-    "SELECT 1 FROM groups WHERE userregexp = '' " . $dbh->sql_limit(1));
+  my $regex_groups_exist
+    = $dbh->selectrow_array("SELECT 1 FROM "
+      . $dbh->quote_identifier('groups')
+      . " WHERE userregexp = '' "
+      . $dbh->sql_limit(1));
   return if !$regex_groups_exist;
 
   my $regex_derivations
@@ -2963,7 +2969,7 @@ sub _rederive_regex_groups {
   my $sth = $dbh->prepare(
     "SELECT profiles.userid, profiles.login_name, groups.id, 
                 groups.userregexp, user_group_map.group_id
-           FROM (profiles CROSS JOIN groups)
+           FROM (profiles CROSS JOIN " . $dbh->quote_identifier('groups') . ")
                 LEFT JOIN user_group_map
                        ON user_group_map.user_id = profiles.userid
                           AND user_group_map.group_id = groups.id
@@ -4115,7 +4121,7 @@ sub _add_password_salt_separator {
   my $profiles
     = $dbh->selectall_arrayref(
         "SELECT userid, cryptpassword FROM profiles WHERE ("
-      . $dbh->sql_regexp("cryptpassword", "'^[^,]+{'")
+      . $dbh->sql_regexp("cryptpassword", "'^[^,]+\\\\{'")
       . ")");
 
   if (@$profiles) {
diff --git a/Bugzilla/Install/Filesystem.pm b/Bugzilla/Install/Filesystem.pm
index 8d6163223..13fee1c11 100644
--- a/Bugzilla/Install/Filesystem.pm
+++ b/Bugzilla/Install/Filesystem.pm
@@ -15,7 +15,7 @@ package Bugzilla::Install::Filesystem;
 # * Files do not have the correct permissions.
 # * The database does not exist.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
@@ -34,7 +34,7 @@ use File::Spec;
 use IO::File;
 use POSIX ();
 
-use parent qw(Exporter);
+use base qw(Exporter);
 our @EXPORT = qw(
   update_filesystem
   create_htaccess
diff --git a/Bugzilla/Install/Localconfig.pm b/Bugzilla/Install/Localconfig.pm
index bc4557309..8006530d9 100644
--- a/Bugzilla/Install/Localconfig.pm
+++ b/Bugzilla/Install/Localconfig.pm
@@ -15,7 +15,7 @@ package Bugzilla::Install::Localconfig;
 # * Files do not have the correct permissions
 # * The database is not up to date
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
@@ -28,7 +28,7 @@ use File::Basename qw(dirname);
 use Safe;
 use Term::ANSIColor;
 
-use parent qw(Exporter);
+use base qw(Exporter);
 
 our @EXPORT_OK = qw(
   read_localconfig
diff --git a/Bugzilla/Install/Requirements.pm b/Bugzilla/Install/Requirements.pm
index 852f7f78e..b051afe7c 100644
--- a/Bugzilla/Install/Requirements.pm
+++ b/Bugzilla/Install/Requirements.pm
@@ -13,7 +13,7 @@ package Bugzilla::Install::Requirements;
 # Subroutines may "require" and "import" from modules, but they
 # MUST NOT "use."
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
@@ -23,7 +23,7 @@ use Bugzilla::Install::Util qw(install_string bin_loc
 use List::Util qw(max);
 use Term::ANSIColor;
 
-use parent qw(Exporter);
+use base qw(Exporter);
 our @EXPORT = qw(
   REQUIRED_MODULES
   OPTIONAL_MODULES
@@ -64,11 +64,11 @@ use constant APACHE => qw(apachectl httpd apache2 apache);
 # If we don't find any of the above binaries in the normal PATH,
 # these are extra places we look.
 use constant APACHE_PATH => [qw(
-    /usr/sbin
-    /usr/local/sbin
-    /usr/libexec
-    /usr/local/libexec
-    )];
+  /usr/sbin
+  /usr/local/sbin
+  /usr/libexec
+  /usr/local/libexec
+)];
 
 # The below two constants are subroutines so that they can implement
 # a hook. Other than that they are actually constants.
@@ -78,8 +78,8 @@ use constant APACHE_PATH => [qw(
 # installed or not. "version" is the version we need, or 0 if we'll accept
 # any version.
 #
-# "blacklist" is an arrayref of regular expressions that describe versions that
-# are 'blacklisted'--that is, even if the version is high enough, Bugzilla
+# "blocklist" is an arrayref of regular expressions that describe versions that
+# are 'blocklisted'--that is, even if the version is high enough, Bugzilla
 # will refuse to say that it's OK to run with that version.
 sub REQUIRED_MODULES {
   my @modules = (
@@ -114,11 +114,23 @@ sub REQUIRED_MODULES {
       version => ($^V >= v5.13.3) ? '1.614' : '1.54'
     },
 
-    # 2.24 contains several useful text virtual methods.
-    {package => 'Template-Toolkit', module => 'Template', version => '2.24'},
+    {package => 'DBIx-Connector', module => 'DBIx::Connector', version => '0.56',},
+
+    {package => 'Moo', module => 'Moo', version => '2.003004',},
+
+    # versions prior to 3.008 are broken, see https://bugzilla.mozilla.org/show_bug.cgi?id=1560873
+    {package => 'Template-Toolkit', module => 'Template', version => '3.008'},
+
+    # versions prior to 2.600 pulled Email::Address, we now use Email::Address::XS
+    {package => 'Email-Sender', module => 'Email::Sender', version => '2.600',},
+
+    # versions prior to 1.05 contain a security risk
+    {
+      package => 'Email-Address-XS',
+      module  => 'Email::Address::XS',
+      version => '1.05',
+    },
 
-    # 1.300011 has a debug mode for SMTP and automatically pass -i to sendmail.
-    {package => 'Email-Sender', module => 'Email::Sender', version => '1.300011',},
     {
       package => 'Email-MIME',
       module  => 'Email::MIME',
@@ -284,7 +296,7 @@ sub OPTIONAL_MODULES {
       version => '0.712',
 
       # SOAP::Transport::HTTP 1.12 is bogus.
-      blacklist => ['^1\.12$'],
+      blocklist => ['^1\.12$'],
       feature   => ['xmlrpc'],
     },
 
@@ -741,13 +753,12 @@ sub have_vers {
   }
   $vnum ||= -1;
 
-  # Must do a string comparison as $vnum may be of the form 5.10.1.
   my $vok
     = ($vnum ne '-1' && version->new($vnum) >= version->new($wanted)) ? 1 : 0;
-  my $blacklisted;
-  if ($vok && $params->{blacklist}) {
-    $blacklisted = grep($vnum =~ /$_/, @{$params->{blacklist}});
-    $vok = 0 if $blacklisted;
+  my $blocklisted;
+  if ($vok && $params->{blocklist}) {
+    $blocklisted = grep($vnum =~ /$_/, @{$params->{blocklist}});
+    $vok         = 0 if $blocklisted;
   }
 
   if ($output) {
@@ -756,7 +767,7 @@ sub have_vers {
       ok          => $vok,
       wanted      => $wanted,
       found       => $vnum,
-      blacklisted => $blacklisted
+      blocklisted => $blocklisted
     });
   }
 
@@ -765,8 +776,8 @@ sub have_vers {
 
 sub _checking_for {
   my ($params) = @_;
-  my ($package, $ok, $wanted, $blacklisted, $found)
-    = @$params{qw(package ok wanted blacklisted found)};
+  my ($package, $ok, $wanted, $blocklisted, $found)
+    = @$params{qw(package ok wanted blocklisted found)};
 
   my $ok_string = $ok ? install_string('module_ok') : '';
 
@@ -794,10 +805,10 @@ sub _checking_for {
     $ok_string = install_string('module_not_found');
   }
 
-  my $black_string = $blacklisted ? install_string('blacklisted') : '';
+  my $block_string = $blocklisted ? install_string('blocklisted') : '';
   my $want_string = $wanted ? "v$wanted" : install_string('any');
 
-  my $str = sprintf "%s %20s %-11s $ok_string $black_string\n",
+  my $str = sprintf "%s %20s %-11s $ok_string $block_string\n",
     install_string('checking_for'), $package, "($want_string)";
   print $ok ? $str : colored($str, COLOR_ERROR);
 }
diff --git a/Bugzilla/Install/Util.pm b/Bugzilla/Install/Util.pm
index 617977241..3b021b983 100644
--- a/Bugzilla/Install/Util.pm
+++ b/Bugzilla/Install/Util.pm
@@ -11,7 +11,7 @@ package Bugzilla::Install::Util;
 # module may require *only* Bugzilla::Constants and built-in
 # perl modules.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
@@ -25,7 +25,7 @@ use Scalar::Util qw(tainted);
 use Term::ANSIColor qw(colored);
 use PerlIO;
 
-use parent qw(Exporter);
+use base qw(Exporter);
 our @EXPORT_OK = qw(
   bin_loc
   get_version_and_os
@@ -267,7 +267,7 @@ sub indicate_progress {
   my ($params) = @_;
   my $current  = $params->{current};
   my $total    = $params->{total};
-  my $every = $params->{every} || 1;
+  my $every    = $params->{every} || 1;
 
   print "." if !($current % $every);
   if ($current == $total || $current % ($every * 60) == 0) {
diff --git a/Bugzilla/Job/BugMail.pm b/Bugzilla/Job/BugMail.pm
index a6deb5777..78bec19e4 100644
--- a/Bugzilla/Job/BugMail.pm
+++ b/Bugzilla/Job/BugMail.pm
@@ -7,12 +7,12 @@
 
 package Bugzilla::Job::BugMail;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
 use Bugzilla::BugMail;
-BEGIN { eval "use parent qw(Bugzilla::Job::Mailer)"; }
+BEGIN { eval "use base qw(Bugzilla::Job::Mailer)"; }
 
 sub work {
   my ($class, $job) = @_;
diff --git a/Bugzilla/Job/Mailer.pm b/Bugzilla/Job/Mailer.pm
index 544d86107..904fd9eb1 100644
--- a/Bugzilla/Job/Mailer.pm
+++ b/Bugzilla/Job/Mailer.pm
@@ -7,12 +7,12 @@
 
 package Bugzilla::Job::Mailer;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
 use Bugzilla::Mailer;
-BEGIN { eval "use parent qw(TheSchwartz::Worker)"; }
+BEGIN { eval "use base qw(TheSchwartz::Worker)"; }
 
 # The longest we expect a job to possibly take, in seconds.
 use constant grab_for => 300;
diff --git a/Bugzilla/JobQueue.pm b/Bugzilla/JobQueue.pm
index e48182007..1a45d471b 100644
--- a/Bugzilla/JobQueue.pm
+++ b/Bugzilla/JobQueue.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::JobQueue;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
@@ -56,7 +56,7 @@ sub new {
   # to write to it.
   my $self = $class->SUPER::new(
     databases => [{
-      dsn    => Bugzilla->dbh_main->{private_bz_dsn},
+      dsn    => Bugzilla->dbh_main->dsn,
       user   => $lc->{db_user},
       pass   => $lc->{db_pass},
       prefix => 'ts_',
diff --git a/Bugzilla/JobQueue/Runner.pm b/Bugzilla/JobQueue/Runner.pm
index a9eb0bca4..ec51c2353 100644
--- a/Bugzilla/JobQueue/Runner.pm
+++ b/Bugzilla/JobQueue/Runner.pm
@@ -11,7 +11,7 @@
 
 package Bugzilla::JobQueue::Runner;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
@@ -23,7 +23,7 @@ use Pod::Usage;
 use Bugzilla::Constants;
 use Bugzilla::JobQueue;
 use Bugzilla::Util qw(get_text);
-BEGIN { eval "use parent qw(Daemon::Generic)"; }
+BEGIN { eval "use base qw(Daemon::Generic)"; }
 
 our $VERSION = BUGZILLA_VERSION;
 
diff --git a/Bugzilla/Keyword.pm b/Bugzilla/Keyword.pm
index f1cb6cadf..a8be1f538 100644
--- a/Bugzilla/Keyword.pm
+++ b/Bugzilla/Keyword.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::Keyword;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Bugzilla::Object);
+use base qw(Bugzilla::Object);
 
 use Bugzilla::Error;
 use Bugzilla::Util;
diff --git a/Bugzilla/MIME.pm b/Bugzilla/MIME.pm
index 660799e66..08ec0b0c7 100644
--- a/Bugzilla/MIME.pm
+++ b/Bugzilla/MIME.pm
@@ -7,14 +7,14 @@
 
 package Bugzilla::MIME;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Email::MIME);
+use base qw(Email::MIME);
 
 sub new {
-  my ($class, $msg) = @_;
+  my ($class, $msg, $args) = @_;
   state $use_utf8 = Bugzilla->params->{'utf8'};
 
   # Template-Toolkit trims trailing newlines, which is problematic when
@@ -54,7 +54,7 @@ sub new {
   # you're running on. See http://perldoc.perl.org/perlport.html#Newlines
   $msg =~ s/(?:\015+)?\012/\015\012/msg;
 
-  return $class->SUPER::new($msg);
+  return $class->SUPER::new($msg, $args);
 }
 
 sub as_string {
@@ -113,7 +113,7 @@ workarounds.
 =head1 SYNOPSIS
 
   use Bugzilla::MIME;
-  my $email = Bugzilla::MIME->new($message);
+  my $email = Bugzilla::MIME->new($message, $args);
 
 =head1 DESCRIPTION
 
diff --git a/Bugzilla/Mailer.pm b/Bugzilla/Mailer.pm
index a5f79b9bc..427b0f9ea 100644
--- a/Bugzilla/Mailer.pm
+++ b/Bugzilla/Mailer.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::Mailer;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Exporter);
+use base qw(Exporter);
 @Bugzilla::Mailer::EXPORT = qw(MessageToMTA build_thread_marker generate_email);
 
 use Bugzilla::Constants;
@@ -21,6 +21,7 @@ use Bugzilla::MIME;
 use Bugzilla::Util;
 use Bugzilla::User;
 
+use Encode qw();
 use Date::Format qw(time2str);
 
 use Email::Sender::Simple qw(sendmail);
@@ -62,6 +63,7 @@ sub generate_email {
       encoding     => 'quoted-printable',
     },
     body_str => $msg_text,
+    encode_check => Encode::FB_DEFAULT
   ));
   if ($templates->{html} && $email_format eq 'html') {
     $template->process($templates->{html}, $vars, \$msg_html)
@@ -74,6 +76,7 @@ sub generate_email {
         encoding     => 'quoted-printable',
       },
       body_str => $msg_html,
+      encode_check => Encode::FB_DEFAULT
       );
   }
 
diff --git a/Bugzilla/Memcached.pm b/Bugzilla/Memcached.pm
index 2f30c186a..6d1633686 100644
--- a/Bugzilla/Memcached.pm
+++ b/Bugzilla/Memcached.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::Memcached;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/Bugzilla/Migrate.pm b/Bugzilla/Migrate.pm
index d8189f5f3..0d601ffcf 100644
--- a/Bugzilla/Migrate.pm
+++ b/Bugzilla/Migrate.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::Migrate;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
@@ -894,8 +894,13 @@ sub _do_table_insert {
   my @values       = map { $hash->{$_} } @fields;
   my $field_sql    = join(',', @fields);
   my $question_sql = join(',', @questions);
-  Bugzilla->dbh->do("INSERT INTO $table ($field_sql) VALUES ($question_sql)",
-    undef, @values);
+  my $dbh          = Bugzilla->dbh;
+  $dbh->do(
+    "INSERT INTO "
+      . $dbh->quote_identifier($table)
+      . " ($field_sql) VALUES ($question_sql)",
+    undef, @values
+  );
 }
 
 ######################
diff --git a/Bugzilla/Migrate/Gnats.pm b/Bugzilla/Migrate/Gnats.pm
index a5aa642e1..db47b94ce 100644
--- a/Bugzilla/Migrate/Gnats.pm
+++ b/Bugzilla/Migrate/Gnats.pm
@@ -7,17 +7,17 @@
 
 package Bugzilla::Migrate::Gnats;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Bugzilla::Migrate);
+use base qw(Bugzilla::Migrate);
 
 use Bugzilla::Constants;
 use Bugzilla::Install::Util qw(indicate_progress);
 use Bugzilla::Util qw(format_time trim generate_random_password);
 
-use Email::Address;
+use Email::Address::XS;
 use Email::MIME;
 use File::Basename;
 use IO::File;
@@ -396,10 +396,10 @@ sub _get_gnats_field_data {
   if ($originator !~ Bugzilla->params->{emailregexp}) {
 
     # We use the raw header sometimes, because it looks like "From: user"
-    # which Email::Address won't parse but we can still use.
+    # which Email::Address::XS won't parse but we can still use.
     my $address = $email->header('From');
-    my ($parsed) = Email::Address->parse($address);
-    if ($parsed) {
+    my ($parsed) = Email::Address::XS->parse($address);
+    if ($parsed->is_valid) {
       $address = $parsed->address;
     }
     if ($address) {
@@ -552,9 +552,9 @@ sub _parse_audit_trail {
         $current_data{bug_when} = $self->parse_date($value);
       }
       if ($column eq 'Why') {
-        $value = '' if !defined $value;
+        $value                 = '' if !defined $value;
         $current_data{comment} = $value;
-        $on_why = 1;
+        $on_why                = 1;
       }
       else {
         $on_why = 0;
diff --git a/Bugzilla/Milestone.pm b/Bugzilla/Milestone.pm
index 815a111f8..149255a88 100644
--- a/Bugzilla/Milestone.pm
+++ b/Bugzilla/Milestone.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::Milestone;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Bugzilla::Object);
+use base qw(Bugzilla::Object);
 
 use Bugzilla::Constants;
 use Bugzilla::Util;
diff --git a/Bugzilla/Object.pm b/Bugzilla/Object.pm
index 753ad9047..ee49de84a 100644
--- a/Bugzilla/Object.pm
+++ b/Bugzilla/Object.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::Object;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
@@ -108,6 +108,7 @@ sub _load_from_db {
   my $table      = $class->DB_TABLE;
   my $name_field = $class->NAME_FIELD;
   my $id_field   = $class->ID_FIELD;
+  my $sql_table  = $dbh->quote_identifier($table);
 
   my $id = $param;
   if (ref $param eq 'HASH') {
@@ -126,11 +127,10 @@ sub _load_from_db {
     # Too large integers make PostgreSQL crash.
     return if $id > MAX_INT_32;
 
-    $object_data = $dbh->selectrow_hashref(
-      qq{
-            SELECT $columns FROM $table
-             WHERE $id_field = ?}, undef, $id
-    );
+    $object_data
+      = $dbh->selectrow_hashref(
+      "SELECT $columns FROM $sql_table WHERE $id_field = ?",
+      undef, $id);
   }
   else {
     unless (defined $param->{name}
@@ -159,7 +159,7 @@ sub _load_from_db {
 
     map { trick_taint($_) } @values;
     $object_data
-      = $dbh->selectrow_hashref("SELECT $columns FROM $table WHERE $condition",
+      = $dbh->selectrow_hashref("SELECT $columns FROM $sql_table WHERE $condition",
       undef, @values);
   }
   return $object_data;
@@ -386,15 +386,16 @@ sub _do_list_select {
   }
 
   if (!$objects) {
-    my $sql = "SELECT $cols FROM $table";
+    my $dbh = Bugzilla->dbh;
+
+    my $sql_table = $dbh->quote_identifier($table);
+    my $sql = "SELECT $cols FROM $sql_table";
     if (defined $where) {
       $sql .= " WHERE $where ";
     }
     $sql .= " ORDER BY $order";
     $sql .= " $postamble" if $postamble;
 
-    my $dbh = Bugzilla->dbh;
-
     # Sometimes the values are tainted, but we don't want to untaint them
     # for the caller. So we copy the array. It's safe to untaint because
     # they're only used in placeholders here.
@@ -533,7 +534,8 @@ sub update {
 
   my $columns = join(', ', map {"$_ = ?"} @update_columns);
 
-  $dbh->do("UPDATE $table SET $columns WHERE $id_field = ?",
+  my $sql_table = $dbh->quote_identifier($table);
+  $dbh->do("UPDATE $sql_table SET $columns WHERE $id_field = ?",
     undef, @values, $self->id)
     if @values;
 
@@ -565,7 +567,8 @@ sub remove_from_db {
   my $dbh      = Bugzilla->dbh;
   $dbh->bz_start_transaction();
   $self->audit_log(AUDIT_REMOVE) if $self->AUDIT_REMOVES;
-  $dbh->do("DELETE FROM $table WHERE $id_field = ?", undef, $self->id);
+  my $sql_table = $dbh->quote_identifier($table);
+  $dbh->do("DELETE FROM $sql_table WHERE $id_field = ?", undef, $self->id);
   $dbh->bz_commit_transaction();
 
   if ($self->USE_MEMCACHED) {
@@ -644,8 +647,9 @@ sub any_exist {
   my $class = shift;
   my $table = $class->DB_TABLE;
   my $dbh   = Bugzilla->dbh;
-  my $any_exist
-    = $dbh->selectrow_array("SELECT 1 FROM $table " . $dbh->sql_limit(1));
+  my $sql_table = $dbh->quote_identifier($table);
+  my $any_exist = $dbh->selectrow_array(
+    "SELECT 1 FROM $sql_table " . $dbh->sql_limit(1));
   return $any_exist ? 1 : 0;
 }
 
@@ -743,9 +747,13 @@ sub insert_create_data {
   my $qmarks = '?,' x @field_names;
   chop($qmarks);
   my $table = $class->DB_TABLE;
+  my $sql_table = $dbh->quote_identifier($table);
   $dbh->do(
-    "INSERT INTO $table (" . join(', ', @field_names) . ") VALUES ($qmarks)",
-    undef, @values);
+    "INSERT INTO $sql_table ("
+      . join(', ', @field_names)
+      . ") VALUES ($qmarks)",
+    undef, @values
+  );
   my $id = $dbh->bz_last_key($table, $class->ID_FIELD);
 
   my $object = $class->new($id);
diff --git a/Bugzilla/Product.pm b/Bugzilla/Product.pm
index 2b39c2819..d4cb7da6c 100644
--- a/Bugzilla/Product.pm
+++ b/Bugzilla/Product.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::Product;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Bugzilla::Field::ChoiceInterface Bugzilla::Object);
+use base qw(Bugzilla::Field::ChoiceInterface Bugzilla::Object);
 
 use Bugzilla::Constants;
 use Bugzilla::Util;
@@ -623,13 +623,13 @@ sub group_controls {
   if (!defined $self->{group_controls} || $full_data) {
 
     # Include name to the list, to allow us sorting data more easily.
-    my $query = qq{SELECT id, name, entry, membercontrol, othercontrol,
+    my $query = "SELECT id, name, entry, membercontrol, othercontrol,
                               canedit, editcomponents, editbugs, canconfirm
-                         FROM groups
+                         FROM " . $dbh->quote_identifier('groups') . "
                               LEFT JOIN group_control_map
                               ON id = group_id 
                 $where_or_and product_id = ?
-                $and_or_where isbuggroup = 1};
+                $and_or_where isbuggroup = 1";
     $self->{group_controls}
       = $dbh->selectall_hashref($query, 'id', undef, $self->id);
 
@@ -666,7 +666,9 @@ sub groups_available {
     $dbh->selectcol_arrayref(
       "SELECT group_id, membercontrol
            FROM group_control_map
-                INNER JOIN groups ON group_control_map.group_id = groups.id
+                INNER JOIN "
+        . $dbh->quote_identifier('groups')
+        . " ON group_control_map.group_id = groups.id
           WHERE isbuggroup = 1 AND isactive = 1 AND product_id = ?
                 AND (membercontrol = $shown OR membercontrol = $default)
                 AND " . Bugzilla->user->groups_in_sql(), {Columns => [1, 2]},
@@ -681,7 +683,9 @@ sub groups_available {
     $dbh->selectcol_arrayref(
       "SELECT group_id, othercontrol
            FROM group_control_map
-                INNER JOIN groups ON group_control_map.group_id = groups.id
+                INNER JOIN "
+        . $dbh->quote_identifier('groups')
+        . " ON group_control_map.group_id = groups.id
           WHERE isbuggroup = 1 AND isactive = 1 AND product_id = ?
                 AND (othercontrol = $shown OR othercontrol = $default)",
       {Columns => [1, 2]}, $self->id
@@ -715,10 +719,13 @@ sub groups_mandatory {
   # For membercontrol we don't check group_id IN, because if membercontrol
   # is Mandatory, the group is Mandatory for everybody, regardless of their
   # group membership.
+  my $dbh = Bugzilla->dbh;
   my $ids = Bugzilla->dbh->selectcol_arrayref(
     "SELECT group_id 
            FROM group_control_map
-                INNER JOIN groups ON group_control_map.group_id = groups.id
+                INNER JOIN "
+      . $dbh->quote_identifier('groups')
+      . " ON group_control_map.group_id = groups.id
           WHERE product_id = ? AND isactive = 1
                 AND (membercontrol = $mandatory
                      OR (othercontrol = $mandatory
@@ -751,10 +758,13 @@ sub groups_valid {
 
   # Note that we don't check OtherControl below, because there is no
   # valid NA/* combination.
-  my $ids = Bugzilla->dbh->selectcol_arrayref(
+  my $dbh = Bugzilla->dbh;
+  my $ids = $dbh->selectcol_arrayref(
     "SELECT DISTINCT group_id
           FROM group_control_map AS gcm
-               INNER JOIN groups ON gcm.group_id = groups.id
+               INNER JOIN "
+      . $dbh->quote_identifier('groups')
+      . " ON gcm.group_id = groups.id
          WHERE product_id = ? AND isbuggroup = 1
                AND membercontrol != " . CONTROLMAPNA, undef, $self->id
   );
diff --git a/Bugzilla/RNG.pm b/Bugzilla/RNG.pm
index b92cbd720..d95ab5d55 100644
--- a/Bugzilla/RNG.pm
+++ b/Bugzilla/RNG.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::RNG;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Exporter);
+use base qw(Exporter);
 use Bugzilla::Constants qw(ON_WINDOWS);
 
 use Math::Random::ISAAC;
diff --git a/Bugzilla/Report.pm b/Bugzilla/Report.pm
index 84ea3b38b..d9a65cdd9 100644
--- a/Bugzilla/Report.pm
+++ b/Bugzilla/Report.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::Report;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Bugzilla::Object);
+use base qw(Bugzilla::Object);
 
 use Bugzilla::CGI;
 use Bugzilla::Constants;
diff --git a/Bugzilla/Search.pm b/Bugzilla/Search.pm
index 2005fe4db..fcd6273ab 100644
--- a/Bugzilla/Search.pm
+++ b/Bugzilla/Search.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::Search;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Exporter);
+use base qw(Exporter);
 @Bugzilla::Search::EXPORT = qw(
   IsValidQueryType
   split_order_term
@@ -1263,8 +1263,9 @@ sub _translate_join {
   if ($extra_condition) {
     $extra_condition = " AND $extra_condition";
   }
-
-  my @join_sql = "$join JOIN $table AS $name"
+  my $sql_table
+    = $table eq 'groups' ? Bugzilla->dbh->quote_identifier($table) : $table;
+  my @join_sql  = "$join JOIN $sql_table AS $name "
     . " ON $from_table.$from = $name.$to$extra_condition";
   return @join_sql;
 }
@@ -1431,7 +1432,7 @@ sub _parse_basic_fields {
     my @values = $self->_param_array($param_name);
     next if !@values;
     my $default_op = $param_name eq 'content' ? 'matches' : 'anyexact';
-    my $operator = $params->{"${param_name}_type"} || $default_op;
+    my $operator   = $params->{"${param_name}_type"} || $default_op;
 
     # Fields that are displayed as multi-selects are passed as arrays,
     # so that they can properly search values that contain commas.
@@ -1685,19 +1686,19 @@ sub _boolean_charts {
   my @param_list = keys %$params;
 
   my @all_field_params = grep {/^field-?\d+/} @param_list;
-  my @chart_ids        = map { /^field(-?\d+)/; $1 } @all_field_params;
+  my @chart_ids        = map  { /^field(-?\d+)/; $1 } @all_field_params;
   @chart_ids = sort { $a <=> $b } uniq @chart_ids;
 
   my $clause = new Bugzilla::Search::Clause();
   foreach my $chart_id (@chart_ids) {
     my @all_and = grep {/^field$chart_id-\d+/} @param_list;
-    my @and_ids = map { /^field$chart_id-(\d+)/; $1 } @all_and;
+    my @and_ids = map  { /^field$chart_id-(\d+)/; $1 } @all_and;
     @and_ids = sort { $a <=> $b } uniq @and_ids;
 
     my $and_clause = new Bugzilla::Search::Clause();
     foreach my $and_id (@and_ids) {
       my @all_or = grep {/^field$chart_id-$and_id-\d+/} @param_list;
-      my @or_ids = map { /^field$chart_id-$and_id-(\d+)/; $1 } @all_or;
+      my @or_ids = map  { /^field$chart_id-$and_id-(\d+)/; $1 } @all_or;
       @or_ids = sort { $a <=> $b } uniq @or_ids;
 
       my $or_clause = new Bugzilla::Search::Clause('OR');
@@ -1776,7 +1777,7 @@ sub _field_ids {
   my @param_list = keys %$params;
 
   my @field_params = grep {/^f\d+$/} @param_list;
-  my @field_ids    = map { /(\d+)/; $1 } @field_params;
+  my @field_ids    = map  { /(\d+)/; $1 } @field_params;
   @field_ids = sort { $a <=> $b } @field_ids;
   return @field_ids;
 }
@@ -1790,26 +1791,19 @@ sub _handle_chart {
   $field = FIELD_MAP->{$field} || $field;
 
   my ($string_value, $orig_value);
-  state $is_mysql = $dbh->isa('Bugzilla::DB::Mysql') ? 1 : 0;
 
   if (ref $value eq 'ARRAY') {
 
     # Trim input and ignore blank values.
-    @$value = map { trim($_) } @$value;
+    @$value = map  { trim($_) } @$value;
     @$value = grep { defined $_ and $_ ne '' } @$value;
     return if !@$value;
-    $orig_value = join(',', @$value);
-    if ($field eq 'longdesc' && $is_mysql) {
-      @$value = map { _convert_unicode_characters($_) } @$value;
-    }
+    $orig_value   = join(',', @$value);
     $string_value = join(',', @$value);
   }
   else {
     return if $value eq '';
-    $orig_value = $value;
-    if ($field eq 'longdesc' && $is_mysql) {
-      $value = _convert_unicode_characters($value);
-    }
+    $orig_value   = $value;
     $string_value = $value;
   }
 
@@ -1870,19 +1864,6 @@ sub _handle_chart {
   $condition->translated(\%search_args);
 }
 
-# XXX - This is a hack for MySQL which doesn't understand Unicode characters
-# above U+FFFF, see Bugzilla::Comment::_check_thetext(). This hack can go away
-# once we require MySQL 5.5.3 and use utf8mb4.
-sub _convert_unicode_characters {
-  my $string = shift;
-
-  # Perl 5.13.8 and older complain about non-characters.
-  no warnings 'utf8';
-  $string
-    =~ s/([\x{10000}-\x{10FFFF}])/"\x{FDD0}[" . uc(sprintf('U+%04x', ord($1))) . "]\x{FDD1}"/eg;
-  return $string;
-}
-
 ##################################
 # do_search_function And Helpers #
 ##################################
@@ -2957,7 +2938,7 @@ sub _multiselect_table {
   }
   elsif ($field eq 'bug_group') {
     $args->{full_field} = 'groups.name';
-    return "bug_group_map INNER JOIN groups
+    return "bug_group_map INNER JOIN " . $dbh->quote_identifier('groups') . "
                                       ON bug_group_map.group_id = groups.id";
   }
   elsif ($field eq 'blocked' or $field eq 'dependson') {
@@ -3027,7 +3008,7 @@ sub _multiselect_isempty {
     = @$args{qw(field operator joins chart_id)};
   my $dbh = Bugzilla->dbh;
   $operator = $self->_reverse_operator($operator) if $not;
-  $not = $operator eq 'isnotempty' ? 'NOT' : '';
+  $not      = $operator eq 'isnotempty' ? 'NOT' : '';
 
   if ($field eq 'keywords') {
     push @$joins,
diff --git a/Bugzilla/Search/Clause.pm b/Bugzilla/Search/Clause.pm
index 940f88ff3..1f61cd680 100644
--- a/Bugzilla/Search/Clause.pm
+++ b/Bugzilla/Search/Clause.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::Search::Clause;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/Bugzilla/Search/ClauseGroup.pm b/Bugzilla/Search/ClauseGroup.pm
index 5c7791734..548da8a7c 100644
--- a/Bugzilla/Search/ClauseGroup.pm
+++ b/Bugzilla/Search/ClauseGroup.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::Search::ClauseGroup;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Bugzilla::Search::Clause);
+use base qw(Bugzilla::Search::Clause);
 
 use Bugzilla::Error;
 use Bugzilla::Search::Condition qw(condition);
diff --git a/Bugzilla/Search/Condition.pm b/Bugzilla/Search/Condition.pm
index 9ddb6a898..39be9f654 100644
--- a/Bugzilla/Search/Condition.pm
+++ b/Bugzilla/Search/Condition.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::Search::Condition;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Exporter);
+use base qw(Exporter);
 our @EXPORT_OK = qw(condition);
 
 sub new {
diff --git a/Bugzilla/Search/Quicksearch.pm b/Bugzilla/Search/Quicksearch.pm
index 85cb2770f..13f35c3f0 100644
--- a/Bugzilla/Search/Quicksearch.pm
+++ b/Bugzilla/Search/Quicksearch.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::Search::Quicksearch;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
@@ -22,7 +22,7 @@ use List::Util qw(min max);
 use List::MoreUtils qw(firstidx);
 use Text::ParseWords qw(parse_line);
 
-use parent qw(Exporter);
+use base qw(Exporter);
 @Bugzilla::Search::Quicksearch::EXPORT = qw(quicksearch);
 
 # Custom mappings for some fields.
@@ -73,7 +73,7 @@ sub FIELD_MAP {
   # Get all the fields whose names don't contain periods. (Fields that
   # contain periods are always handled in MAPPINGS.)
   my @db_fields = grep { $_->name !~ /\./ } @{Bugzilla->fields({obsolete => 0})};
-  my %full_map = (%{MAPPINGS()}, map { $_->name => $_->name } @db_fields);
+  my %full_map  = (%{MAPPINGS()}, map { $_->name => $_->name } @db_fields);
 
   # Eliminate the fields that start with bug_ or rep_, because those are
   # handled by the MAPPINGS instead, and we don't want too many names
@@ -86,9 +86,9 @@ sub FIELD_MAP {
   # (both because it's unnecessary and because otherwise
   # "reporter_accessible" and "reporter" both match "rep".
   delete @full_map{qw(rep_platform bug_status bug_file_loc bug_group
-      bug_severity bug_status
-      status_whiteboard
-      cclist_accessible reporter_accessible)};
+    bug_severity bug_status
+    status_whiteboard
+  cclist_accessible reporter_accessible)};
 
   Bugzilla::Hook::process('quicksearch_map', {'map' => \%full_map});
 
diff --git a/Bugzilla/Search/Recent.pm b/Bugzilla/Search/Recent.pm
index 5c12db156..e6dd15a36 100644
--- a/Bugzilla/Search/Recent.pm
+++ b/Bugzilla/Search/Recent.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::Search::Recent;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Bugzilla::Object);
+use base qw(Bugzilla::Object);
 
 use Bugzilla::Constants;
 use Bugzilla::Error;
diff --git a/Bugzilla/Search/Saved.pm b/Bugzilla/Search/Saved.pm
index c988e8997..20cd93804 100644
--- a/Bugzilla/Search/Saved.pm
+++ b/Bugzilla/Search/Saved.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::Search::Saved;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Bugzilla::Object);
+use base qw(Bugzilla::Object);
 
 use Bugzilla::CGI;
 use Bugzilla::Constants;
diff --git a/Bugzilla/Sender/Transport/Sendmail.pm b/Bugzilla/Sender/Transport/Sendmail.pm
index d2be2cded..fdc71729c 100644
--- a/Bugzilla/Sender/Transport/Sendmail.pm
+++ b/Bugzilla/Sender/Transport/Sendmail.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::Sender::Transport::Sendmail;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Email::Sender::Transport::Sendmail);
+use base qw(Email::Sender::Transport::Sendmail);
 
 use Email::Sender::Failure;
 
diff --git a/Bugzilla/Series.pm b/Bugzilla/Series.pm
index ec0a16e34..d716ad6e5 100644
--- a/Bugzilla/Series.pm
+++ b/Bugzilla/Series.pm
@@ -14,7 +14,7 @@
 
 package Bugzilla::Series;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/Bugzilla/Status.pm b/Bugzilla/Status.pm
index 615c7b250..21e7bcaa0 100644
--- a/Bugzilla/Status.pm
+++ b/Bugzilla/Status.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::Status;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
@@ -15,7 +15,7 @@ use warnings;
 # ChoiceInterface, because a bug status literally is a special type
 # of Field::Choice, not just an object that happens to have the same
 # methods.
-use parent qw(Bugzilla::Field::Choice Exporter);
+use base qw(Bugzilla::Field::Choice Exporter);
 @Bugzilla::Status::EXPORT = qw(
   BUG_STATE_OPEN
   SPECIAL_STATUS_WORKFLOW_ACTIONS
diff --git a/Bugzilla/Template.pm b/Bugzilla/Template.pm
index ea319b4f0..8a65f35aa 100644
--- a/Bugzilla/Template.pm
+++ b/Bugzilla/Template.pm
@@ -8,7 +8,7 @@
 
 package Bugzilla::Template;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
@@ -37,7 +37,7 @@ use IO::Dir;
 use List::MoreUtils qw(firstidx);
 use Scalar::Util qw(blessed);
 
-use parent qw(Template);
+use base qw(Template);
 
 use constant FORMAT_TRIPLE => '%19s|%-28s|%-28s';
 use constant FORMAT_3_SIZE => [19, 28, 28];
diff --git a/Bugzilla/Template/Context.pm b/Bugzilla/Template/Context.pm
index 01c8c5981..90dbbda88 100644
--- a/Bugzilla/Template/Context.pm
+++ b/Bugzilla/Template/Context.pm
@@ -8,11 +8,11 @@
 # This exists to implement the template-before_process hook.
 package Bugzilla::Template::Context;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Template::Context);
+use base qw(Template::Context);
 
 use Bugzilla::Hook;
 use Scalar::Util qw(blessed);
diff --git a/Bugzilla/Template/Plugin/Bugzilla.pm b/Bugzilla/Template/Plugin/Bugzilla.pm
index 0734fb942..abe542ccd 100644
--- a/Bugzilla/Template/Plugin/Bugzilla.pm
+++ b/Bugzilla/Template/Plugin/Bugzilla.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::Template::Plugin::Bugzilla;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Template::Plugin);
+use base qw(Template::Plugin);
 
 use Bugzilla;
 
diff --git a/Bugzilla/Template/Plugin/Hook.pm b/Bugzilla/Template/Plugin/Hook.pm
index c57db4223..e315ab79e 100644
--- a/Bugzilla/Template/Plugin/Hook.pm
+++ b/Bugzilla/Template/Plugin/Hook.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::Template::Plugin::Hook;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Template::Plugin);
+use base qw(Template::Plugin);
 
 use Bugzilla::Constants;
 use Bugzilla::Install::Util qw(template_include_path);
@@ -49,7 +49,7 @@ sub process {
   # Get the hooks out of the cache if they exist. Otherwise, read them
   # from the disk.
   my $cache = Bugzilla->request_cache->{template_plugin_hook_cache} ||= {};
-  my $lang = $context->{bz_language} || '';
+  my $lang  = $context->{bz_language} || '';
   $cache->{"${lang}__$extension_template"}
     ||= $self->_get_hooks($extension_template);
 
diff --git a/Bugzilla/Token.pm b/Bugzilla/Token.pm
index 62106c5e5..66aaf8425 100644
--- a/Bugzilla/Token.pm
+++ b/Bugzilla/Token.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::Token;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
@@ -22,7 +22,7 @@ use Date::Parse;
 use File::Basename;
 use Digest::SHA qw(hmac_sha256_base64);
 
-use parent qw(Exporter);
+use base qw(Exporter);
 
 @Bugzilla::Token::EXPORT = qw(issue_api_token issue_session_token
   check_token_data delete_token
@@ -237,7 +237,7 @@ sub check_hash_token {
     $vars->{'script_name'} = basename($0);
     $vars->{'token'}       = issue_hash_token($data);
     $vars->{'reason'}
-      = (!$token) ? 'missing_token'
+      = (!$token)                   ? 'missing_token'
       : ($expected_token ne $token) ? 'invalid_token'
       :                               'expired_token';
     print Bugzilla->cgi->header();
diff --git a/Bugzilla/Update.pm b/Bugzilla/Update.pm
index 9f9288162..239a2fd92 100644
--- a/Bugzilla/Update.pm
+++ b/Bugzilla/Update.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::Update;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
@@ -52,7 +52,8 @@ sub get_notifications {
       'latest_ver' => $branch->{'att'}->{'vid'},
       'status'     => $branch->{'att'}->{'status'},
       'url'        => $branch->{'att'}->{'url'},
-      'date'       => $branch->{'att'}->{'date'}
+      'date'       => $branch->{'att'}->{'date'},
+      'eos_date'   => exists($branch->{'att'}->{'eos-date'}) ? $branch->{'att'}->{'eos-date'} : undef,
     };
     push(@releases, $release);
   }
@@ -72,6 +73,35 @@ sub get_notifications {
     }
   }
   elsif (Bugzilla->params->{'upgrade_notification'} eq 'latest_stable_release') {
+    # We want the latest stable version for the current branch.
+    # If we are running a development snapshot, we won't match anything.
+    my $branch_version = $current_version[0] . '.' . $current_version[1];
+
+    # We do a string comparison instead of a numerical one, because
+    # e.g. 2.2 == 2.20, but 2.2 ne 2.20 (and 2.2 is indeed much older).
+    @release = grep { $_->{'branch_ver'} eq $branch_version } @releases;
+
+    # If the branch has an end-of-support date listed, we should
+    # strongly suggest to upgrade to the latest stable release
+    # available.
+    if (scalar(@release) && $release[0]->{'status'} ne 'closed'
+      && defined($release[0]->{'eos_date'})) {
+      my $eos_date = $release[0]->{'eos_date'};
+      @release = grep {$_->{'status'} eq 'stable'} @releases;
+      return {'data' => $release[0],
+              'branch_version' => $branch_version,
+              'eos_date' => $eos_date}
+    };
+
+    # If the branch is now closed, we should strongly suggest
+    # to upgrade to the latest stable release available.
+    if (scalar(@release) && $release[0]->{'status'} eq 'closed') {
+      @release = grep { $_->{'status'} eq 'stable' } @releases;
+      return {'data' => $release[0], 'deprecated' => $branch_version};
+    }
+
+    # If we get here, then we want to recommend the lastest stable
+    # release without any other messages.
     @release = grep { $_->{'status'} eq 'stable' } @releases;
   }
   elsif (Bugzilla->params->{'upgrade_notification'} eq 'stable_branch_release') {
@@ -84,6 +114,18 @@ sub get_notifications {
     # e.g. 2.2 == 2.20, but 2.2 ne 2.20 (and 2.2 is indeed much older).
     @release = grep { $_->{'branch_ver'} eq $branch_version } @releases;
 
+    # If the branch has an end-of-support date listed, we should
+    # strongly suggest to upgrade to the latest stable release
+    # available.
+    if (scalar(@release) && $release[0]->{'status'} ne 'closed'
+      && defined($release[0]->{'eos_date'})) {
+      my $eos_date = $release[0]->{'eos_date'};
+      @release = grep {$_->{'status'} eq 'stable'} @releases;
+      return {'data' => $release[0],
+              'branch_version' => $branch_version,
+              'eos_date' => $eos_date}
+    };
+
     # If the branch is now closed, we should strongly suggest
     # to upgrade to the latest stable release available.
     if (scalar(@release) && $release[0]->{'status'} eq 'closed') {
diff --git a/Bugzilla/User.pm b/Bugzilla/User.pm
index 5426cc8fa..63faac032 100644
--- a/Bugzilla/User.pm
+++ b/Bugzilla/User.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::User;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
@@ -31,7 +31,7 @@ use Storable qw(dclone);
 use URI;
 use URI::QueryParam;
 
-use parent qw(Bugzilla::Object Exporter);
+use base qw(Bugzilla::Object Exporter);
 @Bugzilla::User::EXPORT = qw(is_available_username
   login_to_id validate_password validate_password_check
   USER_MATCH_MULTIPLE USER_MATCH_FAILED USER_MATCH_SUCCESS
@@ -131,7 +131,19 @@ sub new {
       $_[0] = $param;
     }
   }
-  return $class->SUPER::new(@_);
+
+  $user = $class->SUPER::new(@_);
+
+  # MySQL considers some non-ascii characters such as umlauts to equal
+  # ascii characters returning a user when it should not.
+  if ($user && ref $param eq 'HASH' && exists $param->{name}) {
+    my $login = $param->{name};
+    if (lc $login ne lc $user->login) {
+      $user = undef;
+    }
+  }
+
+  return $user;
 }
 
 sub super_user {
@@ -331,7 +343,7 @@ sub set_password { $_[0]->set('cryptpassword', $_[1]); }
 
 sub set_disabledtext {
   $_[0]->set('disabledtext', $_[1]);
-  $_[0]->set('is_enabled', $_[1] ? 0 : 1);
+  $_[0]->set('is_enabled',   $_[1] ? 0 : 1);
 }
 
 sub set_groups {
@@ -425,7 +437,7 @@ sub _set_groups_to_object {
     # Go through the array, and turn items into group objects
     my @groups = ();
     foreach my $value (@{$changes->{$key}}) {
-      my $type = $value =~ /^\d+$/ ? 'id' : 'name';
+      my $type  = $value =~ /^\d+$/ ? 'id' : 'name';
       my $group = Bugzilla::Group->new({$type => $value});
 
       if (!$group || !$user->can_bless($group->id)) {
@@ -554,7 +566,7 @@ sub queries_subscribed {
   return [] unless $self->id;
 
   # Exclude the user's own queries.
-  my @my_query_ids = map($_->id, @{$self->queries});
+  my @my_query_ids    = map($_->id, @{$self->queries});
   my $query_id_string = join(',', @my_query_ids) || '-1';
 
   # Only show subscriptions that we can still actually see. If a
@@ -581,7 +593,7 @@ sub queries_available {
   return [] unless $self->id;
 
   # Exclude the user's own queries.
-  my @my_query_ids = map($_->id, @{$self->queries});
+  my @my_query_ids    = map($_->id, @{$self->queries});
   my $query_id_string = join(',', @my_query_ids) || '-1';
 
   my $avail_query_ids = Bugzilla->dbh->selectcol_arrayref(
@@ -1630,7 +1642,7 @@ sub visible_groups_direct {
   }
   else {
     # All groups are visible if usevisibilitygroups is off.
-    $sth = $dbh->prepare('SELECT id FROM groups');
+    $sth = $dbh->prepare('SELECT id FROM ' . $dbh->quote_identifier('groups'));
   }
   $sth->execute();
 
@@ -1694,7 +1706,7 @@ sub derive_regexp_groups {
 
   $sth = $dbh->prepare(
     "SELECT id, userregexp, user_group_map.group_id
-                            FROM groups
+                            FROM " . $dbh->quote_identifier('groups') . "
                        LEFT JOIN user_group_map
                               ON groups.id = user_group_map.group_id
                              AND user_group_map.user_id = ?
diff --git a/Bugzilla/User/APIKey.pm b/Bugzilla/User/APIKey.pm
index d2e337c5e..7b7699dee 100644
--- a/Bugzilla/User/APIKey.pm
+++ b/Bugzilla/User/APIKey.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::User::APIKey;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Bugzilla::Object);
+use base qw(Bugzilla::Object);
 
 use Bugzilla::User;
 use Bugzilla::Util qw(generate_random_password trim);
@@ -75,7 +75,7 @@ sub set_description { $_[0]->set('description', $_[1]); }
 sub set_revoked     { $_[0]->set('revoked',     $_[1]); }
 
 # Validators
-sub _check_api_key { return generate_random_password(40); }
+sub _check_api_key     { return generate_random_password(40); }
 sub _check_description { return trim($_[1]) || ''; }
 1;
 
diff --git a/Bugzilla/User/Setting.pm b/Bugzilla/User/Setting.pm
index 67f5c5684..1b2ecf25c 100644
--- a/Bugzilla/User/Setting.pm
+++ b/Bugzilla/User/Setting.pm
@@ -8,11 +8,11 @@
 
 package Bugzilla::User::Setting;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Exporter);
+use base qw(Exporter);
 
 
 # Module stuff
diff --git a/Bugzilla/User/Setting/Lang.pm b/Bugzilla/User/Setting/Lang.pm
index d1aeb3421..b8aea7e09 100644
--- a/Bugzilla/User/Setting/Lang.pm
+++ b/Bugzilla/User/Setting/Lang.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::User::Setting::Lang;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Bugzilla::User::Setting);
+use base qw(Bugzilla::User::Setting);
 
 use Bugzilla::Constants;
 
diff --git a/Bugzilla/User/Setting/Skin.pm b/Bugzilla/User/Setting/Skin.pm
index 0447b02ab..73e3a2c53 100644
--- a/Bugzilla/User/Setting/Skin.pm
+++ b/Bugzilla/User/Setting/Skin.pm
@@ -8,11 +8,11 @@
 
 package Bugzilla::User::Setting::Skin;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Bugzilla::User::Setting);
+use base qw(Bugzilla::User::Setting);
 
 use Bugzilla::Constants;
 use File::Spec::Functions;
diff --git a/Bugzilla/User/Setting/Timezone.pm b/Bugzilla/User/Setting/Timezone.pm
index b6b2503b5..a4069da8a 100644
--- a/Bugzilla/User/Setting/Timezone.pm
+++ b/Bugzilla/User/Setting/Timezone.pm
@@ -7,13 +7,13 @@
 
 package Bugzilla::User::Setting::Timezone;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
 use DateTime::TimeZone;
 
-use parent qw(Bugzilla::User::Setting);
+use base qw(Bugzilla::User::Setting);
 
 use Bugzilla::Constants;
 
diff --git a/Bugzilla/UserAgent.pm b/Bugzilla/UserAgent.pm
index 1995cc82f..aad0cae67 100644
--- a/Bugzilla/UserAgent.pm
+++ b/Bugzilla/UserAgent.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::UserAgent;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Exporter);
+use base qw(Exporter);
 our @EXPORT = qw(detect_platform detect_op_sys);
 
 use Bugzilla::Field;
diff --git a/Bugzilla/Util.pm b/Bugzilla/Util.pm
index 572479777..7e65e35c9 100644
--- a/Bugzilla/Util.pm
+++ b/Bugzilla/Util.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::Util;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Exporter);
+use base qw(Exporter);
 @Bugzilla::Util::EXPORT = qw(trick_taint detaint_natural detaint_signed
   html_quote url_quote xml_quote
   css_class_quote html_light_quote
@@ -33,7 +33,7 @@ use Bugzilla::Error;
 use Date::Parse;
 use Date::Format;
 use Digest;
-use Email::Address;
+use Email::Address::XS;
 use List::Util qw(first);
 use Scalar::Util qw(tainted blessed);
 use Text::Wrap;
@@ -223,13 +223,19 @@ sub html_light_quote {
 sub email_filter {
   my ($toencode) = @_;
   if (!Bugzilla->user->id) {
-    my @emails = Email::Address->parse($toencode);
-    if (scalar @emails) {
-      my @hosts    = map { quotemeta($_->host) } @emails;
-      my $hosts_re = join('|', @hosts);
-      $toencode =~ s/\@(?:$hosts_re)//g;
-      return $toencode;
+    my $email_re = qr/([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/;
+    my @hosts;
+    while ($toencode =~ /$email_re/g) {
+      my @emails = Email::Address::XS->parse($1);
+      if (scalar @emails) {
+        my @these_hosts = map { quotemeta($_->host) } @emails;
+        push @hosts, @these_hosts;
+      }
     }
+    my $hosts_re = join('|', @hosts);
+
+    $toencode =~ s/\@(?:$hosts_re)//g;
+    return $toencode;
   }
   return $toencode;
 }
@@ -740,17 +746,15 @@ sub validate_email_syntax {
   my $match  = Bugzilla->params->{'emailregexp'};
   my $email  = $addr . Bugzilla->params->{'emailsuffix'};
 
-  # This regexp follows RFC 2822 section 3.4.1.
-  my $addr_spec = $Email::Address::addr_spec;
-
   # RFC 2822 section 2.1 specifies that email addresses must
   # be made of US-ASCII characters only.
   # Email::Address::addr_spec doesn't enforce this.
   # We set the max length to 127 to ensure addresses aren't truncated when
   # inserted into the tokens.eventdata field.
+  my $address = Email::Address::XS->parse_bare_address($email);
   if ( $addr =~ /$match/
+    && $address->is_valid
     && $email !~ /\P{ASCII}/
-    && $email =~ /^$addr_spec$/
     && length($email) <= 127)
   {
     # We assume these checks to suffice to consider the address untainted.
@@ -1313,18 +1317,44 @@ if Bugzilla is currently using the shadowdb or not. Used like:
 
 =back
 
-=head1 B<Methods in need of POD>
-
 =over
 
-=item do_ssl_redirect_if_required
+=item C<write_text($filename, $content)>
+
+Writes $content to $filename. The content will be encoded as UTF-8. Returns 1 if
+the atomic write was successful, 0 otherwise. C<$!> will be set to the error
+from C<rename()>.
+
+=item C<read_text($filename)>
+
+Reads the contents of $filename and returns it as a string. The string will be
+decoded as UTF-8.
+
+=item C<is_ipv4>
+
+Returns true if the given IP address is an IPv4 address.
+
+=item C<is_ipv6>
+
+Returns true if the given IP address is an IPv6 address.
+
+=item C<do_ssl_redirect_if_required>
+
+If Bugzilla is configured to redirect all HTTP requests to HTTPS, this function
+will redirect the user to the HTTPS version of the current page. It will not do
+anything if the user is already on HTTPS, or if there is no C<sslbase> parameter
+set.
 
-=item validate_time
+=item C<validate_time>
 
-=item is_ipv4
+Validates a time string. Returns true or false depending on whether the time
+string is valid.
 
-=item is_ipv6
+=item C<display_value>
 
-=item display_value
+Returns the display value for a given field and value. This value comes from the
+value_descs template variable. The value_descs variable is set in the template
+file C<global/value-descs.none.tmpl>. This is used for localizing Bugzilla to
+other languages.
 
 =back
diff --git a/Bugzilla/Version.pm b/Bugzilla/Version.pm
index dfc5a8e0f..a96030265 100644
--- a/Bugzilla/Version.pm
+++ b/Bugzilla/Version.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::Version;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Bugzilla::Object Exporter);
+use base qw(Bugzilla::Object Exporter);
 
 @Bugzilla::Version::EXPORT = qw(vers_cmp);
 
diff --git a/Bugzilla/WebService.pm b/Bugzilla/WebService.pm
index 2630e3565..16bc86e61 100644
--- a/Bugzilla/WebService.pm
+++ b/Bugzilla/WebService.pm
@@ -9,7 +9,7 @@
 # actual RPC server, see Bugzilla::WebService::Server and its subclasses.
 package Bugzilla::WebService;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/Bugzilla/WebService/Bug.pm b/Bugzilla/WebService/Bug.pm
index 5852928c1..b4834d964 100644
--- a/Bugzilla/WebService/Bug.pm
+++ b/Bugzilla/WebService/Bug.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::WebService::Bug;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Bugzilla::WebService);
+use base qw(Bugzilla::WebService);
 
 use Bugzilla::Comment;
 use Bugzilla::Comment::TagWeights;
@@ -150,14 +150,14 @@ sub fields {
     }
 
     my %field_data = (
-      id               => $self->type('int',     $field->id),
-      type             => $self->type('int',     $field->type),
-      is_custom        => $self->type('boolean', $field->custom),
-      name             => $self->type('string',  $field->name),
-      display_name     => $self->type('string',  $field->description),
-      is_mandatory     => $self->type('boolean', $field->is_mandatory),
-      is_on_bug_entry  => $self->type('boolean', $field->enter_bug),
-      visibility_field => $self->type('string',  $visibility_field),
+      id                => $self->type('int',           $field->id),
+      type              => $self->type('int',           $field->type),
+      is_custom         => $self->type('boolean',       $field->custom),
+      name              => $self->type('string',        $field->name),
+      display_name      => $self->type('string',        $field->description),
+      is_mandatory      => $self->type('boolean',       $field->is_mandatory),
+      is_on_bug_entry   => $self->type('boolean',       $field->enter_bug),
+      visibility_field  => $self->type('string',        $visibility_field),
       visibility_values => [map { $self->type('string', $_->name) } @$vis_values],
     );
     if ($has_values) {
@@ -527,7 +527,7 @@ sub search {
   my %options = (fields => ['bug_id']);
 
   # Find the highest custom field id
-  my @field_ids = grep(/^f(\d+)$/, keys %$match_params);
+  my @field_ids     = grep(/^f(\d+)$/, keys %$match_params);
   my $last_field_id = @field_ids ? max @field_ids + 1 : 1;
 
   # Do special search types for certain fields.
diff --git a/Bugzilla/WebService/BugUserLastVisit.pm b/Bugzilla/WebService/BugUserLastVisit.pm
index 128507376..240191383 100644
--- a/Bugzilla/WebService/BugUserLastVisit.pm
+++ b/Bugzilla/WebService/BugUserLastVisit.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::WebService::BugUserLastVisit;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Bugzilla::WebService);
+use base qw(Bugzilla::WebService);
 
 use Bugzilla::Bug;
 use Bugzilla::Error;
diff --git a/Bugzilla/WebService/Bugzilla.pm b/Bugzilla/WebService/Bugzilla.pm
index f93892068..721e9d0cb 100644
--- a/Bugzilla/WebService/Bugzilla.pm
+++ b/Bugzilla/WebService/Bugzilla.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::WebService::Bugzilla;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Bugzilla::WebService);
+use base qw(Bugzilla::WebService);
 use Bugzilla::Constants;
 use Bugzilla::Util qw(datetime_from);
 use Bugzilla::WebService::Util qw(validate filter_wants);
diff --git a/Bugzilla/WebService/Classification.pm b/Bugzilla/WebService/Classification.pm
index ab539b339..be7991e95 100644
--- a/Bugzilla/WebService/Classification.pm
+++ b/Bugzilla/WebService/Classification.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::WebService::Classification;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw (Bugzilla::WebService);
+use base qw (Bugzilla::WebService);
 
 use Bugzilla::Classification;
 use Bugzilla::Error;
@@ -70,11 +70,11 @@ sub _classification_to_hash {
 
   return filter $params,
     {
-    id          => $self->type('int',    $classification->id),
-    name        => $self->type('string', $classification->name),
-    description => $self->type('string', $classification->description),
-    sort_key    => $self->type('int',    $classification->sortkey),
-    products => [map { $self->_product_to_hash($_, $params) } @$products],
+    id          => $self->type('int',                 $classification->id),
+    name        => $self->type('string',              $classification->name),
+    description => $self->type('string',              $classification->description),
+    sort_key    => $self->type('int',                 $classification->sortkey),
+    products    => [map { $self->_product_to_hash($_, $params) } @$products],
     };
 }
 
diff --git a/Bugzilla/WebService/Component.pm b/Bugzilla/WebService/Component.pm
index 802f40c73..af74d599a 100644
--- a/Bugzilla/WebService/Component.pm
+++ b/Bugzilla/WebService/Component.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::WebService::Component;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/Bugzilla/WebService/Constants.pm b/Bugzilla/WebService/Constants.pm
index a2f83f528..c2736e00a 100644
--- a/Bugzilla/WebService/Constants.pm
+++ b/Bugzilla/WebService/Constants.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::WebService::Constants;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Exporter);
+use base qw(Exporter);
 
 our @EXPORT = qw(
   WS_ERROR_CODE
diff --git a/Bugzilla/WebService/FlagType.pm b/Bugzilla/WebService/FlagType.pm
index 9dc240c7f..99767eeac 100644
--- a/Bugzilla/WebService/FlagType.pm
+++ b/Bugzilla/WebService/FlagType.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::WebService::FlagType;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Bugzilla::WebService);
+use base qw(Bugzilla::WebService);
 use Bugzilla::Component;
 use Bugzilla::Constants;
 use Bugzilla::Error;
diff --git a/Bugzilla/WebService/Group.pm b/Bugzilla/WebService/Group.pm
index c92583d0b..fc7aed87b 100644
--- a/Bugzilla/WebService/Group.pm
+++ b/Bugzilla/WebService/Group.pm
@@ -7,15 +7,19 @@
 
 package Bugzilla::WebService::Group;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Bugzilla::WebService);
+use base qw(Bugzilla::WebService);
 use Bugzilla::Constants;
 use Bugzilla::Error;
 use Bugzilla::WebService::Util qw(validate translate params_to_objects);
 
+use constant READ_ONLY => qw(
+  get
+);
+
 use constant PUBLIC_METHODS => qw(
   create
   get
diff --git a/Bugzilla/WebService/Product.pm b/Bugzilla/WebService/Product.pm
index a7980172e..eecad472d 100644
--- a/Bugzilla/WebService/Product.pm
+++ b/Bugzilla/WebService/Product.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::WebService::Product;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Bugzilla::WebService);
+use base qw(Bugzilla::WebService);
 use Bugzilla::Product;
 use Bugzilla::User;
 use Bugzilla::Error;
diff --git a/Bugzilla/WebService/Server.pm b/Bugzilla/WebService/Server.pm
index f2844cdcb..190c3d569 100644
--- a/Bugzilla/WebService/Server.pm
+++ b/Bugzilla/WebService/Server.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::WebService::Server;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/Bugzilla/WebService/Server/JSONRPC.pm b/Bugzilla/WebService/Server/JSONRPC.pm
index 7847029ba..f8f5c9e1d 100644
--- a/Bugzilla/WebService/Server/JSONRPC.pm
+++ b/Bugzilla/WebService/Server/JSONRPC.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::WebService::Server::JSONRPC;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/Bugzilla/WebService/Server/REST.pm b/Bugzilla/WebService/Server/REST.pm
index 143b938b6..2ccc3bbc1 100644
--- a/Bugzilla/WebService/Server/REST.pm
+++ b/Bugzilla/WebService/Server/REST.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::WebService::Server::REST;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Bugzilla::WebService::Server::JSONRPC);
+use base qw(Bugzilla::WebService::Server::JSONRPC);
 
 use Bugzilla::Constants;
 use Bugzilla::Error;
diff --git a/Bugzilla/WebService/Server/REST/Resources/Bug.pm b/Bugzilla/WebService/Server/REST/Resources/Bug.pm
index 5cc25f432..01a75f5ac 100644
--- a/Bugzilla/WebService/Server/REST/Resources/Bug.pm
+++ b/Bugzilla/WebService/Server/REST/Resources/Bug.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::WebService::Server::REST::Resources::Bug;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/Bugzilla/WebService/Server/REST/Resources/BugUserLastVisit.pm b/Bugzilla/WebService/Server/REST/Resources/BugUserLastVisit.pm
index 806c3f9c7..4211edb19 100644
--- a/Bugzilla/WebService/Server/REST/Resources/BugUserLastVisit.pm
+++ b/Bugzilla/WebService/Server/REST/Resources/BugUserLastVisit.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::WebService::Server::REST::Resources::BugUserLastVisit;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/Bugzilla/WebService/Server/REST/Resources/Bugzilla.pm b/Bugzilla/WebService/Server/REST/Resources/Bugzilla.pm
index 072cfe2f6..0fd6a9838 100644
--- a/Bugzilla/WebService/Server/REST/Resources/Bugzilla.pm
+++ b/Bugzilla/WebService/Server/REST/Resources/Bugzilla.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::WebService::Server::REST::Resources::Bugzilla;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/Bugzilla/WebService/Server/REST/Resources/Classification.pm b/Bugzilla/WebService/Server/REST/Resources/Classification.pm
index ed65aea5c..f70e3cd5e 100644
--- a/Bugzilla/WebService/Server/REST/Resources/Classification.pm
+++ b/Bugzilla/WebService/Server/REST/Resources/Classification.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::WebService::Server::REST::Resources::Classification;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/Bugzilla/WebService/Server/REST/Resources/Component.pm b/Bugzilla/WebService/Server/REST/Resources/Component.pm
index 8870a0f04..d688b66b4 100644
--- a/Bugzilla/WebService/Server/REST/Resources/Component.pm
+++ b/Bugzilla/WebService/Server/REST/Resources/Component.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::WebService::Server::REST::Resources::Component;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/Bugzilla/WebService/Server/REST/Resources/FlagType.pm b/Bugzilla/WebService/Server/REST/Resources/FlagType.pm
index 438c8fb30..2bc4ce6e2 100644
--- a/Bugzilla/WebService/Server/REST/Resources/FlagType.pm
+++ b/Bugzilla/WebService/Server/REST/Resources/FlagType.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::WebService::Server::REST::Resources::FlagType;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/Bugzilla/WebService/Server/REST/Resources/Group.pm b/Bugzilla/WebService/Server/REST/Resources/Group.pm
index 7f607b7d1..d2e2604df 100644
--- a/Bugzilla/WebService/Server/REST/Resources/Group.pm
+++ b/Bugzilla/WebService/Server/REST/Resources/Group.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::WebService::Server::REST::Resources::Group;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/Bugzilla/WebService/Server/REST/Resources/Product.pm b/Bugzilla/WebService/Server/REST/Resources/Product.pm
index eabe19681..2df867e54 100644
--- a/Bugzilla/WebService/Server/REST/Resources/Product.pm
+++ b/Bugzilla/WebService/Server/REST/Resources/Product.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::WebService::Server::REST::Resources::Product;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/Bugzilla/WebService/Server/REST/Resources/User.pm b/Bugzilla/WebService/Server/REST/Resources/User.pm
index 4555b4dbc..6b817d88e 100644
--- a/Bugzilla/WebService/Server/REST/Resources/User.pm
+++ b/Bugzilla/WebService/Server/REST/Resources/User.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::WebService::Server::REST::Resources::User;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/Bugzilla/WebService/Server/XMLRPC.pm b/Bugzilla/WebService/Server/XMLRPC.pm
index f96f960da..7aeed36b1 100644
--- a/Bugzilla/WebService/Server/XMLRPC.pm
+++ b/Bugzilla/WebService/Server/XMLRPC.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::WebService::Server::XMLRPC;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
@@ -128,11 +128,11 @@ sub handle_login {
 # and also, in some cases, to more-usefully decode them.
 package Bugzilla::XMLRPC::Deserializer;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-# We can't use "use parent" because XMLRPC::Serializer doesn't return
+# We can't use "use base" because XMLRPC::Serializer doesn't return
 # a true value.
 use XMLRPC::Lite;
 our @ISA = qw(XMLRPC::Deserializer);
@@ -254,7 +254,7 @@ sub _validation_subs {
 
 package Bugzilla::XMLRPC::SOM;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
@@ -281,13 +281,13 @@ sub paramsin {
 # See http://rt.cpan.org/Public/Bug/Display.html?id=32952.
 package Bugzilla::XMLRPC::Serializer;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
 use Scalar::Util qw(blessed reftype);
 
-# We can't use "use parent" because XMLRPC::Serializer doesn't return
+# We can't use "use base" because XMLRPC::Serializer doesn't return
 # a true value.
 use XMLRPC::Lite;
 our @ISA = qw(XMLRPC::Serializer);
diff --git a/Bugzilla/WebService/User.pm b/Bugzilla/WebService/User.pm
index 591021831..b33ba1611 100644
--- a/Bugzilla/WebService/User.pm
+++ b/Bugzilla/WebService/User.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::WebService::User;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Bugzilla::WebService);
+use base qw(Bugzilla::WebService);
 
 use Bugzilla::Constants;
 use Bugzilla::Error;
diff --git a/Bugzilla/WebService/Util.pm b/Bugzilla/WebService/Util.pm
index 3e70921b3..807daa78c 100644
--- a/Bugzilla/WebService/Util.pm
+++ b/Bugzilla/WebService/Util.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::WebService::Util;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
@@ -18,7 +18,7 @@ use Bugzilla::Error;
 use Storable qw(dclone);
 use List::MoreUtils qw(any none);
 
-use parent qw(Exporter);
+use base qw(Exporter);
 
 # We have to "require", not "use" this, because otherwise it tries to
 # use features of Test::More during import().
diff --git a/Bugzilla/Whine.pm b/Bugzilla/Whine.pm
index 081933cba..d2d98a567 100644
--- a/Bugzilla/Whine.pm
+++ b/Bugzilla/Whine.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::Whine;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Bugzilla::Object);
+use base qw(Bugzilla::Object);
 
 use Bugzilla::Constants;
 use Bugzilla::Error;
diff --git a/Bugzilla/Whine/Query.pm b/Bugzilla/Whine/Query.pm
index 6648eab66..1a12acc79 100644
--- a/Bugzilla/Whine/Query.pm
+++ b/Bugzilla/Whine/Query.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::Whine::Query;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Bugzilla::Object);
+use base qw(Bugzilla::Object);
 
 use Bugzilla::Constants;
 use Bugzilla::Search::Saved;
diff --git a/Bugzilla/Whine/Schedule.pm b/Bugzilla/Whine/Schedule.pm
index 7517a3f26..1309d4bc3 100644
--- a/Bugzilla/Whine/Schedule.pm
+++ b/Bugzilla/Whine/Schedule.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::Whine::Schedule;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Bugzilla::Object);
+use base qw(Bugzilla::Object);
 
 use Bugzilla::Constants;
 
diff --git a/Build.PL b/Build.PL
index 024a56024..4d9d0c4fc 100644
--- a/Build.PL
+++ b/Build.PL
@@ -6,7 +6,7 @@
 # This Source Code Form is "Incompatible With Secondary Licenses", as
 # defined by the Mozilla Public License, v. 2.0.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
@@ -33,10 +33,10 @@ sub build_requires {
 
 sub recommends {
     my $recommends = OPTIONAL_MODULES();
-    my @blacklist = ('Apache-SizeLimit', 'mod_perl'); # Does not compile properly on Travis
+    my @blocklist = ('Apache-SizeLimit', 'mod_perl'); # Does not compile properly on Travis
     my $hrecommends = {};
     foreach my $module (@$recommends) {
-        next if grep($_ eq $module->{package}, @blacklist);
+        next if grep($_ eq $module->{package}, @blocklist);
         $hrecommends->{$module->{module}} = $module->{version};
     }
     return $hrecommends;
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 000000000..8b6deebd5
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,68 @@
+FROM ubuntu:22.04
+
+ENV DEBIAN_FRONTEND noninteractive
+
+RUN apt-get update && apt-get -y dist-upgrade
+RUN apt-get -y install \
+ apache2 \
+ mariadb-client \
+ netcat-traditional \
+ libappconfig-perl \
+ libdate-calc-perl \
+ libtemplate-perl \
+ build-essential \
+ libdatetime-timezone-perl \
+ libdatetime-perl \
+ libemail-address-perl \
+ libemail-sender-perl \
+ libemail-mime-perl \
+ libemail-mime-modifier-perl \
+ libdbi-perl \
+ libdbix-connector-perl \
+ libcgi-pm-perl \
+ liblocale-codes-perl \
+ libmath-random-isaac-perl \
+ libmath-random-isaac-xs-perl \
+ libapache2-mod-perl2 \
+ libapache2-mod-perl2-dev \
+ libchart-perl \
+ libxml-perl \
+ libxml-twig-perl \
+ perlmagick \
+ libgd-graph-perl \
+ libtemplate-plugin-gd-perl \
+ libsoap-lite-perl \
+ libhtml-scrubber-perl \
+ libjson-rpc-perl \
+ libdaemon-generic-perl \
+ libtheschwartz-perl \
+ libtest-taint-perl \
+ libauthen-radius-perl \
+ libfile-slurp-perl \
+ libencode-detect-perl \
+ libmodule-build-perl \
+ libnet-ldap-perl \
+ libauthen-sasl-perl \
+ libfile-mimeinfo-perl \
+ libhtml-formattext-withlinks-perl \
+ libgd-dev \
+ libmysqlclient-dev \
+ graphviz \
+ vim-common
+
+# Ubuntu22 doesn't ship new enough versions of a few modules, so get them from CPAN
+RUN cpan install Template::Toolkit Email::Address::XS Email::Sender DBD::MariaDB
+
+WORKDIR /var/www/html
+COPY --chown=root:www-data . /var/www/html
+COPY ./docker/000-default.conf /etc/apache2/sites-available/000-default.conf
+COPY ./docker /root/docker
+
+# we don't want Docker droppings accessible by the web browser since they
+# might contain setup info you don't want public
+RUN rm -rf /var/www/html/docker* /var/www/html/Dockerfile*
+RUN rm -rf /var/www/html/data /var/www/html/localconfig /var/www/html/index.html && \
+    mkdir /var/www/html/data
+RUN a2enmod expires && a2enmod headers && a2enmod rewrite && a2dismod mpm_event && a2enmod mpm_prefork
+EXPOSE 80/tcp
+CMD docker/startup.sh
diff --git a/Dockerfile.mariadb b/Dockerfile.mariadb
new file mode 100644
index 000000000..ee6993640
--- /dev/null
+++ b/Dockerfile.mariadb
@@ -0,0 +1,4 @@
+FROM mariadb:10.6
+COPY docker/my.cnf /etc/my.cnf
+COPY docker/mysql /etc/mysql/conf.d
+RUN apt update && apt -y dist-upgrade
diff --git a/README b/README
index e68afd8e4..b867a994d 100644
--- a/README
+++ b/README
@@ -6,9 +6,6 @@ developed by an active group of volunteers in the Mozilla community, and used
 by thousands of projects and companies around the world. It can be installed on
 Linux and other flavors of Unix, Windows or Mac OS X.
 
-You can try Bugzilla out using our testing installation:
-https://landfill.bugzilla.org/bugzilla-tip/
-
 Documentation
 =============
 
@@ -16,6 +13,15 @@ Bugzilla's comprehensive documentation, including installation instructions,
 can be found here:
 http://www.bugzilla.org/docs/
 
+Docker Quick Start
+==================
+
+If you have Docker installed and just want to take a quick look around Bugzilla
+you can cd into the bugzilla directory (same directory containing this README)
+and type `docker compose up`. The URL to access and the username and password
+for the default admin account will be shown on the console once it finishes
+setting it up.
+
 Reporting Bugs
 ==============
 
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 000000000..d64afa5fe
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,41 @@
+# Security Policy
+
+Current policy is to maintain two stable releases at all times. There
+will be three stable releases for 4 months after each major release,
+after which support for the oldest stable release will be dropped, 
+returning us to two stable releases.
+
+Right now we are in an "in-between" situation where a release was mistakenly
+made on the 5.0.x branch that should have been 5.2 but was instead 5.0.5.
+This caused us to fork the 5.0 branch after 5.0.4 to continue supporting
+that code base, and support for 5.0.6 will continue on the 5.2.x branch
+(5.2 will be considered a "point release" from 5.0.6, and 5.0.4.1 will
+be the "point release" from 5.0.4, whereas 5.0.5 is a "major release"
+from 5.0.4).
+
+## Supported Versions
+
+| Version             | Supported          | End of Support             |
+| ------------------- | ------------------ | -------------------------- |
+| 5.9.x (harmony/6.0) | :ballot_box_with_check: | 4 months after 6.4 release |
+| 5.1.x               | :ballot_box_with_check: | When 5.9.1 is released     |
+| 5.0.6 (5.2.x)       | :white_check_mark: | 4 months after 6.2 release |
+| 5.0.4.x             | :white_check_mark: | 4 months after 6.0 release |
+| 4.4.x               | :white_check_mark: | 4 months after 5.2 release |
+| < 4.4               | :x:                |                            |
+
+:ballot_box_with_check: = Development Branch
+
+:white_check_mark: = Security Supported Branch
+
+:x: = No longer supported
+
+## Reporting a Vulnerability
+
+Security vulnerabilities should be reported to
+[Bugzilla](https://bugzilla.mozilla.org/enter_bug.cgi?product=Bugzilla).
+There is a checkbox at the bottom of the submission form to indicate
+a security issue. Checking the box will hide the bug from the public
+and notify the security team. You should receive a response within a few
+days. Note that we are a very small volunteer team, so time to fix the
+problem may vary.
diff --git a/admin.cgi b/admin.cgi
index 2ba0cdc7f..f7e6d92cb 100755
--- a/admin.cgi
+++ b/admin.cgi
@@ -6,7 +6,7 @@
 # This Source Code Form is "Incompatible With Secondary Licenses", as
 # defined by the Mozilla Public License, v. 2.0.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/attachment.cgi b/attachment.cgi
index d404c85df..651dac6f0 100755
--- a/attachment.cgi
+++ b/attachment.cgi
@@ -6,7 +6,7 @@
 # This Source Code Form is "Incompatible With Secondary Licenses", as
 # defined by the Mozilla Public License, v. 2.0.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/buglist.cgi b/buglist.cgi
index 57b593930..df644f6d7 100755
--- a/buglist.cgi
+++ b/buglist.cgi
@@ -6,7 +6,7 @@
 # This Source Code Form is "Incompatible With Secondary Licenses", as
 # defined by the Mozilla Public License, v. 2.0.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/chart.cgi b/chart.cgi
index bfbbf300e..fb1fc9c78 100755
--- a/chart.cgi
+++ b/chart.cgi
@@ -27,7 +27,7 @@
 # Bonus:
 # Offer subscription when you get a "series already exists" error?
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
@@ -318,15 +318,11 @@ sub plot {
 
   my $format
     = $template->get_format("reports/chart", "", scalar($cgi->param('ctype')));
-  $format->{'ctype'} = 'text/html' if $cgi->param('debug');
 
   $cgi->set_dated_content_disp('inline', 'chart', $format->{extension});
   print $cgi->header($format->{'ctype'});
   disable_utf8() if ($format->{'ctype'} =~ /^image\//);
 
-  # Debugging PNGs is a pain; we need to be able to see the error messages
-  $vars->{'chart'}->dump() if $cgi->param('debug');
-
   $template->process($format->{'template'}, $vars)
     || ThrowTemplateError($template->error());
 }
@@ -362,10 +358,6 @@ sub view {
 
   print $cgi->header();
 
-  # If we have having problems with bad data, we can set debug=1 to dump
-  # the data structure.
-  $chart->dump() if $cgi->param('debug');
-
   $template->process("reports/create-chart.html.tmpl", $vars)
     || ThrowTemplateError($template->error());
 }
diff --git a/checksetup.pl b/checksetup.pl
index 857584937..0dbd95328 100755
--- a/checksetup.pl
+++ b/checksetup.pl
@@ -12,7 +12,7 @@
 # Initialization
 ######################################################################
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
@@ -119,6 +119,16 @@ Bugzilla::DB::bz_create_database() if $lc_hash->{'db_check'};
 # now get a handle to the database:
 my $dbh = Bugzilla->dbh;
 
+# We want to catch if the user is trying to "upgrade" from 5.1 because
+# that's actually a downgrade and you can't do that.
+my $bz51install;
+eval { $bz51install = $dbh->bz_index_info('bz_schema', 'bz_schema_version_idx'); };
+if ($bz51install) {
+  require Bugzilla::Error;
+  import Bugzilla::Error;
+  ThrowCodeError("bz51_attempted_upgrade");
+}
+
 # Create the tables, and do any database-specific schema changes.
 $dbh->bz_setup_database();
 
diff --git a/clean-bug-user-last-visit.pl b/clean-bug-user-last-visit.pl
index d9d9f83be..b2f2ee187 100755
--- a/clean-bug-user-last-visit.pl
+++ b/clean-bug-user-last-visit.pl
@@ -19,7 +19,7 @@ It takes no arguments and produces no output except in the case of errors.
 
 =cut
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 use lib qw(. lib);
diff --git a/colchange.cgi b/colchange.cgi
index c5f08b794..281cbfca2 100755
--- a/colchange.cgi
+++ b/colchange.cgi
@@ -6,7 +6,7 @@
 # This Source Code Form is "Incompatible With Secondary Licenses", as
 # defined by the Mozilla Public License, v. 2.0.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/collectstats.pl b/collectstats.pl
index f7a926499..47ec919cb 100755
--- a/collectstats.pl
+++ b/collectstats.pl
@@ -6,7 +6,7 @@
 # This Source Code Form is "Incompatible With Secondary Licenses", as
 # defined by the Mozilla Public License, v. 2.0.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/config.cgi b/config.cgi
index 3a939803d..33c10201f 100755
--- a/config.cgi
+++ b/config.cgi
@@ -6,7 +6,7 @@
 # This Source Code Form is "Incompatible With Secondary Licenses", as
 # defined by the Mozilla Public License, v. 2.0.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/contrib/Bugzilla.pm b/contrib/Bugzilla.pm
index e7452bbb8..6806b36eb 100644
--- a/contrib/Bugzilla.pm
+++ b/contrib/Bugzilla.pm
@@ -4,7 +4,7 @@
 
 package Bugzilla;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/contrib/bz_webservice_demo.pl b/contrib/bz_webservice_demo.pl
index bf6d504ad..c6fd77eeb 100755
--- a/contrib/bz_webservice_demo.pl
+++ b/contrib/bz_webservice_demo.pl
@@ -18,7 +18,7 @@ C<bz_webservice_demo.pl --help> for detailed help
 
 =cut
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/contrib/bzdbcopy.pl b/contrib/bzdbcopy.pl
index b92d9205d..d77acb93a 100755
--- a/contrib/bzdbcopy.pl
+++ b/contrib/bzdbcopy.pl
@@ -6,7 +6,7 @@
 # This Source Code Form is "Incompatible With Secondary Licenses", as
 # defined by the Mozilla Public License, v. 2.0.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/contrib/convert-workflow.pl b/contrib/convert-workflow.pl
index f25bb0ea7..bd3a1a2ee 100755
--- a/contrib/convert-workflow.pl
+++ b/contrib/convert-workflow.pl
@@ -6,7 +6,7 @@
 # This Source Code Form is "Incompatible With Secondary Licenses", as
 # defined by the Mozilla Public License, v. 2.0.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/contrib/extension-convert.pl b/contrib/extension-convert.pl
index a70888dc1..fd2daea0e 100755
--- a/contrib/extension-convert.pl
+++ b/contrib/extension-convert.pl
@@ -6,7 +6,7 @@
 # This Source Code Form is "Incompatible With Secondary Licenses", as
 # defined by the Mozilla Public License, v. 2.0.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/contrib/fixperms.pl b/contrib/fixperms.pl
index 33b042c66..7e0d5820d 100755
--- a/contrib/fixperms.pl
+++ b/contrib/fixperms.pl
@@ -6,7 +6,7 @@
 # This Source Code Form is "Incompatible With Secondary Licenses", as
 # defined by the Mozilla Public License, v. 2.0.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/contrib/merge-users.pl b/contrib/merge-users.pl
index 02129980d..df68a942c 100755
--- a/contrib/merge-users.pl
+++ b/contrib/merge-users.pl
@@ -6,7 +6,7 @@
 # This Source Code Form is "Incompatible With Secondary Licenses", as
 # defined by the Mozilla Public License, v. 2.0.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/contrib/mysqld-watcher.pl b/contrib/mysqld-watcher.pl
index 5dbbe320a..57a0f1206 100755
--- a/contrib/mysqld-watcher.pl
+++ b/contrib/mysqld-watcher.pl
@@ -9,7 +9,7 @@
 # mysqld-watcher.pl - a script that watches the running instance of
 # mysqld and kills off any long-running SELECTs against the shadow_db
 #
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/contrib/perl-fmt b/contrib/perl-fmt
index 7ac47eeb8..2aef4184a 100755
--- a/contrib/perl-fmt
+++ b/contrib/perl-fmt
@@ -6,7 +6,7 @@
 # This Source Code Form is "Incompatible With Secondary Licenses", as
 # defined by the Mozilla Public License, v. 2.0.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/contrib/recode.pl b/contrib/recode.pl
index fdb3ae0ac..8b3b8818d 100755
--- a/contrib/recode.pl
+++ b/contrib/recode.pl
@@ -6,7 +6,7 @@
 # This Source Code Form is "Incompatible With Secondary Licenses", as
 # defined by the Mozilla Public License, v. 2.0.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/contrib/sendbugmail.pl b/contrib/sendbugmail.pl
index b1b5fa8a5..bd34d3daf 100755
--- a/contrib/sendbugmail.pl
+++ b/contrib/sendbugmail.pl
@@ -6,7 +6,7 @@
 # This Source Code Form is "Incompatible With Secondary Licenses", as
 # defined by the Mozilla Public License, v. 2.0.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/contrib/sendunsentbugmail.pl b/contrib/sendunsentbugmail.pl
index 58bde594c..2a68d843b 100755
--- a/contrib/sendunsentbugmail.pl
+++ b/contrib/sendunsentbugmail.pl
@@ -6,7 +6,7 @@
 # This Source Code Form is "Incompatible With Secondary Licenses", as
 # defined by the Mozilla Public License, v. 2.0.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/contrib/syncLDAP.pl b/contrib/syncLDAP.pl
index 4c75bbc1f..8e82e00f7 100755
--- a/contrib/syncLDAP.pl
+++ b/contrib/syncLDAP.pl
@@ -6,7 +6,7 @@
 # This Source Code Form is "Incompatible With Secondary Licenses", as
 # defined by the Mozilla Public License, v. 2.0.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/createaccount.cgi b/createaccount.cgi
index 0251114e8..b60ae4bb3 100755
--- a/createaccount.cgi
+++ b/createaccount.cgi
@@ -6,7 +6,7 @@
 # This Source Code Form is "Incompatible With Secondary Licenses", as
 # defined by the Mozilla Public License, v. 2.0.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/describecomponents.cgi b/describecomponents.cgi
index 19e1af4a5..8817dbae9 100755
--- a/describecomponents.cgi
+++ b/describecomponents.cgi
@@ -6,7 +6,7 @@
 # This Source Code Form is "Incompatible With Secondary Licenses", as
 # defined by the Mozilla Public License, v. 2.0.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/describekeywords.cgi b/describekeywords.cgi
index ac18d5f22..d6f07650e 100755
--- a/describekeywords.cgi
+++ b/describekeywords.cgi
@@ -6,7 +6,7 @@
 # This Source Code Form is "Incompatible With Secondary Licenses", as
 # defined by the Mozilla Public License, v. 2.0.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 000000000..eb2e68543
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,49 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+version: '3.6'
+
+services:
+  bugzilla5.web:
+    build:
+      context: .
+      dockerfile: Dockerfile
+    command: /root/docker/startup.sh
+    volumes:
+      - bugzilla5-data-dir:/var/www/html/data
+      - .:/mnt/sync
+    tmpfs:
+      - /tmp
+      - /run/lock
+    ports:
+      - 8080:80
+    depends_on:
+      - bugzilla5.db
+    environment: &bugzilla_env
+      - BZ_ADMIN_EMAIL=admin@bugzilla.test
+      - BZ_ADMIN_PASSWORD=password01!
+      - BZ_ADMIN_REALNAME=Test Admin
+      - BZ_URLBASE=http://127.0.0.1:8080/
+      - BZ_DB_HOST=bugzilla5.db
+      - BZ_DB_PORT=3306
+      - BZ_DB_USER=bugs
+      - BZ_DB_NAME=bugs
+      - BZ_DB_PASS=bugzilla
+      - MARIADB_ROOT_HOST=%
+      - MARIADB_ROOT_PASSWORD=bugzilla
+
+  bugzilla5.db:
+    build:
+      context: .
+      dockerfile: Dockerfile.mariadb
+    volumes:
+      - bugzilla5-mysql-db:/var/lib/mysql
+    tmpfs:
+      - /tmp
+      - /var/lock
+    environment: *bugzilla_env
+
+volumes:
+  bugzilla5-mysql-db:
+  bugzilla5-data-dir:
diff --git a/docker/000-default.conf b/docker/000-default.conf
new file mode 100644
index 000000000..8d4d59291
--- /dev/null
+++ b/docker/000-default.conf
@@ -0,0 +1,37 @@
+PerlSwitches -w -T
+ServerName localhost
+<VirtualHost *:80>
+	# The ServerName directive sets the request scheme, hostname and port that
+	# the server uses to identify itself. This is used when creating
+	# redirection URLs. In the context of virtual hosts, the ServerName
+	# specifies what hostname must appear in the request's Host: header to
+	# match this virtual host. For the default virtual host (this file) this
+	# value is not decisive as it is used as a last resort host regardless.
+	# However, you must set it for any further virtual host explicitly.
+ServerName 172.17.0.3
+
+	ServerAdmin webmaster@localhost
+	DocumentRoot /var/www/html
+
+	# Available loglevels: trace8, ..., trace1, debug, info, notice, warn,
+	# error, crit, alert, emerg.
+	# It is also possible to configure the loglevel for particular
+	# modules, e.g.
+	#LogLevel info ssl:warn
+
+	ErrorLog ${APACHE_LOG_DIR}/error.log
+	CustomLog ${APACHE_LOG_DIR}/access.log combined
+
+	# For most configuration files from conf-available/, which are
+	# enabled or disabled at a global level, it is possible to
+	# include a line for only one particular virtual host. For example the
+	# following line enables the CGI configuration for this host only
+	# after it has been globally disabled with "a2disconf".
+	#Include conf-available/serve-cgi-bin.conf
+
+PerlSwitches -w -T
+PerlConfigRequire /var/www/html/mod_perl.pl
+
+</VirtualHost>
+
+# vim: syntax=apache ts=4 sw=4 sts=4 sr noet
diff --git a/docker/checksetup_answers.txt b/docker/checksetup_answers.txt
new file mode 100644
index 000000000..1a4282232
--- /dev/null
+++ b/docker/checksetup_answers.txt
@@ -0,0 +1,39 @@
+$answer{'ADMIN_EMAIL'}          = %%BZ_ADMIN_EMAIL%%;
+$answer{'ADMIN_OK'}             = 'Y';
+$answer{'ADMIN_PASSWORD'}       = %%BZ_ADMIN_PASSWORD%%;
+$answer{'ADMIN_REALNAME'}       = %%BZ_ADMIN_REALNAME%%;
+$answer{'webservergroup'}       = 'www-data';
+$answer{'use_suexec'}           = '0';
+$answer{'db_driver'}            = 'mariadb';
+$answer{'db_host'}              = %%BZ_DB_HOST%%;
+$answer{'db_port'}              = %%BZ_DB_PORT%%;
+$answer{'db_sock'}              = '';
+$answer{'db_name'}              = %%BZ_DB_NAME%%;
+$answer{'db_user'}              = %%BZ_DB_USER%%;
+$answer{'db_pass'}              = %%BZ_DB_PASS%%;
+$answer{'size_limit'}           = 750000;
+$answer{'bugzilla_version'}     = '1';
+$answer{'create_htaccess'}      = '1';
+$answer{'db_check'}             = 1;
+$answer{'diffpath'}             = '/usr/bin';
+$answer{'index_html'}           = 0;
+$answer{'interdiffbin'}         = '/usr/bin/interdiff';
+$answer{'user_info_class'}      = 'CGI';
+$answer{'user_verify_class'}    = 'DB';
+$answer{'use_mailer_queue'}     = 0;
+$answer{'useclassification'}    = 1;
+$answer{'usebugaliases'}        = 1;
+$answer{'upgrade_notification'} = 'disabled';
+$answer{'usestatuswhiteboard'}  = 1;
+$answer{'usetargetmilestone'}   = 1;
+$answer{'webdotbase'}           = '/usr/bin/dot';
+$answer{'insidergroup'}         = 'admin';
+$answer{'default_bug_type'}     = '--';
+$answer{'defaultpriority'}      = '--';
+$answer{'defaultseverity'}      = 'normal';
+$answer{'urlbase'}              = %%BZ_URLBASE%%;
+$answer{'docs_urlbase'}         = 'https://bugzilla.readthedocs.io/en/5.0/';
+$answer{'db_mysql_ssl_ca_file'}     = '';
+$answer{'db_mysql_ssl_ca_path'}     = '';
+$answer{'db_mysql_ssl_client_cert'} = '';
+$answer{'db_mysql_ssl_client_key'}  = '';
diff --git a/docker/my.cnf b/docker/my.cnf
new file mode 100644
index 000000000..1c8dab421
--- /dev/null
+++ b/docker/my.cnf
@@ -0,0 +1,31 @@
+[mysqld]
+#
+# Remove leading # and set to the amount of RAM for the most important data
+# cache in MySQL. Start at 70% of total RAM for dedicated server, else 10%.
+# innodb_buffer_pool_size = 128M
+#
+# Remove leading # to turn on a very important data integrity option: logging
+# changes to the binary log between backups.
+# log_bin
+#
+# Remove leading # to set options mainly useful for reporting servers.
+# The server defaults are faster for transactions and fast SELECTs.
+# Adjust sizes as needed, experiment to find the optimal values.
+# join_buffer_size = 128M
+# sort_buffer_size = 2M
+# read_rnd_buffer_size = 2M
+skip-host-cache
+skip-name-resolve
+datadir=/var/lib/mysql
+socket=/var/lib/mysql/mysql.sock
+secure-file-priv=/var/lib/mysql
+user=mysql
+wait-timeout=28800
+
+# Disabling symbolic-links is recommended to prevent assorted security risks
+symbolic-links=0
+
+log-error=/var/log/mysqld.log
+pid-file=/var/run/mysqld/mysqld.pid
+lower_case_table_names=1
+max_allowed_packet=1G
diff --git a/docker/mysql/bugzilla.cnf b/docker/mysql/bugzilla.cnf
new file mode 100644
index 000000000..6f8ba4778
--- /dev/null
+++ b/docker/mysql/bugzilla.cnf
@@ -0,0 +1,9 @@
+[mysqld]
+max_allowed_packet = 1G
+innodb_file_per_table = ON
+innodb_large_prefix   = ON
+innodb_file_format = Barracuda
+ft_min_word_len = 2
+
+[mysql]
+max_allowed_packet = 1G
diff --git a/docker/startup.sh b/docker/startup.sh
new file mode 100755
index 000000000..205613828
--- /dev/null
+++ b/docker/startup.sh
@@ -0,0 +1,73 @@
+#!/bin/bash
+
+[ -z "$BZ_DB_HOST" ] && echo "Missing Docker Environment, check docker-compose.yml" && exit -1
+cd /var/www/html
+apachectl start
+while :
+do
+  echo "Waiting for database to be available..."
+  nc -z $BZ_DB_HOST $BZ_DB_PORT
+  [ $? -eq 0 ] && break
+  sleep 2
+done
+echo "Checking database..."
+cat - >/root/docker/myclient-root.cnf <<EOF
+[client]
+host='$BZ_DB_HOST'
+port=$BZ_DB_PORT
+user='root'
+password='$MARIADB_ROOT_PASSWORD'
+EOF
+TESTSQL="show databases like '$BZ_DB_NAME'"
+DBEXISTS=`echo "$TESTSQL" | mysql --defaults-file=/root/docker/myclient-root.cnf -BN`
+[ -z "$DBEXISTS" ] && (
+  echo "Database not found, creating..."
+  CREATESQL="
+CREATE DATABASE \`$BZ_DB_NAME\`;
+GRANT SELECT, INSERT,
+UPDATE, DELETE, INDEX, ALTER, CREATE, LOCK TABLES,
+CREATE TEMPORARY TABLES, DROP, REFERENCES ON \`$BZ_DB_NAME\`.*
+TO '$BZ_DB_USER'@'%' IDENTIFIED BY '$BZ_DB_PASS';
+FLUSH PRIVILEGES;
+"
+  echo "$CREATESQL" | mysql --defaults-file=/root/docker/myclient-root.cnf -BN
+)
+echo "Beginning checksetup..."
+perl -pi -e "
+s/%%BZ_ADMIN_EMAIL%%/'${BZ_ADMIN_EMAIL//@/\\@}'/;
+s/%%BZ_ADMIN_PASSWORD%%/'${BZ_ADMIN_PASSWORD//@/\\@//$/\\$}'/;
+s/%%BZ_ADMIN_REALNAME%%/'${BZ_ADMIN_REALNAME//@/\\@//$/\\$}'/;
+s/%%BZ_DB_HOST%%/'$BZ_DB_HOST'/;
+s/%%BZ_DB_PORT%%/$BZ_DB_PORT/;
+s/%%BZ_DB_NAME%%/'$BZ_DB_NAME'/;
+s/%%BZ_DB_USER%%/'$BZ_DB_USER'/;
+s/%%BZ_DB_PASS%%/'${BZ_DB_PASS//@/\\@//$/\\$}'/;
+s@%%BZ_URLBASE%%@'${BZ_URLBASE//@/\\@}'@;
+" /root/docker/checksetup_answers.txt
+perl checksetup.pl /root/docker/checksetup_answers.txt
+echo "Checksetup completed."
+
+LOGIN_USER="Admin user: $BZ_ADMIN_EMAIL"
+LOGIN_PASS="Admin password: $BZ_ADMIN_PASSWORD"
+cat - <<EOF
+#########################################
+##                                     ##
+##  Your Bugzilla installation should  ##
+##         now be reachable at:        ##
+##                                     ##
+EOF
+printf "##%*s%*s##\n" $(( (${#BZ_URLBASE} + 37) / 2)) $BZ_URLBASE $(( 37 - ((${#BZ_URLBASE} + 37) / 2)  )) " "
+cat - <<EOF
+##                                     ##
+EOF
+printf "##%*s%*s##\n" $(( (${#LOGIN_USER} + 37) / 2)) "$LOGIN_USER" $(( 37 - ((${#LOGIN_USER} + 37) / 2)  )) " "
+printf "##%*s%*s##\n" $(( (${#LOGIN_PASS} + 37) / 2)) "$LOGIN_PASS" $(( 37 - ((${#LOGIN_PASS} + 37) / 2)  )) " "
+cat - <<EOF
+##                                     ##
+##   user/password only valid if you   ##
+##    haven't already changed them.    ##
+##                                     ##
+#########################################
+EOF
+# don't exit docker
+while [ 1 ]; do sleep 1000; done
diff --git a/docs/en/images/bugzilla-logo-white.svg b/docs/en/images/bugzilla-logo-white.svg
new file mode 100644
index 000000000..e53952646
--- /dev/null
+++ b/docs/en/images/bugzilla-logo-white.svg
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg version="1.1" viewBox="0 0 252 92" xmlns="http://www.w3.org/2000/svg">
+ <g stroke-width="1.85">
+  <path d="m47.645 32.913v-4.0971h0.45851c-0.66923-0.31388-1.413-0.49457-2.1995-0.49457h-26.59c-2.8692 0-5.2043 2.3351-5.2043 5.2043v26.589c0 2.8707 2.3351 5.2058 5.2043 5.2058h26.59c2.869 0 5.2043-2.3351 5.2043-5.2058v-26.589c0-0.20781-0.01577-0.41143-0.03934-0.61277z" opacity=".5"/>
+  <path d="m45.295 64.499h-26.591c-2.8694 0-5.2045-2.3351-5.2045-5.2056v-26.589c0-2.8694 2.3351-5.2045 5.2045-5.2045h26.591c2.869 0 5.2041 2.3351 5.2041 5.2045v26.589c0 2.8703-2.3351 5.2056-5.2041 5.2056" fill="#fff"/>
+  <path d="m48.938 59.293c0 2.0125-1.6313 3.6436-3.6435 3.6436h-26.591c-2.0123 0-3.6435-1.6311-3.6435-3.6436v-26.591c0-2.0125 1.6313-3.6436 3.6435-3.6436h26.591c2.0123 0 3.6435 1.6311 3.6435 3.6436z" fill="#90449e"/>
+  <path d="m52.368 33.122c-2.5532-1.8454-5.0983-3.2457-6.05-3.75 0.76658 1.7745 1.0498 3.1673 1.0584 3.1784-0.0055-8.07e-4 -0.77317-1.6996-1.8931-3.7752-1.1183-2.0796-2.9018-4.4205-5.5279-5.5431-0.89116-0.38159-1.7076-0.53544-2.4548-0.53544-2.7299-0.0095-4.6589 2.1098-5.4461 3.1915-0.26242 0.35715-0.39041 0.58269-0.39041 0.58389l-0.05461 0.09601-0.10015 0.04512c-0.84406 0.37323-1.8258 1.3976-2.5987 2.1053-2.2602 2.0506-3.6377 4.6279-3.6377 7.427 0 1.027 0.18506 2.0872 0.58509 3.1711 0.72889 1.9695 1.9749 3.5276 3.6366 4.8077 1.8987 1.5385 1.7923 5.702 1.8236 5.7345v4.3996c0.04393 0.04592 0.08222 0.08948 0.13176 0.13924 0.14272 0.14193 0.30866 0.29427 0.50054 0.44771 0.71949 0.57647 1.7555 1.1422 3.0496 1.1422 0.82245-8.04e-4 1.7575-0.22816 2.8041-0.84267l0.06105 0.0076c0.46188-3.1684 1.5229-4.8561 3.2118-5.6898 1.6984-0.8347 3.8851-0.82708 6.6322-0.93983 2.7282-0.10594 5.3845-0.54084 7.3842-1.6243 1.9979-1.0897 3.3548-2.7708 3.614-5.5279 0.0082-0.0985 0.01052-0.1979 0.01052-0.29688 0.03801-2.5675-3.1171-5.6382-6.3515-7.9511" fill="#f6bd6d"/>
+  <path d="m25.45 62.958 4.148-7.5529s-7.2059-3.1612-10.85 7.4662v0.08722z"/>
+  <path d="m41.14 26.494c0.1074 0.26455 0.18531 0.54964 0.2477 0.84289-0.0335-0.25678-0.08247-0.50639-0.15186-0.74412-0.03268-0.03194-0.06296-0.0675-0.09589-0.09887"/>
+  <path d="m43.896 31.72c-0.17567-1.8082-1.2347-3.706-2.6089-5.072 1.2529 1.474 2.1418 3.4757 2.1418 5.2965 0 3.402-1.8478 4.8822-4.6628 4.8582-1.7723-0.01324-2.9282-0.34755-3.6823-0.91641 0.77638 0.93887 2.1172 1.436 4.5633 1.2191 2.8568-0.25386 4.5842-1.9357 4.2492-5.3854"/>
+  <path d="m36.587 24.843c-0.0082 0.0019-0.01577 0.0056-0.02134 0.0076-0.0624 0.0616-0.1216 0.12853-0.18023 0.19601 0.06668-0.06962 0.13149-0.14113 0.20298-0.20322"/>
+  <path d="m41.23 26.577c0.0014 0.0056 0.0019 0.0095 0.0027 0.01538 0.0187 0.01694 0.03536 0.03688 0.05301 0.05449-0.0187-0.02409-0.03694-0.0482-0.0568-0.07094" fill="#fff"/>
+  <path d="m41.288 26.647c-0.0187-0.01979-0.03507-0.0369-0.05303-0.05451 0.06941 0.23808 0.11838 0.48756 0.15209 0.74416 0.02705 0.20835 0.04658 0.42023 0.04658 0.63835 0 2.1682-1.3593 3.9256-3.0363 3.9256s-3.0365-1.7575-3.0365-3.9256c0-1.1668 0.39759-2.2089 1.0227-2.9279 0.05868-0.0675 0.11786-0.13445 0.18023-0.19582 0.11918-0.11721 0.24343-0.226 0.37539-0.3195-2.3693 0.36638-3.2692 2.9599-3.2692 6.111 0 2.2285-0.0187 4.1622 1.4143 5.2428 0.75396 0.5689 1.91 0.90178 3.6825 0.91662 2.815 0.02409 4.6628-1.4564 4.6628-4.8584 0-1.8206-0.88896-3.8225-2.1418-5.2965" fill="#fff"/>
+  <path d="m35.36 27.976c0 2.1681 1.3595 3.9256 3.0365 3.9256 1.6769 0 3.0363-1.7575 3.0363-3.9256 0-0.49353-0.07363-0.96388-0.20245-1.3989-1.0776-1.2427-2.4154-2.0929-3.7004-2.0929-0.20781 0-0.40262 0.01979-0.59151 0.04876-0.94043 0.66701-1.5784 1.9584-1.5784 3.4433"/>
+  <path d="m38.898 26.999c0 0.83888-0.56365 1.5189-1.2585 1.5189-0.69545 0-1.2585-0.68007-1.2585-1.5189 0-0.83869 0.56308-1.5189 1.2585-1.5189 0.69485 0 1.2585 0.68026 1.2585 1.5189" fill="#fff"/>
+  <path d="m30.419 53.994 5.88e-4 -5.94e-4c-4.01e-4 -3.8e-4 -5.88e-4 -3.8e-4 -5.88e-4 -3.8e-4z" fill="#231e1f"/>
+  <path d="m38.431 55.363c0.02134-0.01324 0.04419-0.02697 0.06666-0.04169l-0.28826-0.43106c-0.13737 0.09161-0.27199 0.17443-0.40562 0.25308l0.41985 0.04935s0.07659 0.06156 0.20728 0.1702" fill="#231e1f"/>
+  <path d="m30.85 54.326 0.60012 0.07114-0.13199-0.01538z" fill="#f6bd6d"/>
+  <path d="m45.865 28.395c0.3217 0.16387 1.6621 0.85551 3.3387 1.855-0.32831-2.0574-2.7094-4.6384-4.1239-5.9001-0.49477-0.44429-0.88855-0.56548-1.1968-0.56648-0.30788-9.74e-4 -0.55706 0.12757-0.73492 0.2627-0.13069 0.09926-0.21101 0.19285-0.24502 0.23564l-0.0273 0.0593c1.3328 1.207 2.2998 2.6851 2.9893 4.0538" fill="#f6bd6d"/>
+  <path d="m43.473 48.903s-3.0363-2.5159-6.94-1.3012c0 0 5.2058-0.17365 5.8126 1.7346z"/>
+ </g>
+ <path d="m29.333 1.9262c0 0.45507-0.36899 0.82426-0.82426 0.82426-0.45488 0-0.82406-0.36919-0.82406-0.82426 0-0.4549 0.36918-0.82408 0.82406-0.82408 0.45528 0 0.82426 0.36919 0.82426 0.82408z" fill="#5f122b" stroke="#000" stroke-width=".5"/>
+ <path d="m53.798 16.674c0 0.45509-0.36895 0.82426-0.82386 0.82426-0.45509 0-0.82404-0.36917-0.82404-0.82426 0-0.45489 0.36895-0.82408 0.82404-0.82408 0.45489 0 0.82386 0.36921 0.82386 0.82408z" fill="#5f122b" stroke="#000" stroke-width=".5"/>
+ <g stroke-width="1.85">
+  <path d="m39.569 55.852c0-0.42647-0.31306-0.77261-0.69946-0.77261-0.25789 0-0.48034 0.1555-0.60191 0.38479-1.1892 0.74477-2.289 1.043-3.262 1.043h-0.0055c-2.1022-0.0095-3.5452-1.3058-4.1935-2.0326l0.02705-0.25708-0.42303 0.16233-0.55285 0.29284-0.17914 0.11418-0.09747 0.48776 0.14329 0.28984-0.01597 0.01977c0.24504 0.26404 0.65959 0.67262 1.2301 1.0865 0.95464 0.69285 2.3138 1.3775 3.9467 1.3765 1.2081-0.0012 2.576-0.27003 4.0751-1.316 0.34152-0.05143 0.60992-0.48715 0.60992-0.87852" fill="#fff"/>
+  <path d="m58.708 41.37c-0.25922 2.7573-1.6164 4.4382-3.614 5.5281-1.9997 1.0833-4.6563 1.5179-7.3844 1.6241-2.7467 0.11303-4.9337 0.10518-6.6318 0.93983-1.6892 0.8337-2.7501 2.5212-3.212 5.6898l-0.06078-0.0076c-1.0466 0.61434-1.9816 0.84165-2.8041 0.84269-1.2944 0-2.3303-0.56587-3.0498-1.1424-0.19199-0.15338-0.35756-0.30585-0.50036-0.44767l-0.60012-0.07077 0.4681 0.05507 0.13201 0.01538c-0.04981-0.04972-0.08785-0.09332-0.13201-0.13922v-4.3994c-0.03133-0.03248 0.07525-4.1959-1.8236-5.7345-1.6613-1.28-2.9075-2.8379-3.6366-4.8075-0.4-1.0839-0.58487-2.1442-0.58487-3.171 0-2.7991 1.3775-5.3766 3.6377-7.427 0.77298-0.70749 1.7547-1.7321 2.5987-2.1053l0.10015-0.04496 0.05463-0.09601c0-0.0011 0.128-0.22682 0.39019-0.58389 0.78719-1.0817 2.7164-3.202 5.4461-3.1915 0.74732 0 1.5638 0.15377 2.4546 0.53562 2.6259 1.1225 4.4097 3.4633 5.5281 5.5429 1.1197 2.0756 1.8868 3.7744 1.893 3.7754-0.0082-0.0095-0.29166-1.404-1.0582-3.1784 0.95145 0.50397 3.4967 1.9044 6.05 3.7498 3.2342 2.3127 6.3895 5.3834 6.3515 7.9512 0 0.09887-0.0027 0.19818-0.01052 0.29667m-19.427 14.907c-0.03213 0.08547-0.07873 0.16292-0.13525 0.22467-0.29623 0.3209-0.75013 0.50619-1.1361 0.69285-0.66682 0.3217-1.318 0.63077-2.0539 0.75498-0.35134 0.0591-0.7155 0.09755-1.0724 0.0983-1.6327 9.74e-4 -2.9919-0.68366-3.9467-1.3765-0.54741-0.3972-0.94683-0.78597-1.1952-1.0502-0.0027-0.01324-0.0055-0.02409-0.0082-0.03616-0.21103-0.81525 0.76895-1.0873 1.0394-1.1464 0.63158 0.71951 2.0896 2.0598 4.2318 2.07 0.94944 0 1.9083-0.27504 2.734-0.73655 0.26189-0.14572 0.54905-0.30346 0.78019-0.49555 0.04578-0.03827 0.29545 0.09906 0.33609 0.12144 0.13149 0.0717 0.25468 0.16541 0.35314 0.27921 0.14247 0.16311 0.14701 0.40207 0.0726 0.60033m3.6205-31.994c0.03402-0.04282 0.11433-0.13614 0.24504-0.23564 0.17809-0.13519 0.42704-0.2638 0.73511-0.26272 0.3081 9.74e-4 0.70209 0.1222 1.1966 0.56648 1.4145 1.2618 3.7955 3.8427 4.1239 5.9001-1.6763-0.99953-3.0168-1.6908-3.3387-1.855-0.68964-1.3687-1.6565-2.8468-2.9893-4.0538zm9.8631 8.2904c-1.0035-0.72451-2.0465-1.4002-3.0118-1.9901-0.13844-2.4607-2.8065-5.2245-4.3257-6.6187-0.55925-0.50737-1.0882-0.7029-1.5454-0.70068-0.87131 0.0095-1.347 0.64239-1.4125 0.72769-0.66383-0.54261-1.4085-1.018-2.2474-1.3819-0.96768-0.41503-1.8796-0.59029-2.7216-0.59029-3.7678 0.0268-5.9808 3.4024-6.3645 4.0216-1.0053 0.49577-1.9507 1.5197-2.6803 2.1764-2.3643 2.1431-3.8594 4.898-3.8594 7.9279 0 1.1087 0.20112 2.2504 0.62675 3.4054 0.77557 2.0998 2.1151 3.7696 3.8582 5.1096 0.71607 0.53324 1.1558 1.8376 1.3539 3.0119 0.20539 1.1712 0.20432 2.1836 0.2062 2.1864v4.0747c-1.8818 0.20794-1.3995 1.8952-1.3995 1.8952l0.03213 0.05089c-0.0012 4.19e-4 -0.0025 8e-4 -0.0027 0.0012 0.659 0.72411 2.697 2.6776 5.6126 2.6875h0.0055c1.4101 0 3.0111-0.49337 4.7089-1.7739 0.11406-0.06236 0.21904-0.15873 0.21904-0.15873l0.0027-0.0056c0.01052-0.0076 0.02134-0.01324 0.03163-0.02409-0.0027-0.0019-0.0082-0.0076-0.01052-0.0095l0.0055-0.0095s0.44286-1.559-1.2347-1.8208c0.48852-2.7443 1.4065-4.0148 2.7652-4.6948 1.4681-0.73814 3.5968-0.76698 6.358-0.87037 2.7804-0.11168 5.5255-0.54321 7.6791-1.7058 2.1558-1.1544 3.7047-3.0958 3.9659-6.0624 0.01052-0.12048 0.01577-0.23886 0.01577-0.35715-0.03616-3.0881-3.3732-6.123-6.6309-8.4998"/>
+  <path d="m44.786 62.958c-0.65221-2.1492-3.2091-4.7709-4.9337-6.353-1.7933 1.429-3.4831 1.9627-4.9638 1.9627h-0.0055c-2.9157-0.0095-4.9535-1.9636-5.6124-2.6875-3.6351 1.6521-5.46 5.976-5.8751 7.0778z" fill="#c7584d"/>
+  <path d="m23.683 62.936c0.19361-0.49899 0.60392-1.4615 1.2593-2.5406 0.92701-1.5287 2.3381-3.2646 4.2689-4.2007 0.79161 0.82909 2.8082 2.6169 5.672 2.632h0.0055c1.4945 9.74e-4 3.1837-0.524 4.9505-1.8817 1.6822 1.5513 4.0342 4.034 4.6724 5.991h0.5381c-0.66982-2.2766-3.274-4.9092-5.0214-6.5243l-0.16385-0.1503-0.17484 0.13883c-1.7561 1.3986-3.3832 1.9053-4.801 1.9063h-0.0055c-2.7917-0.0056-4.7831-1.8987-5.4197-2.6023l-0.12853-0.14037-0.17219 0.07764c-3.8397 1.7612-5.6654 6.294-6.0354 7.2943z"/>
+ </g>
+ <g fill="#fff">
+  <path d="m84.72 46.444q3.5027 0.64133 5.624 2.516 2.1707 1.8747 2.1707 5.7227 0 5.328-3.7493 7.5973-3.7493 2.22-10.508 2.22h-10.261v-34.089h8.9293q14.109 0 14.109 8.6827 0 3.0093-1.8253 4.9333-1.8253 1.8747-4.4893 2.4173zm-10.212-11.248v9.2253h3.996q2.7133 0 4.292-1.184 1.5787-1.2333 1.5787-3.6013 0-2.4667-1.6773-3.4533-1.628-0.98667-5.1307-0.98667zm3.7987 24.42q3.6013 0 5.476-1.0853 1.924-1.0853 1.924-4.0947 0-2.9107-1.8253-4.1933-1.776-1.2827-4.884-1.2827h-4.4893v10.656z"/>
+  <path d="m118.71 64.5h-5.476l-0.34533-3.6507q-1.48 2.22-3.404 3.3053t-4.588 1.0853q-3.5027 0-5.3773-2.1213-1.8747-2.1707-1.8747-5.9693v-18.747h6.3147v17.957q0 2.2693 0.74 3.2067 0.74 0.888 2.4173 0.888 3.108 0 5.2787-3.7v-18.352h6.3147z"/>
+  <path d="m148.9 39.784q-2.516 0.888-7.548 0.888 2.4173 1.0853 3.552 2.7133 1.184 1.5787 1.184 3.996 0 2.5653-1.2827 4.5387-1.2333 1.9733-3.6507 3.108-2.368 1.1347-5.5747 1.1347-1.776 0-3.108-0.39467-1.0853 0.74-1.0853 1.924 0 1.8253 3.1573 1.8253h3.996q2.8613 0 4.9827 0.93733 2.1707 0.93733 3.3547 2.664 1.184 1.6773 1.184 3.7987 0 3.9467-3.3547 6.1173-3.3547 2.22-9.6693 2.22-4.5387 0-7.1533-0.93733-2.5653-0.93733-3.6507-2.7133t-1.0853-4.5387h5.6733q0 1.3813 0.49333 2.1707 0.54267 0.78933 1.8747 1.184 1.3813 0.39467 3.9467 0.39467 3.6013 0 5.0813-0.888t1.48-2.5653q0-1.48-1.1347-2.3187-1.1347-0.78933-3.3053-0.78933h-3.9467q-3.7 0-5.5747-1.4307t-1.8747-3.6013q0-1.3813 0.78933-2.664 0.83867-1.2827 2.2693-2.1707-2.368-1.2333-3.5027-3.0093-1.0853-1.8253-1.0853-4.3413 0-2.812 1.3813-4.9333t3.7987-3.256q2.4667-1.184 5.624-1.184 4.144 0.09867 6.66-0.592 2.5653-0.74 5.5253-2.3187zm-13.665 2.1707q-2.1213 0-3.3547 1.3813-1.184 1.332-1.184 3.6013 0 2.368 1.2333 3.7493t3.404 1.3813q2.2693 0 3.4533-1.332 1.184-1.3813 1.184-3.848 0-4.9333-4.736-4.9333z"/>
+  <path d="m169.52 42.941-12.629 16.625h12.876l-0.64134 4.9333h-19.487v-4.5387l12.679-16.625h-11.593v-4.9333h18.796z"/>
+  <path d="m180.67 64.5h-6.3147v-26.097h6.3147zm-3.2067-39.319q1.776 0 2.8613 1.1347 1.1347 1.0853 1.1347 2.7133t-1.1347 2.7133q-1.0853 1.0853-2.8613 1.0853-1.7267 0-2.8613-1.0853-1.0853-1.0853-1.0853-2.7133t1.0853-2.7133q1.1347-1.1347 2.8613-1.1347z"/>
+  <path d="m194.14 65.24q-3.0093 0-4.6867-1.7267-1.6773-1.776-1.6773-4.9827v-30.587l6.3147-0.69067v31.08q0 0.93733 0.34533 1.3813 0.34534 0.444 1.1347 0.444t1.4307-0.24667l1.3813 4.44q-1.9733 0.888-4.2427 0.888z"/>
+  <path d="m208.79 65.24q-3.0093 0-4.6867-1.7267-1.6773-1.776-1.6773-4.9827v-30.587l6.3147-0.69067v31.08q0 0.93733 0.34534 1.3813 0.34533 0.444 1.1347 0.444 0.78934 0 1.4307-0.24667l1.3813 4.44q-1.9733 0.888-4.2427 0.888z"/>
+  <path d="m236.52 57.593q0 1.5293 0.444 2.2693 0.49333 0.69067 1.5293 1.0853l-1.332 4.1933q-2.22-0.19733-3.6507-1.0853-1.3813-0.888-2.1213-2.7133-1.332 1.9733-3.3547 2.96-1.9733 0.93733-4.44 0.93733-3.8973 0-6.1667-2.22-2.2693-2.22-2.2693-5.8213 0-4.2427 3.0587-6.4627 3.108-2.2693 8.8307-2.2693h3.256v-1.48q0-2.516-1.184-3.5027-1.1347-1.036-3.7-1.036-2.8613 0-6.9067 1.3813l-1.48-4.292q4.884-1.8747 9.472-1.8747 10.015 0 10.015 8.9787zm-11.248 3.0587q3.2067 0 5.032-3.2067v-5.2787h-2.6147q-6.1173 0-6.1173 4.5387 0 1.8747 0.93733 2.9107t2.7627 1.036z"/>
+ </g>
+ <path d="m34.664 23.049c2.4053-4.7231 8.3249-13.622 17.966-6.7529" fill="none" stroke="#000" stroke-width=".7"/>
+ <path d="m33.66 23.667c1.8691-8.1031 5.4653-19.196-4.3232-21.654" fill="none" stroke="#000" stroke-width=".7"/>
+</svg>
\ No newline at end of file
diff --git a/docs/en/rst/_static/bugzilla.css b/docs/en/rst/_static/bugzilla.css
index 06f5c8375..8d1132f24 100644
--- a/docs/en/rst/_static/bugzilla.css
+++ b/docs/en/rst/_static/bugzilla.css
@@ -1,21 +1,5 @@
-@import 'default.css';
-
-dt { font-weight: bold; }
-
-/* Custom roles */
-.param    { font-weight: bold; }
-.paramval { font-family: monospace; }
-.group    { font-family: monospace; }
-.field    { font-weight: bold; }
-.command  { font-family: monospace; font-size: 130% }
-
-.admonition-todo {
-  background-color: lightpink;
-  border: 2px darkred solid;
-}
-
-/* Make Buggie's antenna not take up so much space */
-.logo {
-  display: block;
-  margin-top: -20px;
+.wy-side-nav-search .wy-dropdown > a img.logo,
+.wy-side-nav-search > a img.logo {
+  min-width: 150px;
+  width: 350px;
 }
diff --git a/docs/en/rst/conf.py b/docs/en/rst/conf.py
index 34acb4778..9169cb648 100644
--- a/docs/en/rst/conf.py
+++ b/docs/en/rst/conf.py
@@ -113,7 +113,7 @@ rst_prolog = """
 .. role:: field
     :class: field
 
-.. |min-perl-ver| replace:: 5.10.1
+.. |min-perl-ver| replace:: 5.14.0
 """
 
 rst_epilog = """
@@ -128,12 +128,31 @@ them `here <https://bugzilla.mozilla.org/enter_bug.cgi?product=Bugzilla&componen
 
 # The theme to use for HTML and HTML Help pages.  See the documentation for
 # a list of builtin themes.
-html_theme = 'default'
+html_theme = "sphinx_rtd_theme"
 
 # Theme options are theme-specific and customize the look and feel of a theme
 # further.  For a list of options available for each theme, see the
 # documentation.
-#html_theme_options = {}
+html_theme_options = {
+    #'analytics_id': 'G-XXXXXXXXXX',  #  Provided by Google in your dashboard
+    #'analytics_anonymize_ip': False,
+    'logo_only': True,
+    'display_version': True,
+    'prev_next_buttons_location': 'bottom',
+    'style_external_links': False,
+    'vcs_pageview_mode': '',
+    #'style_nav_header_background': 'white',
+    # Toc options
+    'collapse_navigation': True,
+    'sticky_navigation': True,
+    'navigation_depth': 4,
+    'includehidden': True,
+    'titles_only': False
+}
+
+html_css_files = [
+    "bugzilla.css"
+]
 
 # Add any paths that contain custom themes here, relative to this directory.
 #html_theme_path = []
@@ -145,11 +164,11 @@ html_theme = 'default'
 # A shorter title for the navigation bar.  Default is the same as html_title.
 #html_short_title = None
 
-html_style = "bugzilla.css"
+#html_style = "bugzilla.css"
 
 # The name of an image file (relative to this directory) to place at the top
 # of the sidebar.
-html_logo = "../images/bugzilla.png"
+html_logo = "../images/bugzilla-logo-white.svg"
 
 # The name of an image file (within the static path) to use as favicon of the
 # docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
@@ -383,13 +402,10 @@ pdf_fit_background_mode = 'scale'
 # Temporary highlighting of TODO items
 todo_include_todos = False
 
-# The readthedocs.org website cannot access POD.
-on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
-
-if on_rtd:
-    base_api_url = 'https://www.bugzilla.org/docs/5.0/en/html/integrating/api/'
-else:
-    base_api_url = '../integrating/api/'
+# Originally, the readthedocs.org website could not access POD,
+# so we would conditionally override that here if we were building
+# on RTD, but that's no longer the case.
+base_api_url = '../integrating/api/'
 
-extlinks = {'bug': ('https://bugzilla.mozilla.org/show_bug.cgi?id=%s', 'bug  '),
-            'api': (base_api_url + '%s', '')}
+extlinks = {'bug': ('https://bugzilla.mozilla.org/show_bug.cgi?id=%s', 'bug  %s'),
+            'api': (base_api_url + '%s', "%s")}
diff --git a/docs/en/rst/installing/db_server.rst b/docs/en/rst/installing/db_server.rst
index 8a7d89633..a06f87850 100644
--- a/docs/en/rst/installing/db_server.rst
+++ b/docs/en/rst/installing/db_server.rst
@@ -3,15 +3,16 @@
 Database Server
 ###############
 
-Bugzilla requires a database to store its data. We recommend either MySQL or
-PostgreSQL for production installations. Oracle 10 should work fine, but very
-little or no testing has been done with Oracle 11 and 12. SQLite is easy to
-configure but, due to its limitations, it should only be used for testing
+Bugzilla requires a database to store its data. We recommend MariaDB, MySQL 
+or PostgreSQL for production installations. Oracle 10 should work fine, but 
+very little or no testing has been done with Oracle 11 and 12. SQLite is easy 
+to configure but, due to its limitations, it should only be used for testing 
 purposes and very small installations.
 
 .. toctree::
    :maxdepth: 1
 
+   mariadb
    mysql
    postgresql
    oracle
diff --git a/docs/en/rst/installing/essential-post-install-config.rst b/docs/en/rst/installing/essential-post-install-config.rst
index 1b9fd6b94..51ff58719 100644
--- a/docs/en/rst/installing/essential-post-install-config.rst
+++ b/docs/en/rst/installing/essential-post-install-config.rst
@@ -94,7 +94,7 @@ This section corresponds to choosing a :param:`mail_delivery_method` of
 :paramval:`Sendmail`.
 
 Unless you know what you are doing, and can deal with the possible problems
-of spam, bounces and blacklists, it is probably unwise to set up your own
+of spam, bounces and blocklists, it is probably unwise to set up your own
 mail server just for Bugzilla. However, if you wish to do so, some guidance
 follows.
 
diff --git a/docs/en/rst/installing/linux.rst b/docs/en/rst/installing/linux.rst
index e2e05c310..49639e1f5 100644
--- a/docs/en/rst/installing/linux.rst
+++ b/docs/en/rst/installing/linux.rst
@@ -18,7 +18,7 @@ Install Packages
 ================
 
 Use your distribution's package manager to install Perl, your preferred
-database engine (MySQL if in doubt), and a webserver (Apache if in doubt).
+database engine (MySQL or MariaDB if in doubt), and a webserver (Apache if in doubt).
 Some distributions even have a Bugzilla package, although that will vary
 in age.
 
@@ -28,64 +28,103 @@ is not found, just remove it from the list and reissue the command. If you
 want to use a different database or webserver, substitute the package
 names as appropriate.
 
-Fedora and Red Hat
-------------------
+Fedora, CentOS Stream and RHEL
+------------------------------
 
-The following command will install Red Hat's packaged version of Bugzilla:
+The following command will install Fedora's packaged version of Bugzilla:
 
-:command:`yum install bugzilla httpd mysql-server`
+:command:`dnf install bugzilla httpd mariadb-server`
 
 Then, you can skip to :ref:`configuring your database <linux-config-database>`.
 It may be useful to know that Fedora stores the Bugzilla files in
 :file:`/usr/share/bugzilla`, so that's where you'll run :file:`checksetup.pl`.
 
-If you want to install a version of Bugzilla from the Bugzilla project, you
-will instead need:
-
-:command:`yum install httpd mysql-server mod_perl mod_perl-devel httpd-devel
-gd-devel mysql-devel
-graphviz patchutils gcc 'perl(Apache2::SizeLimit)' 'perl(Authen::Radius)'
-'perl(Authen::SASL)' 'perl(Cache::Memcached)' 'perl(CGI)' 'perl(Chart::Lines)'
-'perl(Daemon::Generic)' 'perl(Date::Format)' 'perl(DateTime)'
-'perl(DateTime::TimeZone)' 'perl(DBI)' 'perl(Digest::SHA)' 'perl(Email::MIME)'
-'perl(Email::Reply)' 'perl(Email::Sender)' 'perl(Encode)' 'perl(Encode::Detect)'
-'perl(File::MimeInfo::Magic)' 'perl(GD)' 'perl(GD::Graph)'
-'perl(GD::Text)' 'perl(HTML::FormatText::WithLinks)' 'perl(HTML::Parser)'
-'perl(HTML::Scrubber)' 'perl(IO::Scalar)' 'perl(JSON::RPC)' 'perl(JSON::XS)'
-'perl(List::MoreUtils)' 'perl(LWP::UserAgent)' 'perl(Math::Random::ISAAC)'
-'perl(MIME::Parser)' 'perl(mod_perl2)' 'perl(Net::LDAP)' 'perl(Net::SMTP::SSL)'
-'perl(PatchReader)' 'perl(SOAP::Lite)' 'perl(Template)'
-'perl(Template::Plugin::GD::Image)' 'perl(Test::Taint)' 'perl(TheSchwartz)'
-'perl(URI)' 'perl(XMLRPC::Lite)' 'perl(XML::Twig)'`
-
-If you are running RHEL6, you will have to enable the "RHEL Server Optional"
-channel in RHN to get some of those packages. 
-
-If you plan to use a database other than MySQL, you will need to also install
+If you want to install a version of Bugzilla from the Bugzilla project
+or have it on RHEL or CentOS, you will need to do the following instead:
+
+On CentOS Stream and RHEl, add the Fedora EPEL repo, in the way described
+in the `installation instructions <https://docs.fedoraproject.org/en-US/epel/>`_.
+
+Run the following to install the base Bugzilla dependencies:
+
+:command:`dnf install git httpd httpd-devel mariadb-devel gcc
+mariadb-server mod_perl mod_perl-devel 'perl(autodie)' 'perl(CGI)'
+'perl(Date::Format)' 'perl(DateTime)' 'perl(DateTime::TimeZone)'
+'perl(DBI)' 'perl(DBD::mysql)' 'perl(DBIx::Connector)' 'perl(Digest::SHA)'
+'perl(Email::MIME)' 'perl(Email::Sender)' 'perl(fields)' 'perl(JSON::XS)'
+'perl(List::MoreUtils)' 'perl(Math::Random::ISAAC)' 'perl(Memoize)'
+'perl(Safe)' 'perl(Template)' 'perl(URI)'`
+
+On Fedora, all the optional dependencies are available:
+
+:command:`dnf install gd-devel graphviz patchutils
+'perl(Apache2::SizeLimit)' 'perl(Authen::Radius)' 'perl(Authen::SASL)'
+'perl(Cache::Memcached)' 'perl(Chart::Lines)' 'perl(Daemon::Generic)'
+'perl(Email::Reply)' 'perl(Encode)' 'perl(Encode::Detect)'
+'perl(File::Copy::Recursive)' 'perl(File::MimeInfo::Magic)'
+'perl(File::Which)' 'perl(GD)' 'perl(GD::Graph)' 'perl(GD::Text)'
+'perl(HTML::FormatText::WithLinks)' 'perl(HTML::Parser)'
+'perl(HTML::Scrubber)' 'perl(IO::Scalar)' 'perl(JSON::RPC)'
+'perl(LWP::UserAgent)' 'perl(MIME::Parser)' 'perl(mod_perl2)'
+'perl(Net::LDAP)' 'perl(Net::SMTP::SSL)' 'perl(PatchReader)'
+'perl(SOAP::Lite)' 'perl(Template::Plugin::GD::Image)'
+'perl(Test::Taint)' 'perl(TheSchwartz)' 'perl(XMLRPC::Lite)'
+'perl(XML::Twig)'`
+
+On CentOS Stream and RHEL with EPEL, some modules are missing in the
+repositories, so use the following instead:
+
+:command:`dnf install gd-devel graphviz patchutils
+'perl(Apache2::SizeLimit)' 'perl(Authen::Radius)' 'perl(Authen::SASL)'
+'perl(Cache::Memcached)' 'perl(Encode)' 'perl(Encode::Detect)'
+'perl(File::Copy::Recursive)' 'perl(File::MimeInfo::Magic)'
+'perl(File::Which)' 'perl(GD)' 'perl(GD::Graph)' 'perl(GD::Text)'
+'perl(HTML::Parser)' 'perl(HTML::Scrubber)' 'perl(IO::Scalar)'
+'perl(JSON::RPC)' 'perl(LWP::UserAgent)' 'perl(MIME::Parser)'
+'perl(mod_perl2)' 'perl(Net::LDAP)' 'perl(Net::SMTP::SSL)'
+'perl(SOAP::Lite)' 'perl(Test::Taint)' 'perl(XMLRPC::Lite)'
+'perl(XML::Twig)'`
+
+and install the missing optional modules with:
+
+:command:`cd /var/www/html/bugzilla/ && ./install-module.pl Chart::Lines
+Daemon::Generic Email::Reply HTML::FormatText::WithLinks PatchReader
+Template::Plugin::GD::Image TheSchwartz`
+
+If you plan to use a database other than MariaDB, you will need to also install
 the appropriate packages for that.
 
 Ubuntu and Debian
 -----------------
 
-:command:`apt-get install git nano`
-
-:command:`apt-get install apache2 mysql-server libappconfig-perl
-libdate-calc-perl libtemplate-perl libmime-perl build-essential
-libdatetime-timezone-perl libdatetime-perl libemail-sender-perl
-libemail-mime-perl libemail-mime-modifier-perl libdbi-perl libdbd-mysql-perl
-libcgi-pm-perl libmath-random-isaac-perl libmath-random-isaac-xs-perl
-apache2-mpm-prefork libapache2-mod-perl2 libapache2-mod-perl2-dev
-libchart-perl libxml-perl libxml-twig-perl perlmagick libgd-graph-perl
-libtemplate-plugin-gd-perl libsoap-lite-perl libhtml-scrubber-perl
-libjson-rpc-perl libdaemon-generic-perl libtheschwartz-perl
-libtest-taint-perl libauthen-radius-perl libfile-slurp-perl
-libencode-detect-perl libmodule-build-perl libnet-ldap-perl
-libauthen-sasl-perl libtemplate-perl-doc libfile-mimeinfo-perl
-libhtml-formattext-withlinks-perl libgd-dev libmysqlclient-dev lynx-cur
-graphviz python-sphinx`
-
-If you plan to use a database other than MySQL, you will need to also install
-the appropriate packages for that.
+You can install required packages with:
+:command:`apt install apache2 build-essential git
+libcgi-pm-perl libdatetime-perl libdatetime-timezone-perl
+libdbi-perl libdbix-connector-perl libdigest-sha-perl libemail-address-perl
+libemail-mime-perl libemail-sender-perl libjson-xs-perl
+liblist-moreutils-perl libmath-random-isaac-perl libtemplate-perl
+libtimedate-perl liburi-perl libmariadb-dev-compat libdbd-mysql-perl
+mariadb-server`
+
+If you plan to use a database other than MariaDB, you will need to also install
+the appropriate packages for that (in the command above, the packages required
+for MariaDB are ``libdbd-mysql-perl``, ``libmariadb-dev-compat`` and ``mariadb-server``).
+
+You can install optional packages with:
+:command:`apt install graphviz libapache2-mod-perl2
+libapache2-mod-perl2-dev libauthen-radius-perl libauthen-sasl-perl
+libcache-memcached-perl libchart-perl libdaemon-generic-perl
+libemail-reply-perl libencode-detect-perl libencode-perl
+libfile-copy-recursive-perl libfile-mimeinfo-perl libfile-which-perl
+libgd-dev libgd-graph-perl libgd-perl libgd-text-perl
+libhtml-formattext-withlinks-perl libhtml-parser-perl
+libhtml-scrubber-perl libio-stringy-perl libjson-rpc-perl
+libmime-tools-perl libnet-ldap-perl libnet-smtp-ssl-perl
+libsoap-lite-perl libtemplate-plugin-gd-perl libtest-taint-perl
+libtheschwartz-perl libwww-perl libxmlrpc-lite-perl libxml-twig-perl`
+
+There is no Ubuntu package for ``PatchReader`` and so you will have to install that module
+outside the package manager if you want it.
 
 Gentoo
 ------
@@ -98,6 +137,16 @@ USE flag enabled, Bugzilla will end up in :file:`/var/www/localhost/bugzilla`.
 Then, you can skip to :ref:`configuring your database
 <linux-config-database>`.
 
+openSUSE
+--------
+
+:command:`zypper in bugzilla`
+
+has been available in the openSUSE Leap repositories since 15.2 and is
+in the openSUSE Tumbleweed repositories, also comes with an optional
+``bugzilla-apache`` package, that allows you to skip to
+:ref:`configuring your database <linux-config-database>`
+
 .. _linux-install-perl:
 
 Perl
@@ -185,10 +234,11 @@ We have specific configuration instructions for the following:
 Database Engine
 ===============
 
-Bugzilla supports MySQL, PostgreSQL, Oracle and SQLite as database servers.
-You only require one of these systems to make use of Bugzilla. MySQL is
-most commonly used. SQLite is good for trial installations as it requires no
-setup. Configure your server according to the instructions below:
+Bugzilla supports MySQL (or MariaDB, its compatible counterpart), PostgreSQL,
+Oracle and SQLite as database servers. You only require one of these systems
+to make use of Bugzilla. MySQL or MariaDB are most commonly used. SQLite is
+good for trial installations as it requires no setup. Configure your server
+according to the instructions below:
 
 * :ref:`mysql`
 * :ref:`postgresql`
diff --git a/docs/en/rst/installing/mariadb.rst b/docs/en/rst/installing/mariadb.rst
new file mode 100644
index 000000000..d29d11e57
--- /dev/null
+++ b/docs/en/rst/installing/mariadb.rst
@@ -0,0 +1,119 @@
+.. _mariadb:
+
+MariaDB
+#######
+
+The minimum required version is MariaDB 10.0.5.
+
+It's possible to test which version of MariaDB you have installed with:
+
+:command:`mariadb -e 'select version()'`
+
+For MariaDB versions prior to 10.4.6, replace the :command:`mariadb` command 
+with :command:`mysql` with the same arguments. 
+
+Installing
+==========
+
+Windows
+-------
+
+Download the MariaDB 32-bit or 64-bit MSI installer from the
+`MariaDB website <https://mariadb.org/download/?t=mariadb&os=windows>`_ (~66 MB).
+
+MariaDB has a standard Windows installer. It's ok to select a the
+default install options. The rest of this documentation assumes assume you
+have installed MariaDB into :file:`C:\\mysql`. Adjust paths appropriately if not.
+
+Linux/Mac OS X
+--------------
+
+The package install instructions given previously should have installed MariaDB
+on your machine, if it didn't come with it already. 
+
+If you did install MariaDB manually rather than from a package, make sure the
+server is started when the machine boots.
+
+.. _mariadb-add-user:
+
+Add a User
+==========
+
+You need to add a new MariaDB user for Bugzilla to use. Run the :file:`mariadb`
+command-line client and enter:
+
+::
+
+    CREATE USER 'bugs'@'localhost' IDENTIFIED BY '$DB_PASS';
+
+    GRANT SELECT, INSERT,
+    UPDATE, DELETE, INDEX, ALTER, CREATE, LOCK TABLES,
+    CREATE TEMPORARY TABLES, DROP, REFERENCES ON bugs.*
+    TO 'bugs'@'localhost';
+
+You need to replace ``$DB_PASS`` with a strong password you have chosen.
+Write that password down somewhere.
+
+The above command permits an account called ``bugs``
+to connect from the local machine, ``localhost``. Modify the command to
+reflect your setup if you will be connecting from another
+machine or as a different user.
+
+Change Configuration
+====================
+
+To change MariaDB's configuration, you need to edit your MariaDB
+configuration file, which is:
+
+* Red Hat/Fedora: :file:`/etc/my.cnf`
+* Debian/Ubuntu: :file:`/etc/mysql/my.cnf` 
+* Windows: :file:`C:\\mysql\\bin\\my.ini`
+* Mac OS X: :file:`/etc/my.cnf`
+
+Or :file:`mariadb.cnf` on Unix-like operating systems.
+
+.. _mariadb-max-allowed-packet:
+
+Allow Large Attachments and Many Comments
+-----------------------------------------
+
+By default on some systems, MariaDB will only allow you to insert things
+into the database that are smaller than 1MB.
+
+Bugzilla attachments
+may be larger than this. Also, Bugzilla combines all comments
+on a single bug into one field for full-text searching, and the
+combination of all comments on a single bug could in some cases
+be larger than 1MB.
+
+We recommend that you allow at least 16MB packets by
+adding or altering the ``max_allowed_packet`` parameter in your MariaDB
+configuration in the ``[mysqld]`` section, so that the number is at least
+16M, like this (note that it's ``M``, not ``MB``):
+
+::
+
+    [mysqld]
+    # Allow packets up to 16M
+    max_allowed_packet=16M
+
+.. _mariadb-small-words:
+
+Allow Small Words in Full-Text Indexes
+--------------------------------------
+
+By default, words must be at least four characters in length
+in order to be indexed by MariaDB's full-text indexes. This causes
+a lot of Bugzilla-specific words to be missed, including "cc",
+"ftp" and "uri".
+
+MariaDB can be configured to index those words by setting the
+``ft_min_word_len`` param to the minimum size of the words to index.
+
+::
+
+    [mysqld]
+    # Allow small words in full-text indexes
+    ft_min_word_len=2
+
+.. _mariadb-attach-table-size:
diff --git a/docs/en/rst/installing/mysql.rst b/docs/en/rst/installing/mysql.rst
index 1107547a6..10e42d322 100644
--- a/docs/en/rst/installing/mysql.rst
+++ b/docs/en/rst/installing/mysql.rst
@@ -3,7 +3,7 @@
 MySQL
 #####
 
-You need MySQL version 5.0.15 or higher.
+The minimum required version is MySQL 5.6.12.
 
 It's possible to test which version of MySQL you have installed with:
 
@@ -15,12 +15,14 @@ Installing
 Windows
 -------
 
-Download the MySQL 32-bit or 64-bit MSI installer from the
-`MySQL website <http://www.mysql.com/downloads/mysql/>`_ (~28 MB).
+Download the MySQL Installer for Windows from the
+[MySQL website](https://dev.mysql.com/downloads/windows/installer/) 
+(sizes vary based on the installer version).
 
-MySQL has a standard Windows installer. It's ok to select a Typical MySQL
-install (the default). The rest of this documentation assumes assume you
-have installed MySQL into :file:`C:\\mysql`. Adjust paths appropriately if not.
+MySQL provides a standard Windows installer. It is recommended to select the 
+default installation options unless you have specific requirements. This 
+documentation assumes that you have installed MySQL in `C:\mysql`. Adjust paths 
+appropriately if your installation directory differs.
 
 Linux/Mac OS X
 --------------
@@ -35,6 +37,17 @@ and follow its advice.
 If you did install MySQL manually rather than from a package, make sure the
 server is started when the machine boots.
 
+Create the Database
+===================
+
+You need to create a database for Bugzilla to use. Run the :file:`mysql`
+command-line client and enter:
+
+::
+    CREATE DATABASE IF NOT EXISTS bugs CHARACTER SET = 'utf8';
+
+The above command makes sure a database like that doesn't exist already.
+
 .. _mysql-add-user:
 
 Add a User
@@ -69,7 +82,7 @@ configuration file, which is:
 * Red Hat/Fedora: :file:`/etc/my.cnf`
 * Debian/Ubuntu: :file:`/etc/mysql/my.cnf`
 * Windows: :file:`C:\\mysql\\bin\\my.ini`
-* Mac OS X: :file:`/etc/my/cnf`
+* Mac OS X: :file:`/etc/my.cnf`
 
 .. _mysql-max-allowed-packet:
 
@@ -130,6 +143,7 @@ command-line client and enter the following, replacing ``$bugs_db``
 with your Bugzilla database name (which is ``bugs`` by default):
 
 .. code-block:: sql
+   :force:
 
     USE $bugs_db;
     
diff --git a/docs/en/rst/installing/quick-start.rst b/docs/en/rst/installing/quick-start.rst
index 7ea5ed58a..5b58563ad 100644
--- a/docs/en/rst/installing/quick-start.rst
+++ b/docs/en/rst/installing/quick-start.rst
@@ -1,40 +1,35 @@
 .. _quick-start:
 
-Quick Start (Ubuntu Linux 14.04)
+Quick Start (Ubuntu Linux 22.04)
 ################################
 
 This quick start guide makes installing Bugzilla as simple as possible for
 those who are able to choose their environment. It creates a system using
-Ubuntu Linux 14.04 LTS, Apache and MySQL. It requires a little familiarity
+Ubuntu Linux 22.04 LTS, Apache and MariaDB. It requires a little familiarity
 with Linux and the command line.
 
 Obtain Your Hardware
 ====================
 
-Ubuntu 14.04 LTS Server requires a 64-bit processor.
+Ubuntu 22.04 LTS Server requires a 64-bit processor.
 Bugzilla itself has no prerequisites beyond that, although you should pick
 reliable hardware. You can also probably use any 64-bit virtual machine
-or cloud instance that you have root access on. 
+or cloud instance that you have root access on.
 
 Install the OS
 ==============
 
-Get `Ubuntu Server 14.04 LTS <http://www.ubuntu.com/download/server>`_
-and follow the `installation instructions <http://www.ubuntu.com/download/server/install-ubuntu-server>`_.
+Get `Ubuntu Server 22.04 LTS <https://www.ubuntu.com/download/server>`_
+and follow the `installation instructions <https://www.ubuntu.com/download/server/install-ubuntu-server>`_.
 Here are some tips:
 
-* Choose any server name you like.
-* When creating the initial Linux user, call it ``bugzilla``, give it a 
-  strong password, and write that password down.
-* You do not need an encrypted home directory.
+* You do not need an encrypted lvm group, root or home directory.
 * Choose all the defaults for the "partitioning" part (excepting of course
   where the default is "No" and you need to press "Yes" to continue).
-* Choose "install security updates automatically" unless you want to do
-  them manually.
-* From the install options, choose "OpenSSH Server" and "LAMP Server".
-* Set the password for the MySQL root user to a strong password, and write
-  that password down.
-* Install the Grub boot loader to the Master Boot Record.
+* Choose any server name you like.
+* When creating the initial Linux user, call it ``bugzilla``, give it a
+  strong password, and write that password down.
+* From the install options, choose "OpenSSH Server".
 
 Reboot when the installer finishes.
 
@@ -44,59 +39,56 @@ Become root
 ssh to the machine as the 'bugzilla' user, or start a console. Then:
 
 :command:`sudo su`
-   
+
 Install Prerequisites
 =====================
 
-:command:`apt-get install git nano`
-
-:command:`apt-get install apache2 mysql-server libappconfig-perl libdate-calc-perl libtemplate-perl libmime-perl build-essential libdatetime-timezone-perl libdatetime-perl libemail-sender-perl libemail-mime-perl libemail-mime-modifier-perl libdbi-perl libdbd-mysql-perl libcgi-pm-perl libmath-random-isaac-perl libmath-random-isaac-xs-perl apache2-mpm-prefork libapache2-mod-perl2 libapache2-mod-perl2-dev libchart-perl libxml-perl libxml-twig-perl perlmagick libgd-graph-perl libtemplate-plugin-gd-perl libsoap-lite-perl libhtml-scrubber-perl libjson-rpc-perl libdaemon-generic-perl libtheschwartz-perl libtest-taint-perl libauthen-radius-perl libfile-slurp-perl libencode-detect-perl libmodule-build-perl libnet-ldap-perl libauthen-sasl-perl libtemplate-perl-doc libfile-mimeinfo-perl libhtml-formattext-withlinks-perl libgd-dev libmysqlclient-dev lynx-cur graphviz python-sphinx`
+:command:`apt install git nano`
+
+:command:`apt install apache2 build-essential mariadb-server
+libcgi-pm-perl libdigest-sha-perl libtimedate-perl libdatetime-perl
+libdatetime-timezone-perl libdbi-perl libdbix-connector-perl libtemplate-perl
+libemail-address-perl libemail-sender-perl libemail-mime-perl
+liburi-perl liblist-moreutils-perl libmath-random-isaac-perl
+libjson-xs-perl libgd-perl libchart-perl libtemplate-plugin-gd-perl
+libgd-text-perl libgd-graph-perl libmime-tools-perl libwww-perl
+libxml-twig-perl libnet-ldap-perl libauthen-sasl-perl
+libnet-smtp-ssl-perl libauthen-radius-perl libsoap-lite-perl
+libxmlrpc-lite-perl libjson-rpc-perl libtest-taint-perl
+libhtml-parser-perl libhtml-scrubber-perl libencode-perl
+libencode-detect-perl libemail-reply-perl
+libhtml-formattext-withlinks-perl libtheschwartz-perl
+libdaemon-generic-perl libapache2-mod-perl2 libapache2-mod-perl2-dev
+libfile-mimeinfo-perl libio-stringy-perl libcache-memcached-perl
+libfile-copy-recursive-perl libfile-which-perl libmariadb-dev
+perlmagick lynx graphviz python3-sphinx rst2pdf`
 
 This will take a little while. It's split into two commands so you can do
 the next steps (up to step 7) in another terminal while you wait for the
 second command to finish. If you start another terminal, you will need to
 :command:`sudo su` again.
 
-Download Bugzilla
+Configure MariaDB
 =================
 
-Get it from our Git repository:
-
-:command:`cd /var/www/html`
-
-:command:`git clone --branch release-X.X-stable https://github.com/bugzilla/bugzilla bugzilla`
-
-(where "X.X" is the 2-digit version number of the stable release of Bugzilla
-that you want - e.g. 5.0)
-
-Configure MySQL
-===============
-
-The following instructions use the simple :file:`nano` editor, but feel
-free to use any text editor you are comfortable with.
+Put in the server settings that Bugzilla needs:
 
-:command:`nano /etc/mysql/my.cnf`
+:command:`cp docker/mysql/bugzilla.cnf /etc/mysql/conf.d/bugzilla.cnf`
 
-Set the following values, which increase the maximum attachment size and
-make it possible to search for short words and terms:
+Then, add a user to MariaDB for Bugzilla to use:
 
-* Alter on Line 52: ``max_allowed_packet=100M``
-* Add as new line 32, in the ``[mysqld]`` section: ``ft_min_word_len=2``
+:command:`mariadb -u root -e "CREATE USER bugs@localhost IDENTIFIED BY '$db_pass'"`
 
-Save and exit.
-
-Then, add a user to MySQL for Bugzilla to use:
+Replace ``$db_pass`` with a strong password you have generated. Write it down.
+You should make ``$db_pass`` different than your login password.
 
-:command:`mysql -u root -p -e "GRANT ALL PRIVILEGES ON bugs.* TO bugs@localhost IDENTIFIED BY '$db_pass'"`
+Next, give the Bugzilla user access to create and manage the Bugzilla database:
 
-Replace ``$db_pass`` with a strong password you have generated. Write it down.
-When you run the above command, it will prompt you for the MySQL root password
-that you configured when you installed Ubuntu. You should make ``$db_pass``
-different to that password.
+:command:`mariadb -u root -e "GRANT ALL PRIVILEGES ON bugs.* TO bugs@localhost"`
 
-Restart MySQL:
+Restart MariaDB:
 
-:command:`service mysql restart`
+:command:`systemctl restart mariadb`
 
 Configure Apache
 ================
@@ -107,21 +99,49 @@ Paste in the following and save:
 
 .. code-block:: apache
 
- ServerName localhost
-
- <Directory /var/www/html/bugzilla>
+ Alias /bugzilla /var/www/webapps/bugzilla
+ <Directory /var/www/webapps/bugzilla>
    AddHandler cgi-script .cgi
    Options +ExecCGI
    DirectoryIndex index.cgi index.html
    AllowOverride All
  </Directory>
 
+This configuration sets up Bugzilla to be served on your server under ``/bugzilla`` path.
+For more in depth setup instructions, refer to :ref:`Apache section of this documentation <apache>`.
+
 :command:`a2ensite bugzilla`
 
-:command:`a2enmod cgi headers expires`
+:command:`a2enmod cgid headers expires rewrite`
 
 :command:`service apache2 restart`
 
+Download Bugzilla
+=================
+
+Get it from our Git repository:
+
+:command:`mkdir -p /var/www/webapps`
+
+:command:`cd /var/www/webapps`
+
+:command:`git clone --branch release-X.X-stable https://github.com/bugzilla/bugzilla bugzilla`
+
+(where "X.X" is the 2-digit version number of the stable release of Bugzilla
+that you want - e.g. 5.2)
+
+Install Additional Perl Modules
+===============================
+
+Bugzilla requires newer versions than Ubuntu ships of a few Perl
+modules. Bugzilla includes a :file:`install-module.pl` script which
+will install them locally inside your Bugzilla directory without
+affecting your system-wide Perl installation.
+
+:command:`cd /var/www/webapps/bugzilla`
+
+:command:`./install-module.pl Template Email::Sender Email::Address::XS DBD::MariaDB`
+
 Check Setup
 ===========
 
@@ -131,8 +151,6 @@ generates a config file (called :file:`localconfig`) for the database
 access information, and the second time (step 10)
 it uses the info you put in the config file to set up the database.
 
-:command:`cd /var/www/html/bugzilla`
-
 :command:`./checksetup.pl`
 
 Edit :file:`localconfig`
@@ -143,8 +161,9 @@ Edit :file:`localconfig`
 You will need to set the following values:
 
 * Line 29: set ``$webservergroup`` to ``www-data``
+* Line 50: set ``db_driver`` to ``mariadb``
 * Line 67: set ``$db_pass`` to the password for the ``bugs`` user you created
-  in MySQL a few steps ago
+  in MariaDB a few steps ago
 
 Check Setup (again)
 ===================
@@ -162,12 +181,10 @@ Test Server
 
 :command:`./testserver.pl http://localhost/bugzilla`
 
-All the tests should pass. You will get warnings about deprecation from
-the ``Chart::Base`` Perl module; just ignore those.
+All the tests should pass. You will get a warning about failing to run
+``gdlib-config``; just ignore it.
 
-.. todo:: Chart::Base gives confusing deprecation warnings :-|
-          https://rt.cpan.org/Public/Bug/Display.html?id=79658 , unfixed for
-          2 years. :bug:`1070117`.
+.. todo:: ``gdlib-config`` is no longer in Ubuntu.
 
 Access Via Web Browser
 ======================
@@ -183,8 +200,28 @@ You might well need to configure your DNS such that the server has, and
 is reachable by, a name rather than IP address. Doing so is out of scope
 of this document. In the mean time, it is available on your local network
 at ``http://<ip address>/bugzilla``, where ``<ip address>`` is (unless you
-have a complex network setup) the "inet addr" value displayed when you run
-:command:`ifconfig eth0`.
+have a complex network setup) the address starting with 192 displayed when
+you run :command:`hostname -I`.
+
+Accessing Bugzilla from the Internet
+====================================
+
+To be able to access Bugzilla from anywhere in the world, you don't have
+to make it internet facing at all, there are free VPN services that let
+you set up your own network that is accessible anywhere. One of those is
+Tailscale, which has a fairly accessible `Quick Start guide <https://tailscale.com/kb/1017/install/>`_.
+
+If you are setting up an internet facing Bugzilla, it's essential to set
+up SSL, so that the communication between the server and users is
+encrypted. For local and intranet installation this matters less, and
+for those cases, you could set up a self signed local certificate
+instead.
+
+There are a few ways to set up free SSL thanks to `Let's Encrypt <https://letsencrypt.org/>`_.
+The two major ones would be Apache's `mod_md <https://httpd.apache.org/docs/2.4/mod/mod_md.html>`_
+and EFF's `certbot <https://certbot.eff.org/instructions?ws=apache&os=ubuntufocal>`_,
+but we don't cover the exact specifics of this here, as that's out of
+scope of this guide.
 
 Configure Bugzilla
 ==================
@@ -198,6 +235,8 @@ the following parameters in the :guilabel:`Required Settings` section:
 
 * :param:`urlbase`:
   :paramval:`http://<servername>/bugzilla/` or :paramval:`http://<ip address>/bugzilla/`
+* :param:`ssl_redirect`:
+  :paramval:`on` if you set up an SSL certificate
 
 Click :guilabel:`Save Changes` at the bottom of the page.
 
diff --git a/docs/lib/Pod/Simple/HTML/Bugzilla.pm b/docs/lib/Pod/Simple/HTML/Bugzilla.pm
index 5a2203473..cb29b0bf9 100644
--- a/docs/lib/Pod/Simple/HTML/Bugzilla.pm
+++ b/docs/lib/Pod/Simple/HTML/Bugzilla.pm
@@ -7,11 +7,11 @@
 
 package Pod::Simple::HTML::Bugzilla;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Pod::Simple::HTML);
+use base qw(Pod::Simple::HTML);
 
 # Without this constant, HTMLBatch will throw undef warnings.
 use constant VERSION    => $Pod::Simple::HTML::VERSION;
diff --git a/docs/lib/Pod/Simple/HTMLBatch/Bugzilla.pm b/docs/lib/Pod/Simple/HTMLBatch/Bugzilla.pm
index bc96c9a0e..a156f4d83 100644
--- a/docs/lib/Pod/Simple/HTMLBatch/Bugzilla.pm
+++ b/docs/lib/Pod/Simple/HTMLBatch/Bugzilla.pm
@@ -7,11 +7,11 @@
 
 package Pod::Simple::HTMLBatch::Bugzilla;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Pod::Simple::HTMLBatch);
+use base qw(Pod::Simple::HTMLBatch);
 
 # This is the same hack that HTMLBatch does to "import" this subroutine.
 BEGIN { *esc = \&Pod::Simple::HTML::esc }
diff --git a/docs/makedocs.pl b/docs/makedocs.pl
index 87e346e23..c29b2adcc 100755
--- a/docs/makedocs.pl
+++ b/docs/makedocs.pl
@@ -23,7 +23,7 @@
 # All these TeX packages together are close to a gig :-| But after you've
 # installed them, you can remove texlive-latex-extra-doc to save 400MB.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
@@ -33,6 +33,7 @@ BEGIN { chdir dirname($0); }
 use lib qw(.. ../lib lib);
 
 use Cwd;
+use File::Copy qw(copy);
 use File::Copy::Recursive qw(rcopy);
 use File::Find;
 use File::Path qw(rmtree make_path);
@@ -83,12 +84,19 @@ END_HTML
 
   $converter->contents_page_start($contents_start);
   $converter->contents_page_end("</body></html>");
-  $converter->add_css('./../../../../style.css');
+  if (exists($::ENV{'READTHEDOCS'})) {
+    $converter->add_css('./../../style.css');
+  } else {
+    $converter->add_css('./../../../../style.css');
+  }
   $converter->javascript_flurry(0);
   $converter->css_flurry(0);
   make_path('html/integrating/api');
   $converter->batch_convert(['../../'], 'html/integrating/api');
 
+  if (exists($::ENV{'READTHEDOCS'})) {
+    copy('../style.css', 'html/style.css') or die "Copy failed: $!";
+  }
   print "\n";
 }
 
diff --git a/docs/requirements.txt b/docs/requirements.txt
new file mode 100644
index 000000000..705daff87
--- /dev/null
+++ b/docs/requirements.txt
@@ -0,0 +1,3 @@
+sphinx==5.3.0
+sphinx_rtd_theme==1.3.0
+readthedocs-sphinx-search==0.3.2
diff --git a/duplicates.cgi b/duplicates.cgi
index 5f22d0084..dff002d6d 100755
--- a/duplicates.cgi
+++ b/duplicates.cgi
@@ -6,7 +6,7 @@
 # This Source Code Form is "Incompatible With Secondary Licenses", as
 # defined by the Mozilla Public License, v. 2.0.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/editclassifications.cgi b/editclassifications.cgi
index b2797af3d..b9cb36cbd 100755
--- a/editclassifications.cgi
+++ b/editclassifications.cgi
@@ -7,7 +7,7 @@
 # defined by the Mozilla Public License, v. 2.0.
 
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/editcomponents.cgi b/editcomponents.cgi
index c0e3b30c0..4997bd0d9 100755
--- a/editcomponents.cgi
+++ b/editcomponents.cgi
@@ -6,7 +6,7 @@
 # This Source Code Form is "Incompatible With Secondary Licenses", as
 # defined by the Mozilla Public License, v. 2.0.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/editfields.cgi b/editfields.cgi
index 633887db2..57f32ac2e 100755
--- a/editfields.cgi
+++ b/editfields.cgi
@@ -6,7 +6,7 @@
 # This Source Code Form is "Incompatible With Secondary Licenses", as
 # defined by the Mozilla Public License, v. 2.0.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/editflagtypes.cgi b/editflagtypes.cgi
index edb031fcd..03bbb4424 100755
--- a/editflagtypes.cgi
+++ b/editflagtypes.cgi
@@ -6,7 +6,7 @@
 # This Source Code Form is "Incompatible With Secondary Licenses", as
 # defined by the Mozilla Public License, v. 2.0.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/editgroups.cgi b/editgroups.cgi
index b93254ebc..8197180b8 100755
--- a/editgroups.cgi
+++ b/editgroups.cgi
@@ -6,7 +6,7 @@
 # This Source Code Form is "Incompatible With Secondary Licenses", as
 # defined by the Mozilla Public License, v. 2.0.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
@@ -45,10 +45,11 @@ my $token  = $cgi->param('token');
 sub CheckGroupID {
   my ($group_id) = @_;
   $group_id = trim($group_id || 0);
+  my $dbh = Bugzilla->dbh;
   ThrowUserError("group_not_specified") unless $group_id;
   (
-    detaint_natural($group_id) && Bugzilla->dbh->selectrow_array(
-      "SELECT id FROM groups WHERE id = ?",
+    detaint_natural($group_id) && $dbh->selectrow_array(
+      'SELECT id FROM ' . $dbh->quote_identifier('groups') . ' WHERE id = ?',
       undef, $group_id
     )
   ) || ThrowUserError("invalid_group_ID");
diff --git a/editkeywords.cgi b/editkeywords.cgi
index 6b308c15d..ea55270d7 100755
--- a/editkeywords.cgi
+++ b/editkeywords.cgi
@@ -6,7 +6,7 @@
 # This Source Code Form is "Incompatible With Secondary Licenses", as
 # defined by the Mozilla Public License, v. 2.0.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/editmilestones.cgi b/editmilestones.cgi
index c5f2e8686..2565ccbb5 100755
--- a/editmilestones.cgi
+++ b/editmilestones.cgi
@@ -6,7 +6,7 @@
 # This Source Code Form is "Incompatible With Secondary Licenses", as
 # defined by the Mozilla Public License, v. 2.0.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/editparams.cgi b/editparams.cgi
index eb34bd53a..2ce0de99c 100755
--- a/editparams.cgi
+++ b/editparams.cgi
@@ -6,7 +6,7 @@
 # This Source Code Form is "Incompatible With Secondary Licenses", as
 # defined by the Mozilla Public License, v. 2.0.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/editproducts.cgi b/editproducts.cgi
index f98225e6f..1b1b9b6e5 100755
--- a/editproducts.cgi
+++ b/editproducts.cgi
@@ -6,7 +6,7 @@
 # This Source Code Form is "Incompatible With Secondary Licenses", as
 # defined by the Mozilla Public License, v. 2.0.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
@@ -368,7 +368,7 @@ if ($action eq 'updategroupcontrols') {
                        FROM bugs
                  INNER JOIN bug_group_map
                          ON bug_group_map.bug_id = bugs.bug_id
-                 INNER JOIN groups
+                 INNER JOIN ' . $dbh->quote_identifier('groups') . '
                          ON bug_group_map.group_id = groups.id
                       WHERE groups.id IN (' . join(', ', @now_na) . ')
                         AND bugs.product_id = ? ' . $dbh->sql_group_by('groups.name'),
@@ -390,7 +390,7 @@ if ($action eq 'updategroupcontrols') {
                                 (SELECT bug_group_map.bug_id FROM bug_group_map
                                   WHERE bug_group_map.group_id = groups.id))
                            AS count
-                      FROM groups
+                      FROM ' . $dbh->quote_identifier('groups') . '
                      WHERE groups.id IN (' . join(', ', @now_mandatory) . ')
                      ORDER BY groups.name', {'Slice' => {}}, $product->id
       );
diff --git a/editsettings.cgi b/editsettings.cgi
index 3f0ab6827..4d148a919 100755
--- a/editsettings.cgi
+++ b/editsettings.cgi
@@ -6,7 +6,7 @@
 # This Source Code Form is "Incompatible With Secondary Licenses", as
 # defined by the Mozilla Public License, v. 2.0.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/editusers.cgi b/editusers.cgi
index 4db7e4441..21e078058 100755
--- a/editusers.cgi
+++ b/editusers.cgi
@@ -6,7 +6,7 @@
 # This Source Code Form is "Incompatible With Secondary Licenses", as
 # defined by the Mozilla Public License, v. 2.0.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
@@ -708,16 +708,16 @@ sub userDataToVars {
   $vars->{'groups'}    = $user->bless_groups();
 
   $vars->{'permissions'} = $dbh->selectall_hashref(
-    qq{SELECT id,
+    'SELECT id,
                   COUNT(directmember.group_id) AS directmember,
                   COUNT(regexpmember.group_id) AS regexpmember,
-                  (CASE WHEN (groups.id IN ($grouplist)
+                  (CASE WHEN (groups.id IN (' . $grouplist . ')
                               AND COUNT(directmember.group_id) = 0
                               AND COUNT(regexpmember.group_id) = 0
                              ) THEN 1 ELSE 0 END) 
                       AS derivedmember,
                   COUNT(directbless.group_id) AS directbless
-           FROM groups
+           FROM ' . $dbh->quote_identifier('groups') . '
            LEFT JOIN user_group_map AS directmember
                   ON directmember.group_id = id
                  AND directmember.user_id = ?
@@ -733,7 +733,7 @@ sub userDataToVars {
                  AND directbless.user_id = ?
                  AND directbless.isbless = 1
                  AND directbless.grant_type = ?
-          } . $dbh->sql_group_by('id'),
+          ' . $dbh->sql_group_by('id'),
     'id', undef,
     (
       $otheruserid, GRANT_DIRECT, $otheruserid, GRANT_REGEXP,
@@ -742,12 +742,14 @@ sub userDataToVars {
   );
 
   # Find indirect bless permission.
-  $query = qq{SELECT groups.id
-                FROM groups, group_group_map AS ggm
+  $query = 'SELECT groups.id
+                FROM '
+    . $dbh->quote_identifier('groups')
+    . ', group_group_map AS ggm
                 WHERE groups.id = ggm.grantor_id
-                  AND ggm.member_id IN ($grouplist)
+                  AND ggm.member_id IN (' . $grouplist . ')
                   AND ggm.grant_type = ?
-               } . $dbh->sql_group_by('id');
+               ' . $dbh->sql_group_by('id');
   foreach (@{$dbh->selectall_arrayref($query, undef, (GROUP_BLESS))}) {
 
     # Merge indirect bless permissions into permission variable.
diff --git a/editvalues.cgi b/editvalues.cgi
index 59e4d639f..2827b1937 100755
--- a/editvalues.cgi
+++ b/editvalues.cgi
@@ -6,7 +6,7 @@
 # This Source Code Form is "Incompatible With Secondary Licenses", as
 # defined by the Mozilla Public License, v. 2.0.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/editversions.cgi b/editversions.cgi
index 23de88071..707e998d6 100755
--- a/editversions.cgi
+++ b/editversions.cgi
@@ -6,7 +6,7 @@
 # This Source Code Form is "Incompatible With Secondary Licenses", as
 # defined by the Mozilla Public License, v. 2.0.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/editwhines.cgi b/editwhines.cgi
index c677273bd..4b8da9b6e 100755
--- a/editwhines.cgi
+++ b/editwhines.cgi
@@ -10,7 +10,7 @@
 # Script Initialization
 ################################################################################
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/editworkflow.cgi b/editworkflow.cgi
index 81b665e42..019dfb74f 100755
--- a/editworkflow.cgi
+++ b/editworkflow.cgi
@@ -6,7 +6,7 @@
 # This Source Code Form is "Incompatible With Secondary Licenses", as
 # defined by the Mozilla Public License, v. 2.0.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/email_in.pl b/email_in.pl
index a03935830..eecda81bf 100755
--- a/email_in.pl
+++ b/email_in.pl
@@ -6,7 +6,7 @@
 # This Source Code Form is "Incompatible With Secondary Licenses", as
 # defined by the Mozilla Public License, v. 2.0.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
@@ -24,7 +24,7 @@ BEGIN {
 use lib qw(. lib);
 
 use Data::Dumper;
-use Email::Address;
+use Email::Address::XS;
 use Email::Reply qw(reply);
 use Email::MIME;
 use Getopt::Long qw(:config bundling);
@@ -129,7 +129,7 @@ sub parse_mail {
 
   %fields = %{Bugzilla::Bug::map_fields(\%fields)};
 
-  my ($reporter) = Email::Address->parse($input_email->header('From'));
+  my ($reporter) = Email::Address::XS->parse($input_email->header('From'));
   $fields{'reporter'} = $reporter->address;
 
   # The summary line only affects us if we're doing a post_bug.
diff --git a/enter_bug.cgi b/enter_bug.cgi
index b4f609500..1ec06d02b 100755
--- a/enter_bug.cgi
+++ b/enter_bug.cgi
@@ -16,7 +16,7 @@
 #
 ##############################################################################
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/extensions/BmpConvert/Config.pm b/extensions/BmpConvert/Config.pm
index ed2df6197..aabb8142e 100644
--- a/extensions/BmpConvert/Config.pm
+++ b/extensions/BmpConvert/Config.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::Extension::BmpConvert;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/extensions/BmpConvert/Extension.pm b/extensions/BmpConvert/Extension.pm
index 84a2c81dc..64d011fee 100644
--- a/extensions/BmpConvert/Extension.pm
+++ b/extensions/BmpConvert/Extension.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::Extension::BmpConvert;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Bugzilla::Extension);
+use base qw(Bugzilla::Extension);
 
 use Image::Magick;
 
diff --git a/extensions/Example/Config.pm b/extensions/Example/Config.pm
index 696da2de9..34d6659db 100644
--- a/extensions/Example/Config.pm
+++ b/extensions/Example/Config.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::Extension::Example;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/extensions/Example/Extension.pm b/extensions/Example/Extension.pm
index 1ecb3e692..8201781f7 100644
--- a/extensions/Example/Extension.pm
+++ b/extensions/Example/Extension.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::Extension::Example;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Bugzilla::Extension);
+use base qw(Bugzilla::Extension);
 
 use Bugzilla::Constants;
 use Bugzilla::Error;
diff --git a/extensions/Example/lib/Auth/Login.pm b/extensions/Example/lib/Auth/Login.pm
index 49af42c22..71a0b7371 100644
--- a/extensions/Example/lib/Auth/Login.pm
+++ b/extensions/Example/lib/Auth/Login.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::Extension::Example::Auth::Login;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Bugzilla::Auth::Login);
+use base qw(Bugzilla::Auth::Login);
 use constant user_can_create_account => 0;
 use Bugzilla::Constants;
 
diff --git a/extensions/Example/lib/Auth/Verify.pm b/extensions/Example/lib/Auth/Verify.pm
index 591c1d4f8..401a00315 100644
--- a/extensions/Example/lib/Auth/Verify.pm
+++ b/extensions/Example/lib/Auth/Verify.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::Extension::Example::Auth::Verify;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Bugzilla::Auth::Verify);
+use base qw(Bugzilla::Auth::Verify);
 use Bugzilla::Constants;
 
 # A verifier that always fails.
diff --git a/extensions/Example/lib/Config.pm b/extensions/Example/lib/Config.pm
index 360a57510..d7d7ace72 100644
--- a/extensions/Example/lib/Config.pm
+++ b/extensions/Example/lib/Config.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::Extension::Example::Config;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/extensions/Example/lib/Util.pm b/extensions/Example/lib/Util.pm
index ccc349c9c..88a9a5a84 100644
--- a/extensions/Example/lib/Util.pm
+++ b/extensions/Example/lib/Util.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::Extension::Example::Util;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/extensions/Example/lib/WebService.pm b/extensions/Example/lib/WebService.pm
index f461fee6f..b11edc578 100644
--- a/extensions/Example/lib/WebService.pm
+++ b/extensions/Example/lib/WebService.pm
@@ -7,10 +7,10 @@
 
 package Bugzilla::Extension::Example::WebService;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
-use parent qw(Bugzilla::WebService);
+use base qw(Bugzilla::WebService);
 use Bugzilla::Error;
 
 use constant PUBLIC_METHODS => qw(
diff --git a/extensions/MoreBugUrl/Config.pm b/extensions/MoreBugUrl/Config.pm
index f8175bb4c..0fea92c0f 100644
--- a/extensions/MoreBugUrl/Config.pm
+++ b/extensions/MoreBugUrl/Config.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::Extension::MoreBugUrl;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/extensions/MoreBugUrl/Extension.pm b/extensions/MoreBugUrl/Extension.pm
index a6eae1be5..42244d0d9 100644
--- a/extensions/MoreBugUrl/Extension.pm
+++ b/extensions/MoreBugUrl/Extension.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::Extension::MoreBugUrl;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Bugzilla::Extension);
+use base qw(Bugzilla::Extension);
 
 use constant MORE_SUB_CLASSES => qw(
   Bugzilla::Extension::MoreBugUrl::BitBucket
@@ -22,6 +22,7 @@ use constant MORE_SUB_CLASSES => qw(
   Bugzilla::Extension::MoreBugUrl::PHP
   Bugzilla::Extension::MoreBugUrl::Redmine
   Bugzilla::Extension::MoreBugUrl::Savane
+  Bugzilla::Extension::MoreBugUrl::WineHQForums
 );
 
 # We need to update bug_see_also table because both
diff --git a/extensions/MoreBugUrl/lib/BitBucket.pm b/extensions/MoreBugUrl/lib/BitBucket.pm
index 683f9aeb3..6a3809298 100644
--- a/extensions/MoreBugUrl/lib/BitBucket.pm
+++ b/extensions/MoreBugUrl/lib/BitBucket.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::Extension::MoreBugUrl::BitBucket;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Bugzilla::BugUrl);
+use base qw(Bugzilla::BugUrl);
 
 ###############################
 ####        Methods        ####
diff --git a/extensions/MoreBugUrl/lib/GetSatisfaction.pm b/extensions/MoreBugUrl/lib/GetSatisfaction.pm
index 34c86b65f..6d62b9b6a 100644
--- a/extensions/MoreBugUrl/lib/GetSatisfaction.pm
+++ b/extensions/MoreBugUrl/lib/GetSatisfaction.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::Extension::MoreBugUrl::GetSatisfaction;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Bugzilla::BugUrl);
+use base qw(Bugzilla::BugUrl);
 
 ###############################
 ####        Methods        ####
diff --git a/extensions/MoreBugUrl/lib/PHP.pm b/extensions/MoreBugUrl/lib/PHP.pm
index 9419bcd14..e326d8222 100644
--- a/extensions/MoreBugUrl/lib/PHP.pm
+++ b/extensions/MoreBugUrl/lib/PHP.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::Extension::MoreBugUrl::PHP;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Bugzilla::BugUrl);
+use base qw(Bugzilla::BugUrl);
 
 ###############################
 ####        Methods        ####
diff --git a/extensions/MoreBugUrl/lib/RT.pm b/extensions/MoreBugUrl/lib/RT.pm
index 54dc2dd4c..d23675d6e 100644
--- a/extensions/MoreBugUrl/lib/RT.pm
+++ b/extensions/MoreBugUrl/lib/RT.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::Extension::MoreBugUrl::RT;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Bugzilla::BugUrl);
+use base qw(Bugzilla::BugUrl);
 
 ###############################
 ####        Methods        ####
diff --git a/extensions/MoreBugUrl/lib/Redmine.pm b/extensions/MoreBugUrl/lib/Redmine.pm
index 712dab197..e21ba2e56 100644
--- a/extensions/MoreBugUrl/lib/Redmine.pm
+++ b/extensions/MoreBugUrl/lib/Redmine.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::Extension::MoreBugUrl::Redmine;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Bugzilla::BugUrl);
+use base qw(Bugzilla::BugUrl);
 
 ###############################
 ####        Methods        ####
diff --git a/extensions/MoreBugUrl/lib/ReviewBoard.pm b/extensions/MoreBugUrl/lib/ReviewBoard.pm
index 48cbecf11..1da4c4c9e 100644
--- a/extensions/MoreBugUrl/lib/ReviewBoard.pm
+++ b/extensions/MoreBugUrl/lib/ReviewBoard.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::Extension::MoreBugUrl::ReviewBoard;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Bugzilla::BugUrl);
+use base qw(Bugzilla::BugUrl);
 
 ###############################
 ####        Methods        ####
diff --git a/extensions/MoreBugUrl/lib/Rietveld.pm b/extensions/MoreBugUrl/lib/Rietveld.pm
index 55a18b0b2..7febde237 100644
--- a/extensions/MoreBugUrl/lib/Rietveld.pm
+++ b/extensions/MoreBugUrl/lib/Rietveld.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::Extension::MoreBugUrl::Rietveld;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Bugzilla::BugUrl);
+use base qw(Bugzilla::BugUrl);
 
 ###############################
 ####        Methods        ####
diff --git a/extensions/MoreBugUrl/lib/Savane.pm b/extensions/MoreBugUrl/lib/Savane.pm
index ff0cd657f..69fa10a9d 100644
--- a/extensions/MoreBugUrl/lib/Savane.pm
+++ b/extensions/MoreBugUrl/lib/Savane.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::Extension::MoreBugUrl::Savane;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Bugzilla::BugUrl);
+use base qw(Bugzilla::BugUrl);
 
 ###############################
 ####        Methods        ####
diff --git a/extensions/MoreBugUrl/template/en/default/hook/global/user-error-bug_url_invalid_tracker.html.tmpl b/extensions/MoreBugUrl/template/en/default/hook/global/user-error-bug_url_invalid_tracker.html.tmpl
index 7e544ef21..d8c8be479 100644
--- a/extensions/MoreBugUrl/template/en/default/hook/global/user-error-bug_url_invalid_tracker.html.tmpl
+++ b/extensions/MoreBugUrl/template/en/default/hook/global/user-error-bug_url_invalid_tracker.html.tmpl
@@ -14,3 +14,4 @@
 <li>A b[% %]ug on b[% %]ugs.php.net.</li>
 <li>An issue in a Redmine installation.</li>
 <li>A b[% %]ug in a Savane installation.</li>
+<li>A b[% %]ug related topic on WineHQ forums</li>
diff --git a/extensions/OldBugMove/Config.pm b/extensions/OldBugMove/Config.pm
index 681cbae2d..851e4ea0b 100644
--- a/extensions/OldBugMove/Config.pm
+++ b/extensions/OldBugMove/Config.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::Extension::OldBugMove;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/extensions/OldBugMove/Extension.pm b/extensions/OldBugMove/Extension.pm
index 70dc5ad30..05f77f2fd 100644
--- a/extensions/OldBugMove/Extension.pm
+++ b/extensions/OldBugMove/Extension.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::Extension::OldBugMove;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Bugzilla::Extension);
+use base qw(Bugzilla::Extension);
 use Bugzilla::Constants;
 use Bugzilla::Error;
 use Bugzilla::Field::Choice;
@@ -183,7 +183,7 @@ sub _move_bug {
   my @fieldlist = (Bugzilla::Bug->fields, 'group', 'long_desc', 'attachment',
     'attachmentdata');
   my %displayfields = map { $_ => 1 } @fieldlist;
-  my $vars = {bugs => [$export_me], displayfields => \%displayfields};
+  my $vars          = {bugs => [$export_me], displayfields => \%displayfields};
   $template->process("bug/show.xml.tmpl", $vars, \$msg)
     || ThrowTemplateError($template->error());
   $msg .= "\n";
diff --git a/extensions/OldBugMove/lib/Params.pm b/extensions/OldBugMove/lib/Params.pm
index e1bab048b..f6a61c0eb 100644
--- a/extensions/OldBugMove/lib/Params.pm
+++ b/extensions/OldBugMove/lib/Params.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::Extension::OldBugMove::Params;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/extensions/Voting/Config.pm b/extensions/Voting/Config.pm
index d72caa3cc..81625f85c 100644
--- a/extensions/Voting/Config.pm
+++ b/extensions/Voting/Config.pm
@@ -7,7 +7,7 @@
 
 package Bugzilla::Extension::Voting;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/extensions/Voting/Extension.pm b/extensions/Voting/Extension.pm
index a46162ed2..f8674837a 100644
--- a/extensions/Voting/Extension.pm
+++ b/extensions/Voting/Extension.pm
@@ -7,11 +7,11 @@
 
 package Bugzilla::Extension::Voting;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Bugzilla::Extension);
+use base qw(Bugzilla::Extension);
 
 use Bugzilla::Bug;
 use Bugzilla::BugMail;
diff --git a/extensions/create.pl b/extensions/create.pl
index f2aa3f933..60667b47c 100755
--- a/extensions/create.pl
+++ b/extensions/create.pl
@@ -6,7 +6,7 @@
 # This Source Code Form is "Incompatible With Secondary Licenses", as
 # defined by the Mozilla Public License, v. 2.0.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/images/favicon.ico b/images/favicon.ico
index c8ade39ad..80975c97e 100644
Binary files a/images/favicon.ico and b/images/favicon.ico differ
diff --git a/importxml.pl b/importxml.pl
index 96264cb0a..3be5bd2b7 100755
--- a/importxml.pl
+++ b/importxml.pl
@@ -10,7 +10,7 @@
 # a new bug into bugzilla. Everything before the beginning <?xml line
 # is removed so you can pipe in email messages.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
@@ -595,10 +595,10 @@ sub process_bug {
     format_time($bug_fields{'delta_ts'}, "%Y-%m-%d %T") || $timestamp);
 
   # Bug Access
-  push(@query, "cclist_accessible");
+  push(@query,  "cclist_accessible");
   push(@values, $bug_fields{'cclist_accessible'} ? 1 : 0);
 
-  push(@query, "reporter_accessible");
+  push(@query,  "reporter_accessible");
   push(@values, $bug_fields{'reporter_accessible'} ? 1 : 0);
 
   my $product = new Bugzilla::Product({name => $bug_fields{'product'} || ''});
diff --git a/index.cgi b/index.cgi
index 474c5c7ce..9985a94c5 100755
--- a/index.cgi
+++ b/index.cgi
@@ -6,7 +6,7 @@
 # This Source Code Form is "Incompatible With Secondary Licenses", as
 # defined by the Mozilla Public License, v. 2.0.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/install-module.pl b/install-module.pl
index 972b4baca..40ec32dca 100755
--- a/install-module.pl
+++ b/install-module.pl
@@ -6,7 +6,7 @@
 # This Source Code Form is "Incompatible With Secondary Licenses", as
 # defined by the Mozilla Public License, v. 2.0.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/jobqueue.pl b/jobqueue.pl
index 310cf4d26..913a0e0af 100755
--- a/jobqueue.pl
+++ b/jobqueue.pl
@@ -6,7 +6,7 @@
 # This Source Code Form is "Incompatible With Secondary Licenses", as
 # defined by the Mozilla Public License, v. 2.0.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/jsonrpc.cgi b/jsonrpc.cgi
index 6933e26cb..bbb058f4c 100755
--- a/jsonrpc.cgi
+++ b/jsonrpc.cgi
@@ -6,7 +6,7 @@
 # This Source Code Form is "Incompatible With Secondary Licenses", as
 # defined by the Mozilla Public License, v. 2.0.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/migrate.pl b/migrate.pl
index 061509027..aa46b1b02 100755
--- a/migrate.pl
+++ b/migrate.pl
@@ -6,7 +6,7 @@
 # This Source Code Form is "Incompatible With Secondary Licenses", as
 # defined by the Mozilla Public License, v. 2.0.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/mod_perl.pl b/mod_perl.pl
index 98f3365ca..8bd89e9f3 100644
--- a/mod_perl.pl
+++ b/mod_perl.pl
@@ -8,7 +8,7 @@
 
 package Bugzilla::ModPerl;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
@@ -113,7 +113,7 @@ foreach my $file (glob "$cgi_path/*.cgi") {
 
 package Bugzilla::ModPerl::ResponseHandler;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
@@ -149,7 +149,7 @@ sub handler : method {
 
 package Bugzilla::ModPerl::CleanupHandler;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/page.cgi b/page.cgi
index 930eb58df..eefffb9ac 100755
--- a/page.cgi
+++ b/page.cgi
@@ -13,7 +13,7 @@
 # a content-type, e.g. html.
 ###############################################################################
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/post_bug.cgi b/post_bug.cgi
index 757db7fdf..aa639a1e4 100755
--- a/post_bug.cgi
+++ b/post_bug.cgi
@@ -6,7 +6,7 @@
 # This Source Code Form is "Incompatible With Secondary Licenses", as
 # defined by the Mozilla Public License, v. 2.0.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/process_bug.cgi b/process_bug.cgi
index 3adce62c1..17ed5a1ee 100755
--- a/process_bug.cgi
+++ b/process_bug.cgi
@@ -6,7 +6,7 @@
 # This Source Code Form is "Incompatible With Secondary Licenses", as
 # defined by the Mozilla Public License, v. 2.0.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/query.cgi b/query.cgi
index 0c176f987..d7c217788 100755
--- a/query.cgi
+++ b/query.cgi
@@ -6,7 +6,7 @@
 # This Source Code Form is "Incompatible With Secondary Licenses", as
 # defined by the Mozilla Public License, v. 2.0.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/quips.cgi b/quips.cgi
index 671ec1f09..98d05d945 100755
--- a/quips.cgi
+++ b/quips.cgi
@@ -6,7 +6,7 @@
 # This Source Code Form is "Incompatible With Secondary Licenses", as
 # defined by the Mozilla Public License, v. 2.0.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/relogin.cgi b/relogin.cgi
index 2cd9bb25c..bc9eb1c87 100755
--- a/relogin.cgi
+++ b/relogin.cgi
@@ -6,7 +6,7 @@
 # This Source Code Form is "Incompatible With Secondary Licenses", as
 # defined by the Mozilla Public License, v. 2.0.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/report.cgi b/report.cgi
index 7895115b3..6aebe20dc 100755
--- a/report.cgi
+++ b/report.cgi
@@ -6,7 +6,7 @@
 # This Source Code Form is "Incompatible With Secondary Licenses", as
 # defined by the Mozilla Public License, v. 2.0.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
@@ -312,13 +312,6 @@ $vars->{'height'}          = $height;
 $vars->{'queries'}         = $extra_data;
 $vars->{'saved_report_id'} = $cgi->param('saved_report_id');
 
-if ( $cgi->param('debug')
-  && Bugzilla->params->{debug_group}
-  && Bugzilla->user->in_group(Bugzilla->params->{debug_group}))
-{
-  $vars->{'debug'} = 1;
-}
-
 if ($action eq "wrap") {
 
   # So which template are we using? If action is "wrap", we will be using
@@ -367,24 +360,9 @@ else {
 my $format = $template->get_format("reports/report", $formatparam,
   scalar($cgi->param('ctype')));
 
-# If we get a template or CGI error, it comes out as HTML, which isn't valid
-# PNG data, and the browser just displays a "corrupt PNG" message. So, you can
-# set debug=1 to always get an HTML content-type, and view the error.
-$format->{'ctype'} = "text/html" if $cgi->param('debug');
-
 $cgi->set_dated_content_disp("inline", "report", $format->{extension});
 print $cgi->header($format->{'ctype'});
 
-# Problems with this CGI are often due to malformed data. Setting debug=1
-# prints out both data structures.
-if ($cgi->param('debug')) {
-  require Data::Dumper;
-  say "<pre>data hash:";
-  say html_quote(Data::Dumper::Dumper(%data));
-  say "\ndata array:";
-  say html_quote(Data::Dumper::Dumper(@image_data)) . "\n\n</pre>";
-}
-
 # All formats point to the same section of the documentation.
 $vars->{'doc_section'} = 'using/reports-and-charts.html#reports';
 
diff --git a/reports.cgi b/reports.cgi
index 860a537d8..66873e40b 100755
--- a/reports.cgi
+++ b/reports.cgi
@@ -6,7 +6,7 @@
 # This Source Code Form is "Incompatible With Secondary Licenses", as
 # defined by the Mozilla Public License, v. 2.0.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/request.cgi b/request.cgi
index 97978ccad..a95ac5399 100755
--- a/request.cgi
+++ b/request.cgi
@@ -6,7 +6,7 @@
 # This Source Code Form is "Incompatible With Secondary Licenses", as
 # defined by the Mozilla Public License, v. 2.0.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/rest.cgi b/rest.cgi
index 892cb62dd..d07957d7f 100755
--- a/rest.cgi
+++ b/rest.cgi
@@ -6,7 +6,7 @@
 # This Source Code Form is "Incompatible With Secondary Licenses", as
 # defined by the Mozilla Public License, v. 2.0.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/runtests.pl b/runtests.pl
index 92e9df806..89a05b46e 100755
--- a/runtests.pl
+++ b/runtests.pl
@@ -6,7 +6,7 @@
 # This Source Code Form is "Incompatible With Secondary Licenses", as
 # defined by the Mozilla Public License, v. 2.0.
 
-use 5.10.1;
+use 5.14.0;
 use diagnostics;
 use strict;
 use warnings;
diff --git a/sanitycheck.cgi b/sanitycheck.cgi
index 3d4334e93..a04fd9be2 100755
--- a/sanitycheck.cgi
+++ b/sanitycheck.cgi
@@ -6,7 +6,7 @@
 # This Source Code Form is "Incompatible With Secondary Licenses", as
 # defined by the Mozilla Public License, v. 2.0.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
@@ -119,7 +119,7 @@ if ($cgi->param('createmissinggroupcontrolmapentries')) {
   # Find all group/product combinations used for bugs but not set up
   # correctly in group_control_map
   my $invalid_combinations = $dbh->selectall_arrayref(
-    qq{    SELECT bugs.product_id,
+    "SELECT bugs.product_id,
                       bgm.group_id,
                       gcm.membercontrol,
                       groups.name,
@@ -127,15 +127,14 @@ if ($cgi->param('createmissinggroupcontrolmapentries')) {
                  FROM bugs
            INNER JOIN bug_group_map AS bgm
                    ON bugs.bug_id = bgm.bug_id
-           INNER JOIN groups
+           INNER JOIN " . $dbh->quote_identifier('groups') . "
                    ON bgm.group_id = groups.id
            INNER JOIN products
                    ON bugs.product_id = products.id
             LEFT JOIN group_control_map AS gcm
                    ON bugs.product_id = gcm.product_id
                   AND    bgm.group_id = gcm.group_id
-                WHERE COALESCE(gcm.membercontrol, $na) = $na
-          }
+                WHERE COALESCE(gcm.membercontrol, $na) = $na "
       . $dbh->sql_group_by(
       'bugs.product_id, bgm.group_id',
       'gcm.membercontrol, groups.name, products.name'
@@ -366,7 +365,7 @@ if ($cgi->param('remove_old_whine_targets')) {
     my $old_ids = $dbh->selectcol_arrayref(
       "SELECT DISTINCT mailto
                                       FROM whine_schedules
-                                 LEFT JOIN $table
+                                 LEFT JOIN " . $dbh->quote_identifier($table) . "
                                         ON $table.$col = whine_schedules.mailto
                                      WHERE mailto_type = $type AND $table.$col IS NULL"
     );
@@ -438,13 +437,14 @@ sub CrossCheck {
     Status('cross_check_from', {table => $refertable, field => $referfield});
 
     my $query
-      = qq{SELECT DISTINCT $refertable.$referfield}
-      . ($keyname ? qq{, $refertable.$keyname } : q{})
-      . qq{ FROM $refertable
-                    LEFT JOIN $table
+      = "SELECT DISTINCT $refertable.$referfield"
+      . ($keyname ? ", $refertable.$keyname " : "")
+      . " FROM "
+      . $dbh->quote_identifier($refertable) . "
+                    LEFT JOIN " . $dbh->quote_identifier($table) . "
                            ON $refertable.$referfield = $table.$field
                         WHERE $table.$field IS NULL
-                          AND $refertable.$referfield IS NOT NULL};
+                          AND $refertable.$referfield IS NOT NULL";
 
     my $sth = $dbh->prepare($query);
     $sth->execute;
@@ -661,14 +661,14 @@ sub DoubleCrossCheck {
       qq{
                         SELECT DISTINCT $refertable.$referfield1, 
                                         $refertable.$referfield2 }
-        . ($keyname ? qq{, $refertable.$keyname } : q{}) . qq{ FROM $refertable
-                     LEFT JOIN $table
+        . ($keyname ? qq{, $refertable.$keyname } : q{}) . " FROM $refertable
+                     LEFT JOIN " . $dbh->quote_identifier($table) . "
                             ON $refertable.$referfield1 = $table.$field1
                            AND $refertable.$referfield2 = $table.$field2 
                          WHERE $table.$field1 IS NULL 
                            AND $table.$field2 IS NULL 
                            AND $refertable.$referfield1 IS NOT NULL 
-                           AND $refertable.$referfield2 IS NOT NULL}
+                           AND $refertable.$referfield2 IS NOT NULL"
     );
 
     foreach my $check (@$d_cross_check) {
@@ -973,7 +973,7 @@ BugCheck(
   "bugs
          INNER JOIN group_control_map
             ON bugs.product_id = group_control_map.product_id
-         INNER JOIN groups
+         INNER JOIN " . $dbh->quote_identifier('groups') . "
             ON group_control_map.group_id = groups.id
           LEFT JOIN bug_group_map
             ON bugs.bug_id = bug_group_map.bug_id
@@ -1018,7 +1018,7 @@ foreach my $target (['groups', 'id', MAILTO_GROUP],
   my $old = $dbh->selectall_arrayref(
     "SELECT whine_schedules.id, mailto
                                           FROM whine_schedules
-                                     LEFT JOIN $table
+                                     LEFT JOIN " . $dbh->quote_identifier($table) . "
                                             ON $table.$col = whine_schedules.mailto
                                          WHERE mailto_type = $type AND $table.$col IS NULL"
   );
diff --git a/sanitycheck.pl b/sanitycheck.pl
index bd1bec8d3..3fcf450ec 100755
--- a/sanitycheck.pl
+++ b/sanitycheck.pl
@@ -6,7 +6,7 @@
 # This Source Code Form is "Incompatible With Secondary Licenses", as
 # defined by the Mozilla Public License, v. 2.0.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/search_plugin.cgi b/search_plugin.cgi
index 46dce5ee2..85e7c76a6 100755
--- a/search_plugin.cgi
+++ b/search_plugin.cgi
@@ -6,7 +6,7 @@
 # This Source Code Form is "Incompatible With Secondary Licenses", as
 # defined by the Mozilla Public License, v. 2.0.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/show_activity.cgi b/show_activity.cgi
index fefd68b8a..e604be3cb 100755
--- a/show_activity.cgi
+++ b/show_activity.cgi
@@ -6,7 +6,7 @@
 # This Source Code Form is "Incompatible With Secondary Licenses", as
 # defined by the Mozilla Public License, v. 2.0.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/show_bug.cgi b/show_bug.cgi
index 64a99d11b..2839abbc0 100755
--- a/show_bug.cgi
+++ b/show_bug.cgi
@@ -6,7 +6,7 @@
 # This Source Code Form is "Incompatible With Secondary Licenses", as
 # defined by the Mozilla Public License, v. 2.0.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/showdependencygraph.cgi b/showdependencygraph.cgi
index 666a6626e..bf9d560e8 100755
--- a/showdependencygraph.cgi
+++ b/showdependencygraph.cgi
@@ -6,7 +6,7 @@
 # This Source Code Form is "Incompatible With Secondary Licenses", as
 # defined by the Mozilla Public License, v. 2.0.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/showdependencytree.cgi b/showdependencytree.cgi
index 94ac15107..c4542b42e 100755
--- a/showdependencytree.cgi
+++ b/showdependencytree.cgi
@@ -6,7 +6,7 @@
 # This Source Code Form is "Incompatible With Secondary Licenses", as
 # defined by the Mozilla Public License, v. 2.0.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/skins/contrib/Dusk/admin.css b/skins/contrib/Dusk/admin.css
new file mode 100644
index 000000000..741447ac6
--- /dev/null
+++ b/skins/contrib/Dusk/admin.css
@@ -0,0 +1,15 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0.
+ */
+
+#menu td.selected_section {
+    color: #008a00;
+}
+
+.sortlist_separator {
+  color: #333;
+}
diff --git a/skins/contrib/Dusk/bug.css b/skins/contrib/Dusk/bug.css
new file mode 100644
index 000000000..846cea12d
--- /dev/null
+++ b/skins/contrib/Dusk/bug.css
@@ -0,0 +1,11 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0.
+ */
+
+.bz_short_desc_container {
+  background-color: #e0e0e0;
+}
diff --git a/skins/contrib/Dusk/buglist.css b/skins/contrib/Dusk/buglist.css
index a9cbfd0fe..8acc00809 100644
--- a/skins/contrib/Dusk/buglist.css
+++ b/skins/contrib/Dusk/buglist.css
@@ -9,3 +9,7 @@
 tr.bz_bugitem:hover {
     background-color: #ccccff;
 }
+
+.bz_sort_order_secondary {
+  color: #555;
+}
diff --git a/skins/contrib/Dusk/global.css b/skins/contrib/Dusk/global.css
index 7ffb91014..6b609ac3f 100644
--- a/skins/contrib/Dusk/global.css
+++ b/skins/contrib/Dusk/global.css
@@ -21,8 +21,8 @@ body {
 }
 
 #header .links, #footer {
-    background-color: #929bb1;
-    color: #ddd;
+    background-color: #667089;
+    color: #fff;
 }
 
 #header {
@@ -51,7 +51,7 @@ body {
 }
 
 a {
-    color: #6070cf;
+    color: #4659c7;
 }
 a:hover {
     color: #8090ef;
@@ -190,7 +190,7 @@ hr {
 }
 
 .tabs td {
-    background: #c8c8c8;
+    background: #e2e2e2;
     border-width: 1px;
 }
 
@@ -235,3 +235,11 @@ hr {
         padding: 0;
     }
 }
+
+/**************/
+/* Bug Fields */
+/**************/
+
+th.required:before, span.required_star {
+    color: #cc3333;
+}
diff --git a/summarize_time.cgi b/summarize_time.cgi
index 9f4e658f3..293a15332 100755
--- a/summarize_time.cgi
+++ b/summarize_time.cgi
@@ -6,7 +6,7 @@
 # This Source Code Form is "Incompatible With Secondary Licenses", as
 # defined by the Mozilla Public License, v. 2.0.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/t/001compile.t b/t/001compile.t
index ad29975af..e9ee6deb7 100644
--- a/t/001compile.t
+++ b/t/001compile.t
@@ -10,7 +10,7 @@
 #Bugzilla Test 1#
 ###Compilation###
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
@@ -84,7 +84,9 @@ SKIP: {
 
     # Check that we have a DBI module to support the DB, if this
     # is a database module (but not Schema)
-    if ($file =~ m{Bugzilla/DB/([^/]+)\.pm$} and $file ne "Bugzilla/DB/Schema.pm") {
+    if ($file =~ m{Bugzilla/DB/([^/]+)\.pm$}
+      and $file ne "Bugzilla/DB/Schema.pm"
+      and $file ne "Bugzilla/DB/QuoteIdentifier.pm") {
       my $module = lc($1);
       my $dbd    = DB_MODULE->{$module}->{dbd}->{module};
       eval("use $dbd; 1") or skip "$file: $dbd not installed", 1;
diff --git a/t/002goodperl.t b/t/002goodperl.t
index 837849282..a20a58b90 100644
--- a/t/002goodperl.t
+++ b/t/002goodperl.t
@@ -10,11 +10,11 @@
 #Bugzilla Test 2#
 ####GoodPerl#####
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use lib 't';
+use lib qw(. lib t);
 
 use Support::Files;
 
@@ -86,6 +86,8 @@ foreach my $file (@testitems) {
 }
 
 foreach my $file (@testitems) {
+  my $enables_strict_re = qr/^\s*use\s+(?:strict|Moo)\s*;/;
+  my $enables_warnings_re = qr/^\s*use\s+(?:warnings|Moo)\s*;/;
   my $found_use_perl     = 0;
   my $found_use_strict   = 0;
   my $found_use_warnings = 0;
@@ -97,17 +99,17 @@ foreach my $file (@testitems) {
     next;
   }
   while (my $file_line = <FILE>) {
-    $found_use_perl     = 1 if $file_line =~ m/^\s*use 5.10.1/;
-    $found_use_strict   = 1 if $file_line =~ m/^\s*use strict/;
-    $found_use_warnings = 1 if $file_line =~ m/^\s*use warnings/;
+    $found_use_perl     = 1 if $file_line =~ m/^\s*use 5.14.0/;
+    $found_use_strict   = 1 if $file_line =~ $enables_strict_re;
+    $found_use_warnings = 1 if $file_line =~ $enables_warnings_re;
     last if ($found_use_perl && $found_use_strict && $found_use_warnings);
   }
   close(FILE);
   if ($found_use_perl) {
-    ok(1, "$file requires Perl 5.10.1");
+    ok(1, "$file requires Perl 5.14.0");
   }
   else {
-    ok(0, "$file DOES NOT require Perl 5.10.1 --WARNING");
+    ok(0, "$file DOES NOT require Perl 5.14.0 --WARNING");
   }
 
   if ($found_use_strict) {
diff --git a/t/003safesys.t b/t/003safesys.t
index 3cc55f835..759c7231c 100644
--- a/t/003safesys.t
+++ b/t/003safesys.t
@@ -10,11 +10,11 @@
 #Bugzilla Test 3#
 ###Safesystem####
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use lib 't';
+use lib qw(. lib t);
 
 use Support::Files;
 
diff --git a/t/004template.t b/t/004template.t
index 938ee8b18..515aad6c8 100644
--- a/t/004template.t
+++ b/t/004template.t
@@ -9,11 +9,11 @@
 #Bugzilla Test 4#
 ####Templates####
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use lib 't';
+use lib qw(. lib t);
 
 use Support::Templates;
 
@@ -140,10 +140,10 @@ foreach my $include_path (@include_paths) {
 
     # Forbid single quotes to delimit URLs, see bug 926085.
     if ($data =~ /href=\\?'/) {
-      ok(0, "$path contains blacklisted constructs: href='...'");
+      ok(0, "$path contains blocklisted constructs: href='...'");
     }
     else {
-      ok(1, "$path contains no blacklisted constructs");
+      ok(1, "$path contains no blocklisted constructs");
     }
   }
 }
diff --git a/t/005whitespace.t b/t/005whitespace.t
index ef589e693..fffb4361c 100644
--- a/t/005whitespace.t
+++ b/t/005whitespace.t
@@ -9,11 +9,11 @@
 #Bugzilla Test 5#
 #####no_tabs#####
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use lib 't';
+use lib qw(. lib t);
 
 use Support::Files;
 use Support::Templates;
diff --git a/t/006spellcheck.t b/t/006spellcheck.t
index ccbd69932..587b5a1b7 100644
--- a/t/006spellcheck.t
+++ b/t/006spellcheck.t
@@ -10,11 +10,11 @@
 #Bugzilla Test 6#
 ####Spelling#####
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use lib 't';
+use lib qw(. lib t);
 use Support::Files;
 
 # -1 because 006spellcheck.t must not be checked.
diff --git a/t/007util.t b/t/007util.t
index 94f62dfc1..8b655fa59 100644
--- a/t/007util.t
+++ b/t/007util.t
@@ -9,11 +9,11 @@
 #Bugzilla Test 7#
 #####Util.pm#####
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use lib 't';
+use lib qw(. lib t);
 use Support::Files;
 use Test::More tests => 17;
 use DateTime;
diff --git a/t/008filter.t b/t/008filter.t
index b60a97579..3541d5ff0 100644
--- a/t/008filter.t
+++ b/t/008filter.t
@@ -15,7 +15,7 @@
 
 # Sample exploit code: '>"><script>alert('Oh dear...')</script>
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/t/009bugwords.t b/t/009bugwords.t
index da432e114..7bd762af1 100644
--- a/t/009bugwords.t
+++ b/t/009bugwords.t
@@ -15,11 +15,11 @@
 # "[% terms.bug %]". This test makes sure the relevant words aren't used
 # bare.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use lib 't';
+use lib qw(. t lib);
 
 use Support::Files;
 use Support::Templates;
diff --git a/t/010dependencies.t b/t/010dependencies.t
index b600766b6..552ead111 100644
--- a/t/010dependencies.t
+++ b/t/010dependencies.t
@@ -10,7 +10,7 @@
 #Bugzilla Test 10#
 ## dependencies ##
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
@@ -71,7 +71,7 @@ foreach my $module (keys %mods) {
         $used =~ s#/#::#g;
         $used =~ s#\.pm$##;
         $used =~ s#\$module#[^:]+#;
-        $used =~ s#\${[^}]+}#[^:]+#;
+        $used =~ s#\$\{[^}]+\}#[^:]+#;
         $used =~ s#[" ]##g;
         push(@use, grep(/^\Q$used\E$/, keys %mods));
       }
diff --git a/t/011pod.t b/t/011pod.t
index 2930ea112..bfd4cedd8 100644
--- a/t/011pod.t
+++ b/t/011pod.t
@@ -10,11 +10,11 @@
 #Bugzilla Test 11#
 ##POD validation##
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use lib 't';
+use lib qw(. lib t);
 
 use Support::Files;
 use Pod::Checker;
diff --git a/t/012throwables.t b/t/012throwables.t
index 6b092d8c2..36bee57df 100644
--- a/t/012throwables.t
+++ b/t/012throwables.t
@@ -10,7 +10,7 @@
 #Bugzilla Test 12#
 ######Errors######
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/t/013dbschema.t b/t/013dbschema.t
index 062a22992..63003265f 100644
--- a/t/013dbschema.t
+++ b/t/013dbschema.t
@@ -12,13 +12,14 @@
 # Check the Bugzilla database schema to ensure no field names conflict
 # with SQL reserved words.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
 use lib qw(. t lib);
 use Bugzilla;
 use Bugzilla::DB::Schema;
+use Bugzilla::DB::Schema::Mysql;
 
 
 # SQL reserved words
@@ -57,7 +58,8 @@ our $schema;
 our @tables;
 
 BEGIN {
-  $schema = Bugzilla::DB::Schema->new("Mysql");
+  our $fake_db = bless {}, 'Bugzilla::DB';
+  $schema = Bugzilla::DB::Schema::Mysql->new(db => $fake_db);
   @tables = $schema->get_table_list();
 }
 
diff --git a/t/Support/Files.pm b/t/Support/Files.pm
index 72737ac1f..3ed3653f6 100644
--- a/t/Support/Files.pm
+++ b/t/Support/Files.pm
@@ -8,7 +8,7 @@
 
 package Support::Files;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/t/Support/Systemexec.pm b/t/Support/Systemexec.pm
index e73763f8f..202c6467f 100644
--- a/t/Support/Systemexec.pm
+++ b/t/Support/Systemexec.pm
@@ -7,11 +7,11 @@
 
 package Support::Systemexec;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
-use parent qw(Exporter);
+use base qw(Exporter);
 @Support::Systemexec::EXPORT = qw(system exec);
 
 sub system($$@) {
diff --git a/t/Support/Templates.pm b/t/Support/Templates.pm
index e08686e0f..c45bba21b 100644
--- a/t/Support/Templates.pm
+++ b/t/Support/Templates.pm
@@ -7,12 +7,12 @@
 
 package Support::Templates;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
 use lib 't';
-use parent qw(Exporter);
+use base qw(Exporter);
 @Support::Templates::EXPORT
   = qw(@languages @include_paths @english_default_include_paths
   @referenced_files %actual_files $num_actual_files);
diff --git a/taskgraph.json b/taskgraph.json
deleted file mode 100644
index ba1d1f3e0..000000000
--- a/taskgraph.json
+++ /dev/null
@@ -1,264 +0,0 @@
-{
-  "metadata": {
-    "name": "Bugzilla Task Graph",
-    "description": "A suite of tests to check the quality of the Bugzilla codebase.",
-    "owner": "dlawrence@mozilla.com",
-    "source": "https://raw.githubusercontent.com/bugzilla/bugzilla/5.0/taskgraph.json"
-  },
-  "tasks": [
-    {
-      "reruns": 3,
-      "maxRunTime": 3000,
-      "task": {
-        "expires": "2018-02-18T17:33:38.806Z",
-        "metadata": {
-          "name": "Basic Sanity Tests"
-        },
-        "provisionerId": "aws-provisioner-v1",
-        "workerType": "b2gtest",
-        "payload": {
-          "image": "bugzilla/bugzilla-ci",
-          "command": ["runtests.sh"],
-          "env": {
-            "TEST_SUITE": "sanity"
-          },
-          "artifacts": {
-            "public/runtests_log": {
-              "type": "file",
-              "path": "/runtests.log",
-              "expires": "2018-02-17T17:33:38.806Z"
-            }
-          }
-        },
-        "extra": {
-          "treeherder": {
-            "symbol": "San",
-            "machine": {
-                "platform": "linux64"
-            },
-            "build": {
-                "platform": "linux64"
-            }
-          }
-        }
-      }
-    },
-    {
-      "reruns": 3,
-      "maxRunTime": 3000,
-      "task": {
-        "expires": "2018-02-18T17:33:38.806Z",
-        "metadata": {
-          "name": "Documentation Build Test"
-        },
-        "provisionerId": "aws-provisioner-v1",
-        "workerType": "b2gtest",
-        "payload": {
-          "image": "bugzilla/bugzilla-ci",
-          "command": ["runtests.sh"],
-          "env": {
-            "TEST_SUITE": "docs"
-          },
-          "artifacts": {
-            "public/runtests_log": {
-              "type": "file",
-              "path": "/runtests.log",
-              "expires": "2018-02-17T17:33:38.806Z"
-            }
-          }
-        },
-        "extra": {
-          "treeherder": {
-            "symbol": "Doc",
-            "machine": {
-                "platform": "linux64"
-            },
-            "build": {
-                "platform": "linux64"
-            }
-          }
-        }
-      }
-    },
-    {
-      "reruns": 3,
-      "maxRunTime": 7200,
-      "task": {
-        "expires": "2018-02-18T17:33:38.806Z",
-        "metadata": {
-          "name": "WebService API Tests (MySQL)"
-        },
-        "provisionerId": "aws-provisioner-v1",
-        "workerType": "b2gtest",
-        "payload": {
-          "image": "bugzilla/bugzilla-ci",
-          "command": ["runtests.sh"],
-          "env": {
-            "TEST_SUITE": "webservices"
-          },
-          "artifacts": {
-            "public/runtests_log": {
-              "type": "file",
-              "path": "/runtests.log",
-              "expires": "2018-02-17T17:33:38.806Z"
-            },
-            "public/httpd_error_log": {
-              "type": "file",
-              "path": "/var/log/httpd/error_log",
-              "expires": "2018-02-17T17:33:38.806Z"
-            }
-          }
-        },
-        "extra": {
-          "treeherder": {
-            "symbol": "API",
-            "machine": {
-                "platform": "linux64"
-            },
-            "build": {
-                "platform": "linux64"
-            }
-          }
-        }
-      }
-    },
-    {
-      "reruns": 3,
-      "maxRunTime": 7200,
-      "task": {
-        "expires": "2018-02-18T17:33:38.806Z",
-        "metadata": {
-          "name": "Selenium Tests (MySQL)"
-        },
-        "provisionerId": "aws-provisioner-v1",
-        "workerType": "b2gtest",
-        "payload": {
-          "image": "bugzilla/bugzilla-ci",
-          "command": ["runtests.sh"],
-          "env": {
-            "TEST_SUITE": "selenium"
-          },
-          "artifacts": {
-            "public/runtests_log": {
-              "type": "file",
-              "path": "/tmp/runtests.log",
-              "expires": "2018-02-17T17:33:38.806Z"
-            },
-            "public/httpd_error_log": {
-              "type": "file",
-              "path": "/var/log/httpd/error_log",
-              "expires": "2018-02-17T17:33:38.806Z"
-            },
-            "public/selenium_log": {
-              "type": "file",
-              "path": "/tmp/selenium.log",
-              "expires": "2018-02-17T17:33:38.806Z"
-            }
-          }
-        },
-        "extra": {
-          "treeherder": {
-            "symbol": "Sel",
-            "machine": {
-                "platform": "linux64"
-            },
-            "build": {
-                "platform": "linux64"
-            }
-          }
-        }
-      }
-    },
-    {
-      "reruns": 3,
-      "maxRunTime": 7200,
-      "task": {
-        "expires": "2018-02-18T17:33:38.806Z",
-        "metadata": {
-          "name": "WebService API Tests (Pg)"
-        },
-        "provisionerId": "aws-provisioner-v1",
-        "workerType": "b2gtest",
-        "payload": {
-          "image": "bugzilla/bugzilla-ci",
-          "command": ["runtests.sh"],
-          "env": {
-            "BUGS_DB_DRIVER": "pg",
-            "TEST_SUITE": "webservices"
-          },
-          "artifacts": {
-            "public/runtests_log": {
-              "type": "file",
-              "path": "/tmp/runtests.log",
-              "expires": "2018-02-17T17:33:38.806Z"
-            },
-            "public/httpd_error_log": {
-              "type": "file",
-              "path": "/var/log/httpd/error_log",
-              "expires": "2018-02-17T17:33:38.806Z"
-            }
-          }
-        },
-        "extra": {
-          "treeherder": {
-            "symbol": "API-Pg",
-            "machine": {
-                "platform": "linux64"
-            },
-            "build": {
-                "platform": "linux64"
-            }
-          }
-        }
-      }
-    },
-    {
-      "reruns": 3,
-      "maxRunTime": 7200,
-      "task": {
-        "expires": "2018-02-18T17:33:38.806Z",
-        "metadata": {
-          "name": "Selenium Tests (Pg)"
-        },
-        "provisionerId": "aws-provisioner-v1",
-        "workerType": "b2gtest",
-        "payload": {
-          "image": "bugzilla/bugzilla-ci",
-          "command": ["runtests.sh"],
-          "env": {
-            "BUGS_DB_DRIVER": "pg",
-            "TEST_SUITE": "selenium"
-          },
-          "artifacts": {
-            "public/runtests_log": {
-              "type": "file",
-              "path": "/tmp/runtests.log",
-              "expires": "2018-02-17T17:33:38.806Z"
-            },
-            "public/httpd_error_log": {
-              "type": "file",
-              "path": "/var/log/httpd/error_log",
-              "expires": "2018-02-17T17:33:38.806Z"
-            },
-            "public/selenium_log": {
-              "type": "file",
-              "path": "/tmp/selenium.log",
-              "expires": "2018-02-17T17:33:38.806Z"
-            }
-          }
-        },
-        "extra": {
-          "treeherder": {
-            "symbol": "Sel-Pg",
-            "machine": {
-                "platform": "linux64"
-            },
-            "build": {
-                "platform": "linux64"
-            }
-          }
-        }
-      }
-    }
-  ]
-}
diff --git a/template/en/default/account/create.html.tmpl b/template/en/default/account/create.html.tmpl
index 5711a726f..6362a86f3 100644
--- a/template/en/default/account/create.html.tmpl
+++ b/template/en/default/account/create.html.tmpl
@@ -64,13 +64,13 @@
 [% END %]
 
 <form id="account_creation_form" method="get" action="createaccount.cgi">
-  <span class="label">
+  <label for="login"><span class="label">
     [% IF Param('emailsuffix') %]
       Login:
     [% ELSE %]
       Email address:
     [% END %]
-  </span>
+  </span></label>
   <input size="35" id="login" name="login" autofocus
          [%- ' type="email"' UNLESS Param('emailsuffix') %] required>
   [% Param('emailsuffix') FILTER html %]
diff --git a/template/en/default/admin/params/common.html.tmpl b/template/en/default/admin/params/common.html.tmpl
index fd9fd3ed1..7be8262df 100644
--- a/template/en/default/admin/params/common.html.tmpl
+++ b/template/en/default/admin/params/common.html.tmpl
@@ -16,6 +16,11 @@
       [% IF param.type == "t" %]
         <input type="text" size="80" name="[% param.name FILTER html %]"
                id="[% param.name FILTER html %]" value="[% Param(param.name) FILTER html %]">
+      [% ELSIF param.type == "r" %]
+        <input type="text" size="80" name="[% param.name FILTER html %]_readonly"
+               id="[% param.name FILTER html %]_readonly" value="[% Param(param.name) FILTER html %]" disabled>
+        <input type="hidden" name="[% param.name FILTER html %]" value="[% Param(param.name) FILTER html %]"><br>
+        This value is read-only and you can't change it.
       [% ELSIF param.type == "p" %]
         <input type="password" size="80" name="[% param.name FILTER html %]"
                id="[% param.name FILTER html %]" value="[% Param(param.name) FILTER html %]"
diff --git a/template/en/default/admin/params/general.html.tmpl b/template/en/default/admin/params/general.html.tmpl
index 6ebef8ea2..c33f3c6fc 100644
--- a/template/en/default/admin/params/general.html.tmpl
+++ b/template/en/default/admin/params/general.html.tmpl
@@ -23,9 +23,12 @@
     _ " only after the data has been converted from existing legacy"
     _ " character encodings to UTF-8, using the <kbd>contrib/recode.pl</kbd>"
     _ " script</strong>."
-    _ " <p>Note that if you turn this parameter from &quot;off&quot; to"
-    _ " &quot;on&quot;, you must re-run <kbd>checksetup.pl</kbd> immediately"
-    _ " afterward.</p>",
+    _ " <p>Note that if you change this parameter you must re-run"
+    _ " <kbd>checksetup.pl</kbd> immediately afterward.</p>",
+
+  utf8_collate =>
+    "The collation to use in database tables. This parameter is"
+    _ " automatically set by checksetup.pl.",
 
   shutdownhtml => 
     "If this field is non-empty, then Bugzilla will be completely"
diff --git a/template/en/default/config.js.tmpl b/template/en/default/config.js.tmpl
index 0399f8b28..0e924d801 100644
--- a/template/en/default/config.js.tmpl
+++ b/template/en/default/config.js.tmpl
@@ -64,7 +64,7 @@ var [% cf.name FILTER js %] = [ [% FOREACH x = cf.legal_values %]'[% x.name FILT
 // =======================
 //
 // It is not necessary to list all products and components here.
-// Instead, you can define a "blacklist" for some commonly used words
+// Instead, you can define a "blocklist" for some commonly used words
 // or word fragments that occur in a product or component name
 // but should _not_ trigger product/component search.
 
@@ -84,7 +84,7 @@ var target_milestone = new Object();
 // Product and Component Exceptions
 // ================================
 //
-// A blacklist for some commonly used words or word fragments 
+// A blocklist for some commonly used words or word fragments
 // that occur in a product or component name but should *not*
 // trigger product/component search in QuickSearch.
 
diff --git a/template/en/default/email/header-common.txt.tmpl b/template/en/default/email/header-common.txt.tmpl
index 02bd38e13..ffac94d1a 100644
--- a/template/en/default/email/header-common.txt.tmpl
+++ b/template/en/default/email/header-common.txt.tmpl
@@ -13,6 +13,7 @@ X-Bugzilla-Component: [% bug.component %]
 X-Bugzilla-Version: [% bug.version %]
 X-Bugzilla-Keywords: [% bug.keywords %]
 X-Bugzilla-Severity: [% bug.bug_severity %]
+X-Bugzilla-ID: [% bug.id %]
 X-Bugzilla-Who: [% changer.login %]
 X-Bugzilla-Status: [% bug.bug_status %]
 X-Bugzilla-Resolution: [% bug.resolution %]
diff --git a/template/en/default/email/new-user-details.txt.tmpl b/template/en/default/email/new-user-details.txt.tmpl
index b1b43caf2..2243c299f 100644
--- a/template/en/default/email/new-user-details.txt.tmpl
+++ b/template/en/default/email/new-user-details.txt.tmpl
@@ -17,7 +17,7 @@ X-Bugzilla-Type: admin
 
 [This e-mail has been automatically generated]
 
-A new [% terms.Bugzilla %] user account has been created at [% urlbase %]:
+A new [% terms.Bugzilla %] user account has been created at [% urlbase %] :
 
 Login: [% new_user.login %]
 [% IF new_user.realname %]
diff --git a/template/en/default/extensions/config.pm.tmpl b/template/en/default/extensions/config.pm.tmpl
index 68ed260c1..599f9ea59 100644
--- a/template/en/default/extensions/config.pm.tmpl
+++ b/template/en/default/extensions/config.pm.tmpl
@@ -14,7 +14,7 @@
 
 package Bugzilla::Extension::[% name %];
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/template/en/default/extensions/extension.pm.tmpl b/template/en/default/extensions/extension.pm.tmpl
index 823440421..985d28ccc 100644
--- a/template/en/default/extensions/extension.pm.tmpl
+++ b/template/en/default/extensions/extension.pm.tmpl
@@ -14,7 +14,7 @@
 
 package Bugzilla::Extension::[% name %];
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/template/en/default/extensions/util.pm.tmpl b/template/en/default/extensions/util.pm.tmpl
index 3baab57fd..6e14428a6 100644
--- a/template/en/default/extensions/util.pm.tmpl
+++ b/template/en/default/extensions/util.pm.tmpl
@@ -14,7 +14,7 @@
 
 package Bugzilla::Extension::[% name %]::Util;
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/template/en/default/global/code-error.html.tmpl b/template/en/default/global/code-error.html.tmpl
index 830a7e7f6..e30d8da90 100644
--- a/template/en/default/global/code-error.html.tmpl
+++ b/template/en/default/global/code-error.html.tmpl
@@ -397,6 +397,13 @@
     Bugzilla does not support the search type
     "[% operator.truncate(30, "...") FILTER html %]".
 
+  [% ELSIF error == "bz51_attempted_upgrade" %]
+    [% title = "Invalid Upgrade Path" %]
+    It looks like you are attempting to "upgrade" from
+    Bugzilla 5.1. This is not supported because 5.2 is
+    based on Bugzilla 5.0.6 and not 5.1, so this is
+    actually a downgrade.
+
   [% ELSE %]
     [%# Try to find hooked error messages %]
     [% error_message = Hook.process("errors") %]
diff --git a/template/en/default/global/common-links.html.tmpl b/template/en/default/global/common-links.html.tmpl
index 78b4eb80a..64da6e874 100644
--- a/template/en/default/global/common-links.html.tmpl
+++ b/template/en/default/global/common-links.html.tmpl
@@ -28,7 +28,8 @@
         no_redirect.value = 1;
       }
     </script>
-    <input class="txt" type="text" id="quicksearch[% qs_suffix FILTER html %]" name="quicksearch" 
+    <input class="txt" type="text" id="quicksearch[% qs_suffix FILTER html %]"
+           name="quicksearch" aria-labelledby="find[% qs_suffix FILTER html %]"
            title="Quick Search" value="[% quicksearch FILTER html %]">
     <input class="btn" type="submit" value="Search" 
            id="find[% qs_suffix FILTER html %]">
diff --git a/template/en/default/global/header.html.tmpl b/template/en/default/global/header.html.tmpl
index bd40fff88..0d6849ddb 100644
--- a/template/en/default/global/header.html.tmpl
+++ b/template/en/default/global/header.html.tmpl
@@ -89,6 +89,8 @@
       <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
     [% END %]
 
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+
     [% SET yui = yui_resolve_deps(yui, yui_deps) %]
 
     [% SET css_sets = css_files(style_urls, yui, yui_css) %]
diff --git a/template/en/default/index.html.tmpl b/template/en/default/index.html.tmpl
index 84a5b7d5c..96afb92f6 100644
--- a/template/en/default/index.html.tmpl
+++ b/template/en/default/index.html.tmpl
@@ -19,6 +19,12 @@
 [% IF release %]
   <div id="new_release">
     [% IF release.data %]
+      [% IF release.eos_date %]
+        <p>[% terms.Bugzilla %] [%+ release.branch_version FILTER html %] will
+        no longer receive security updates after [% release.eos_date FILTER html %].
+        You are highly encouraged to upgrade in order to keep your
+        system secure.</p>
+      [% END %]
       [% IF release.deprecated %]
         <p>Bugzilla [%+ release.deprecated FILTER html %] is no longer
         supported. You are highly encouraged to upgrade in order to keep your
@@ -93,6 +99,7 @@
     <form id="quicksearchForm" name="quicksearchForm" action="buglist.cgi">
       <input id="quicksearch_main" name="quicksearch" title="Quick Search"
              placeholder="Enter [% terms.abug %] # or some search terms"
+             aria-labelledby="find"
              autofocus required>
       <input id="find" type="submit" value="Quick Search">
       <a href="page.cgi?id=quicksearch.html" title="Quick Search help">[?]</a>
@@ -137,10 +144,12 @@
         <a href="buglist.cgi?chfield=%5BBug%20creation%5D&amp;chfieldfrom=24h">last 24 hours</a>
         [% title = BLOCK %][% terms.Bugs %] reported in the last 24 hours[% END %]
         <a href="buglist.cgi?chfield=%5BBug%20creation%5D&amp;chfieldfrom=24h&amp;ctype=atom&amp;title=[% title FILTER uri %]"
+           aria-label="Bugs reported in the last 24 hours - RSS feed"
            class="rss">&nbsp;</a>
         | <a href="buglist.cgi?chfield=%5BBug%20creation%5D&amp;chfieldfrom=7d">last 7 days</a>
         [% title = BLOCK %][% terms.Bugs %] reported in the last 7 days[% END %]
         <a href="buglist.cgi?chfield=%5BBug%20creation%5D&amp;chfieldfrom=7d&amp;ctype=atom&amp;title=[% title FILTER uri %]"
+           aria-label="Bugs reported in the last 7 days - RSS feed"
            class="rss">&nbsp;</a>
       </li>
       <li>
@@ -148,10 +157,12 @@
         <a href="buglist.cgi?chfieldfrom=24h">last 24 hours</a>
         [% title = BLOCK %][% terms.Bugs %] changed in the last 24 hours[% END %]
         <a href="buglist.cgi?chfieldfrom=24h&amp;ctype=atom&amp;title=[% title FILTER uri %]"
+           aria-label="Bugs changed in the last 24 hours - RSS feed"
            class="rss">&nbsp;</a>
         | <a href="buglist.cgi?chfieldfrom=7d">last 7 days</a>
         [% title = BLOCK %][% terms.Bugs %] changed in the last 7 days[% END %]
         <a href="buglist.cgi?chfieldfrom=7d&amp;ctype=atom&amp;title=[% title FILTER uri %]"
+           aria-label="Bugs changed in the last 7 days - RSS feed"
            class="rss">&nbsp;</a>
       </li>
     </ul>
diff --git a/template/en/default/pages/quicksearch.html.tmpl b/template/en/default/pages/quicksearch.html.tmpl
index 8564f64d0..d98f31a83 100644
--- a/template/en/default/pages/quicksearch.html.tmpl
+++ b/template/en/default/pages/quicksearch.html.tmpl
@@ -17,7 +17,8 @@
   to search for:</label></p>
 
 <form name="f" action="buglist.cgi" method="get">
-  <input size="40" name="quicksearch" id="quicksearch" autofocus required>
+  <input size="40" name="quicksearch" id="quicksearch"
+         aria-labelledby="find" autofocus required>
   <input type="submit" value="Search" id="find">
 </form>
 
diff --git a/template/en/default/pages/release-notes.html.tmpl b/template/en/default/pages/release-notes.html.tmpl
index fd6c9e00d..f94cfc180 100644
--- a/template/en/default/pages/release-notes.html.tmpl
+++ b/template/en/default/pages/release-notes.html.tmpl
@@ -6,7 +6,7 @@
   # defined by the Mozilla Public License, v. 2.0.
   #%]
 
-[% SET title = "Bugzilla 5.0 Release Notes" %]
+[% SET title = "Bugzilla 5.2 Release Notes" %]
 [% INCLUDE global/header.html.tmpl
   title = title
   bodyclasses = ['narrow_page']
@@ -16,7 +16,7 @@
 
 <ul class="bz_toc">
   <li><a href="#introduction">Introduction</a></li>
-  <li><a href="#point">Updates in this 5.0.x Release</a></li>
+  <li><a href="#point">Updates since 5.0.4</a></li>
   <li><a href="#req">Minimum Requirements</a></li>
   <li><a href="#feat">New Features and Improvements</a></li>
   <li><a href="#issues">Outstanding Issues</a></li>
@@ -29,9 +29,27 @@
 
 <h2 id="introduction">Introduction</h2>
 
-<p>Welcome to Bugzilla 5.0! It has been slightly over two years since we released
-  Bugzilla 4.4 in May of 2013. This new major release comes with many new features
-  and improvements to WebServices and performance.</p>
+<p>Welcome to [% terms.Bugzilla %] 5.2! This is kind of a weird release to explain. Back in
+early 2019, versions 5.0.5 and 5.0.6 were released for the 5.0.x branch,
+however, these releases contained invasive database schema and code
+reformatting changes that technically should not have been allowed to land on a
+stable branch. Some people noticed this, and never upgraded from 5.0.4 since
+5.0.5 and 5.0.6 did not contain any security fixes. To fix this situation, 5.2
+is picking up where 5.0.6 left off, since 5.0.5 technically should have been
+5.2. Those still on 5.0.4 can upgrade to 5.0.4.1 if they're not ready to go a
+full version release yet.</p>
+
+<p><strong>NOTE:</strong> Because of the above situation, version 5.1.x
+(development release) is actually NEWER code than 5.2, and you cannot "upgrade"
+to 5.2 from 5.1.x. The 5.1.x series has been retroactively renamed to
+5.3.x.</p>
+
+<p>[% terms.Bugzilla %] is making an attempt to be 100% accessible. We're not
+there yet. We have made accessibilty improvements but we're quite sure that
+we've missed a lot. If you find any accessibility issues such as fields missing
+labels, poor contrast, or things that don't play well with screen readers,
+please file a new [% terms.bug %] which blocks our
+<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=bugzilla-section508">Section 508 Compliance [% terms.bug %]</a>.</p>
 
 <p>If you're upgrading, make sure to read
   <a href="https://bugzilla.readthedocs.org/en/latest/installing/upgrading.html">
@@ -41,150 +59,24 @@
   your version and this one, <strong>particularly the Upgrading section of each
   version's release notes</strong>.</p>
 
-<h2 id="point">Updates in this 5.0.x Release</h2>
+<h2 id="point">Updates in this 5.2.x Release</h2>
 
-<h3>5.0.6</h3>
-<p>This release contains a schema change to the flagtypes table, allowing for many more flagtypes.</p>.
-<p>The flagtypes table should have been using a mediumint for several releases, but due to a bug in the schema migration code this never happened.</p>
-
-<h3>5.0.5</h3>
-
-<p>This release reformats the code according to the same conventions as the popular Mojolicious product and includes a <code>.perltidyrc</code> to do the same.
-You may use whatever coding style you want, but all files commited to the repo must be reformatted according to those rules.</p>
-
-<p>Additionally, we no longer follow the same release process as before. Releases will be more frequent.</p>
-
-<p>As it is now 2019, the bugs_fulltext table is now InnoDB instead of MyISAM. This may cause upgrade headaches.</p>
-
-<h3>5.0.4</h3>
-
-<p>This release fixes one security issue. See the
-  <a href="https://www.bugzilla.org/security/4.4.12/">Security Advisory</a>
-  for details.</p>
-
-<p>This release also contains the following [% terms.bug %] fixes:</p>
-
-<ul>
-  <li><kbd>checksetup.pl</kbd> would fail to update Chart storage during pre-3.6 to 5.0 upgrade.
-    (<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1273846">[% terms.Bug %] 1273846</a>)</li>
-  <li><kbd>editflagtypes.cgi</kbd> would crash when classifications are enabled and
-    the user did not have global <kbd>editcomponents</kbd> privileges.
-    (<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1310728">[% terms.Bug %] 1310728</a>)</li>
-  <li>The <kbd>File::Slurp</kbd> would trigger warnings on perl 5.24.
-    (<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1301887">[% terms.Bug %] 1301887</a>)</li>
-  <li>All the time entries in the 'when' column had the correct date but the time
-    was fixed to 00:00 when using Sqlite.
-    (<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1303702">[% terms.Bug %] 1303702</a>)</li>
-</ul>
-
-<h3>5.0.3</h3>
-
-<p>This release fixes one security issue. See the
-  <a href="https://www.bugzilla.org/security/4.4.11/">Security Advisory</a>
-  for details.</p>
-
-<p>This release also contains the following [% terms.bug %] fixes:</p>
+<p>Future releases on this branch will contain a list of the updates since the
+last point release in this section.</p>
 
-<ul>
-  <li>A regression in Bugzilla 5.0.2 caused <kbd>whine.pl</kbd> to be unable
-    to send emails due to a missing subroutine.
-    (<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1235395">[% terms.Bug %] 1235395</a>)</li>
-  <li>The <kbd>Encode</kbd> module changed the way it encodes strings, causing
-    email addresses in emails sent by [%terms.Bugzilla %] to be encoded,
-    preventing emails from being correctly delivered to recipients.
-    We now encode email headers correctly.
-    (<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1246228">[% terms.Bug %] 1246228</a>)</li>
-  <li>Fix additional taint issues with Strawberry Perl.
-    (<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=987742">[% terms.Bug %] 987742</a> and
-    <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1089448">[% terms.bug %] 1089448</a>)</li>
-  <li>When exporting a buglist as a CSV file, fields starting with either
-    "=", "+", "-" or "@" are preceded by a space to not trigger formula
-    execution in Excel.
-    (<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1259881">[% terms.Bug %] 1259881</a>)</li>
-  <li>An extension which allows user-controlled data to be used as a link in
-    tabs could trigger XSS if the data is not correctly sanitized.
-    [%+ terms.Bugzilla %] no longer relies on the extension to do the sanity
-    check. A vanilla installation is not affected as no tab is user-controlled.
-    (<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1250114">[% terms.Bug %] 1250114</a>)</li>
-  <li>Extensions can now easily override the favicon used for the
-    [%+ terms.Bugzilla %] website.
-    (<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1250264">[% terms.Bug %] 1250264</a>)</li>
-</ul>
-
-<h3>5.0.2</h3>
-
-<p>This release fixes two security issues. See the
-  <a href="https://www.bugzilla.org/security/4.2.15/">Security Advisory</a>
-  for details.</p>
-
-<p>This release also contains the following [% terms.bug %] fixes:</p>
-
-<ul>
-  <li>mod_perl now works correctly with mod_access_compat turned off on
-    Apache 2.4. To regenerate the <kbd>.htaccess</kbd> files, you must first
-    delete all existing ones in subdirectories:
-    <pre>find . -mindepth 2 -name .htaccess -exec rm -f {} \;</pre>
-    You must then run <kbd>checksetup.pl</kbd> again to recreate them with
-    the correct syntax.
-    (<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1223790">[% terms.Bug %] 1223790</a>)</li>
-  <li>Emails sent by [% terms.Bugzilla %] are now correctly encoded as UTF-8.
-    (<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=714724">[% terms.Bug %] 714724</a>)</li>
-  <li>Strawberry Perl is now fully supported on Windows.
-    (<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1089448">[% terms.Bug %] 1089448</a>
-    and <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=987742">[% terms.bug %] 987742</a>)</li>
-  <li>The XML-RPC API now works with IIS on Windows.
-    (<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=708252">[% terms.Bug %] 708252</a>)</li>
-  <li>Some queries should now be faster on PostgreSQL.
-    (<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1184431">[% terms.Bug %] 1184431</a>)</li>
-</ul>
-
-<h3>5.0.1</h3>
-
-<p>This release fixes one security issue. See the
-  <a href="https://www.bugzilla.org/security/4.2.14/">Security Advisory</a>
-  for details.</p>
-
-<p>This release also contains the following [% terms.bug %] fixes:</p>
-
-<ul>
-  <li>Users whose login name is not an email address could not log in on
-    installations which use LDAP to authenticate users.
-    (<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1179160">[% terms.Bug %] 1179160</a>)</li>
-  <li>If a mandatory custom field was hidden, it was not possible to create
-    a new [% terms.bug %] or to edit existing ones.
-    (<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1183398">[% terms.Bug %] 1183398</a>
-    and <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1196969">[% terms.bug %] 1196969</a>)</li>
-  <li>A user editing his login name to point to a non-existent email address
-    could cause Bugzilla to stop working, causing a denial of service.
-    (<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1194987">[% terms.Bug %] 1194987</a>)</li>
-  <li>Emails generated during a transaction made PostgreSQL stop working.
-    (<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1186700">[% terms.Bug %] 1186700</a>)</li>
-  <li>[% terms.Bugs %] containing a comment with a reference to a [% terms.bug %]
-    ID larger than 2^31 could not be displayed anymore using PostgreSQL.
-    (<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1191937">[% terms.Bug %] 1191937</a>)</li>
-  <li>The date picker in the "Time Summary" page was broken.
-    (<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1181649">[% terms.Bug %] 1181649</a>)</li>
-  <li>If <kbd>Test::Taint</kbd> or any other Perl module required to use the
-    JSON-RPC API was not installed or was too old, the UI to tag comments was
-    displayed anyway, you could tag comments, but tags were not persistent
-    (they were lost on page reload). Now the UI to tag comments is not displayed
-    at all until the missing Perl modules are installed and up-to-date.
-    (<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1183227">[% terms.Bug %] 1183227</a>)</li>
-  <li>Custom fields of type <kbd>INTEGER</kbd> now accept negative integers.
-    (<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1198659">[% terms.Bug %] 1198659</a>)</li>
-  <li>On Windows, the <kbd>checksetup.pl</kbd> installation script no longer
-    asks for a SMTP server. It can be set after the installation is complete.
-    (<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1191255">[% terms.Bug %] 1191255</a>)</li>
-</ul>
+<p>Because of the weird way versions 5.0.5 and 5.0.6 were released, changes
+from those releases are included <a href="#point504">below</a> in case you're upgrading from the
+5.0.4.x branch.</p>
 
 <h2 id="req">Minimum Requirements</h2>
 
-<p>Any requirements that are new since 4.4 will look like
+<p>Any requirements that are new since 5.0 will look like
   <span class="req_new">this</span>.</p>
 
 <ul>
   <li><a href="#req_perl">Perl</a></li>
   <li><a href="#req_mysql">For MySQL Users</a></li>
+  <li><a href="#req_mariadb">For MariaDB Users</a></li>
   <li><a href="#req_pg">For PostgreSQL Users</a></li>
   <li><a href="#req_oracle">For Oracle Users</a></li>
   <li><a href="#req_sqlite">For SQLite Users</a></li>
@@ -195,7 +87,9 @@ You may use whatever coding style you want, but all files commited to the repo m
 
 <h3 id="req_perl">Perl</h3>
 
-<p>Perl <span class="req_new">v5.10.1</span></p>
+<p>Perl <span class="req_new">v5.14</span></p>
+
+[% INCLUDE db_req db='mariadb' %]
 
 [% INCLUDE db_req db='mysql' %]
 
@@ -215,7 +109,7 @@ You may use whatever coding style you want, but all files commited to the repo m
 <h3 id="req_optional_mod">Optional Perl Modules</h3>
 
 <p>The following perl modules, if installed, enable various
-  features of Bugzilla:</p>
+  features of [% terms.Bugzilla %]:</p>
 
 [% INCLUDE req_table reqs = OPTIONAL_MODULES
                      new  = ['Cache-Memcached','File-Copy-Recursive']
@@ -224,7 +118,7 @@ You may use whatever coding style you want, but all files commited to the repo m
 
 <h3 id="req_apache">Optional Apache Modules</h3>
 
-<p>If you are using Apache as your webserver, Bugzilla can
+<p>If you are using Apache as your webserver, [% terms.Bugzilla %] can
   take advantage of some Apache features if you have the below Apache
   modules installed and enabled.</p>
 
@@ -239,244 +133,149 @@ You may use whatever coding style you want, but all files commited to the repo m
   you.</p>
 
 
-<a name="v50_feat"></a>
+<a name="v52_feat"></a>
 <h2 id="feat">New Features and Improvements</h2>
 
 <ul>
-  <li><a href="#feat_webservices">Improved WebServices</a></li>
-  <li><a href="#feat_caching_performance">Improved Caching using Memcached</a></li>
-  <li><a href="#feat_comment_tags">Abililty to Tag [% terms.Bug %] Comments</a></li>
-  <li><a href="#feat_bug_groups">Improved [% terms.Bug %] Group Membership Checking</a></li>
-  <li><a href="#feat_documentation">Improved Documentation for Users and Administrators</a></li>
+  <li><a href="#feat_mobilerender">Improved Mobile Rendering</a></li>
+  <li><a href="#feat_mariadb">Explicit MariaDB Support</a></li>
+  <li><a href="#feat_mysql8">MySQL 8+ Support</a></li>
+  <li><a href="#feat_utf8mb4">Better Unicode support on MySQL and MariaDB</a></li>
+  <li><a href="#feat_docker">Demo Docker Configuration</a></li>
   <li><a href="#feat_other">Other Enhancements and Changes</a></li>
 </ul>
 
-<h3 id="feat_webservices">Improved WebServices</h3>
+<h3 id="feat_mobilerender">Improved Mobile Rendering</h3>
 
 <p>
-  This release has major improvements in the WebServices interface. One big
-  addition is a new REST-like endpoint alongside the existing XML-RPC and JSON-RPC
-  endpoints. This will allow clients to access Bugzilla data using standard HTTP
-  calls for easy development. <strong>Note:</strong> XML-RPC and JSON-RPC are
-  deprecated in favor of REST and will likely be removed in the Bugzilla 7.0 release.
-</p>
-<p>
-  Also API key support has been added so that API calls will no longer need to use
-  cookies or a user's login and password. Users can create a different API key for
-  each application and revoke API keys that have been compromised or are no longer
-  needed. The API key will simply be passed to each call as credentials.
-</p>
-<p>
-  Several methods have been added and existing ones improved to allow returning
-  data that was not available before such as <kbd>Group.get</kbd>. <kbd>B[%%]ug.search</kbd>
-  is now as full featured as the Advanced Query UI allowing for the same searches
-  to be executed. Attachment data such as flags and other metadata can now be
-  updated through the API. Other WebService changes are detailed
-  <a href="#feat_webservices_other">below</a>.
+Metadata to assist browsers in properly rendering [% terms.Bugzilla %] on mobile browsers was added. Without this metadata Google Search tools would report [% terms.Bugzilla %] pages as not suitable for mobile devices. [% terms.Bugzilla %] still needs work to actually look nice on mobile devices but this is a good start. (<a href="https://github.com/bugzilla/bugzilla/pull/78">PR #78</a>)
 </p>
 
-<h3 id="feat_caching_performance">Improved Caching using Memcached</h3>
+<h3 id="feat_mariadb">Explicit MariaDB Support</h3>
 
-<p>
-  Bugzilla now has the ability to connect to a Memcached server running either
-  locally or on the network to allow fast access to different types of data.
-  This cuts down on the amount of database hits and can improve performance. Other
-  areas have been improved as well to take advantage of caching in memory for
-  objects that are retrieved multiple times during a request such as user data, etc.
-</p>
+<p>Newer versions of MariaDB (10.6 and newer) have diverged from MySQL
+sufficiently that you can't really call it a drop-in replacement for MySQL
+anymore. We new have a 'mariadb' database driver for all versions of MariaDB.
+<code>checksetup.pl</code> will prompt you to switch to it if it detects that you are connected to a MariaDB database server.
+[%+ INCLUDE buglink id=1467006 %]</p> 
 
-<h3 id="feat_comment_tags">Ability to Tag [% terms.Bug %] Comments</h3>
+<h3 id="feat_mysql8">MySQL 8+ Support</h3>
 
-<p>
-  Users can add tags, visible to other users, to [% terms.bug %] comments. This
-  gives the users the ability to thread conversations, mark comments as spam,
-  identify important comments, etc. Users can hide comments that contain specific
-  tags if desired. The tag input field also supports autocompletion so commonly
-  used tags can be selected. Administrators can make specifically tagged comments
-  be automatically hidden from view.
-</p>
+<p>Bugzilla previously did not work on MySQL 8 or newer. Now it does.
+[%+ INCLUDE buglink id=1592129 %]</p>
 
-<h3 id="feat_bug_groups">Improved [% terms.Bug %] Group Membership Checking</h3>
+<h3 if="feat_utf8mb4">Better Unicode support on MySQL and MariaDB</h3>
 
-<p>
-  In the past, Bugzilla restricted who can view [% terms.abug %] to everyone
-  who was a member of ALL the groups the [% terms.bug %] was in. That is, the
-  groups were ANDed together. This made some access control scenarios rather
-  difficult to achieve. So now, Bugzilla defaults to (and can be switched to,
-  in existing installations) a mode where the [% terms.bug %] can be viewed by
-  everyone who is a member of ANY group the [% terms.bug %] is in. That is, the
-  groups are ORed together. This give more flexibility in the way [% terms.bugs %]
-  are made private to specific groups of users.
-</p>
-<p>
-  <strong>Note:</strong> Group memberships for [% terms.bugs %] and users are
-  not changed at all when this setting is switched. When switching from AND to
-  OR, this means that [% terms.bugs %] may be more widely viewable than previously.
-  It is the responsibility of the administrator to make sure that no [% terms.bugs %]
-  are accidentally revealed to the wrong people when changing this setting.
-</p>
+<p>The <b>utf8mb4</b> character set is now used by default on new installs making use of either MariaDB or MySQL. This should resolve many issues related to character encoding found in older versions.
+[%+ INCLUDE buglink id=1891882 %]</p>
 
-<h3 id="feat_documentation">Improved Documentation for Users and Administrators</h3>
+<h3 id="feat_docker">Demo Docker Configuration</h3>
 
-<p>
-  The standard documentation that is shipped along with the Bugzilla code has been
-  rewritten and improved using the reStructuredText format. This allows the
-  documentation to be easily hosted at sites such as ReadTheDocs.org and can
-  also be more easily converted into different formats such as HTML and PDF.
-  A new section dedicated to the new REST WebService API has also been added,
-  significantly improving on the old WebService documentation.
-</p>
+<p>[% terms.Bugzilla %] now ships with a Docker Compose configuration which
+provides an out-of-the-box [% terms.Bugzilla %] with a default configuration to
+test with. Type <code>docker compose up</code> in the root [% terms.bugzilla %]
+directory to start it up. You will be prompted on the console with how to
+connect to it once it comes up. Requires that you have Docker already
+installed. This configuration is not suitable for production use, and is just
+for playing around with it and testing it without needing to do a full
+installation.
+[%+ INCLUDE buglink id=1888068 %]</p>
 
 <h3 id="feat_other">Other Enhancements and Changes</h3>
 
 <h4>Enhancements for Users</h4>
 
 <ul>
-  <li><strong>[% terms.Bugs %]:</strong> The deadline field is now visible to users
-    not in the the <kbd>timetracking</kbd> group.</li>
-  <li><strong>[% terms.Bugs %]:</strong> There is now a "Preview" mode when
-    creating a new comment that allows you to see how the comment will look
-    before committing to the database.</li>
-  <li><strong>[% terms.Bugs %]:</strong> The reporter is now allowed to enter
-    keywords at time of [% terms.bug %] creation.</li>
-  <li><strong>[% terms.Bugs %]:</strong> "See Also" now allows spaces as well as
-    commas to delimit multiple values.</li>
-  <li><strong>[% terms.Bugs %]:</strong> Auto linkification in comments of [% terms.bug %]
-    IDs and comment IDs has been improved.</li>
-  <li><strong>[% terms.Bugs %]:</strong> [% terms.Bugs %] can now have multiple
-    aliases assigned to them. Before each [% terms.bug %] could only have a single
-    value. Also, aliases are now visible in the browser's title bar.</li>
-  <li><strong>[% terms.Bugs %]:</strong> Users can now change the flags of multiple
-    [%+ terms.bugs %] at once using the mass-edit form.</li>
-  <li><strong>Charts and Reports:</strong> UTF-8 characters are now correctly
-    displayed in "New Charts" and graphical reports.</li>
-  <li><strong>Charts and Reports:</strong> Custom multi-select fields are now
-    available as report axis options. This makes them usable for categorizing
-    [%+ terms.bugs %] in reports.</li>
-  <li><strong>Email:</strong> You can now choose to not receive any mail at all
-    about a particular [% terms.bug %], even if you continue to have a role on
-    that [% terms.bug %] (e.g. reporter).</li>
-  <li><strong>Email:</strong> When adding or removing [% terms.abug %] as a
-    dependency, the summary of the [% terms.bug %] is included in the email
-    notification.</li>
-  <li><strong>Requests:</strong> <kbd>request.cgi</kbd> can now output results in
-    CSV format.</li>
-  <li><strong>Requests:</strong><kbd> X-Bugzilla-*</kbd> headers are now included
-    in flag notification emails.</li>
-  <li><strong>Searches:</strong> Some useful searches have been added to the
-    Bugzilla home page.</li>
-  <li><strong>Searches:</strong> Quicksearch now allows for use of comparison
-    operators such as !=, >=, >, &lt;, etc., in addition to substring searches.</li>
-  <li><strong>Searches:</strong> The "Blocks" and "Depends On" values can now be
-    displayed as columns in [% terms.abug %] list.</li>
-  <li><strong>Searches:</strong> The "is empty" and "is not empty" search operators
-    have been added to the Advanced Search UI. This allows searching for null
-    and not null values for certain fields.</li>
+  <li><strong>UI:</strong> WineHQ Forum links are now accepted in the See Also field on [% terms.bugs %]. [% INCLUDE buglink id=1523814 %]</li>
+  <li><strong>Accessibility:</strong> Color contrast in the default theme was adjusted to comply with WCAG AA 2.0 recommendations. [% INCLUDE buglink id=1798166 %]</li>
+  <li><strong>Email:</strong> Emails about [% terms.bugs %] now include an <code>X-Bugzilla-ID</code> header which lists the [% terms.Bug %] ID, for potential use in filtering. [% INCLUDE buglink id=1403169 %]</li>
 </ul>
 
 <h4>Enhancements for Administrators and Developers</h4>
 
 <ul>
-  <li><strong>Administration:</strong> There are now <kbd>INTEGER</kbd> and
-    <kbd>DATE</kbd> custom field types.</li>
-  <li><strong>Administration:</strong> Filenames used to store product data for
-    "Old Charts" are now based on product IDs to avoid data loss when changing
-    product names.</li>
-  <li><strong>Administration:</strong> JavaScript and CSS files are now minified
-    and concatenated to improve page load performance. When changes are made,
-    <kbd>checksetup.pl</kbd> should be run to regenerate the combined files.</li>
-  <li><strong>[% terms.Bugs %]:</strong> Bugzilla now keeps track of the last
-    time each user visited (that is, loaded the show_bug page in a web browser)
-    each [% terms.bug %]. This could be useful for dashboards or API clients.</li>
-  <li><strong>Database:</strong> Text that contained unicode
-    supplementary characters (outside BMP) was cut off when using MySQL as backend.
-    This has been fixed to prevent data loss.</li>
-  <li><strong>Database:</strong> SSL connections are now possible when using
-    MySQL as backend.</li>
-  <li><strong>Database:</strong> For version 8.x of PostgreSQL, <kbd>plpgsql</kbd>
-    was not always installed by default and <kbd>checksetup.pl</kbd> would
-    generate an error. This has been fixed.</li>
-  <li><strong>Development:</strong> Bugzilla is now HTML5 compliant. As a
-    consequence, Internet Explorer 6 and 7 are no longer supported.</li>
-  <li><strong>Email:</strong> Email generation originally was done before the
-    jobqueue job was inserted. This is now delayed and done by
-    <kbd>jobqueue.pl</kbd> right before sending the email which can improve
-    responsiveness when processing [% terms.bug %] changes.</li>
-  <li><strong>Email:</strong> When a site administrator creates a new user, an
-    email is sent to the user.</li>
-  <li><strong>Email:</strong> For dependency email notifications, the header
-    <kbd>X-B[%%]ugzilla-Type: dep_changed</kbd> is set.</li>
-  <li><strong>Email:</strong> <kbd>whine.pl</kbd> emails now use
-    <kbd>DEFAULT_COLUMN_LIST</kbd> (the same default columns seen in the buglist
-    page) instead of hard coded column list.</li>
-  <li><strong>Security:</strong> Support for increased values for
-    <kbd>PASSWORD_SALT_LENGTH</kbd> without breaking compatibility with old
-    hashes.</li>
+  <li><strong>Database:</strong> <strong>UTF8 data encoding is now
+    enforced.</strong> Older version of [% terms.Bugzilla %] made the
+    conversion of older data to UTF8 optional. Converting your existing data to
+    UTF8 is now mandatory. Your data will be converted when you run
+    <kbd>checksetup.pl</kbd>. If you are not already encoding your data in UTF8
+    you may want to verify that it will correctly convert on a backup copy of
+    your database before upgrading.</li>
+  <li><strong>Database:</strong>If you are using MySQL, the majority of the
+    database tables will also be converted to the InnoDB storage engine. This
+    makes searching more efficent and allows for real referential
+    integrity.</li>
+  <li><strong>Installation:</strong> [% terms.Bugzilla %] now supports
+    Email::MIME version 1.949 and newer, which previously would crash
+    [%+ terms.Bugzilla %] when it tried to send email.
+    [%+ INCLUDE buglink id=1657496 %]</li>
+  <li><strong>Installation:</strong> The <code>testserver.pl</code> script will
+    now work with self-signed SSL certificates if you pass the
+    <code>--self-signed</code> option on the command line. [% INCLUDE buglink id=1851398 %]</li>
+  <li><strong>Installation:</strong> An foreign key error when upgrading from
+    versions prior to 3.0 to 5.0.6 or newer was fixed. [% INCLUDE buglink id=1902375 %]</li>
 </ul>
 
 <h4 id="feat_webservices_other">WebService Changes</h4>
 
 <ul>
-  <li><kbd>B[%%]ug.search</kbd> now allows for full search functionality
-    similar to what is possible using the Advanced Query UI.</li>
-  <li>Basic support for eTag headers has been added to all WebServices
-    to allow for better network performance.</li>
-  <li>Administrators can now change a parameter that filters all email
-    addresses returned in WebService calls similar to filtering that
-    happens in the web UI.</li>
-  <li>WebService calls now support use of API keys for authentication.
-    Usernames and passwords remain supported.</li>
-  <li>Invalid or expired authentication cookies and tokens now throw
-    errors instead of being silently ignored. <kbd>User.valid_login</kbd>
-    can be used to determine if they are still valid or not.</li>
-  <li>WebService calls that are used to create and update [% terms.bugs %]
-    and attachments now support setting and updating of flags.</li>
-  <li><kbd>B[%%]ug.update_attachment</kbd> can update an attachment's
-    metadata as well as its flags.</li>
-  <li>The <kbd>product</kbd> parameter for <kbd>B[%%]ug.possible_duplicates</kbd>
-    has been renamed to <kbd>products</kbd>.</li>
-  <li>Some compatibility fields included in returned data that were marked
-    to be removed in this release are now gone.</li>
-  <li><kbd>Group.get</kbd> has been added to get information about a group and
-    its members.</li>
-  <li><kbd>FlagType.get</kbd> has been added to get information about valid
-    flag types for a given product and component.</li>
-  <li>The deprecated <kbd>B[%%]ug.get_bugs</kbd>, <kbd>B[%%]ug.get_history</kbd>
-    and <kbd>Product.get_products</kbd> methods are no longer supported.
-    They have been renamed to <kbd>B[%%]ug.get</kbd>, <kbd>B[%%]ug.history</kbd>
-    and <kbd>Product.get</kbd> respectively.</li>
+  <li><strong>Group.get</strong> method is now properly marked as Read Only. [% INCLUDE buglink id=1584477 %]</li>
+</ul>
+
+<h4 id="code_changes">Code Changes Which May Affect Customizations and Extensions</h4>
+
+<ul>
+  <li>The Bugzilla::DB object now has a qi attribute which returns a special
+    hashref that can be used inside double-quoted strings to quote database
+    identifiers.<br>
+    <code>
+  my $q = Bugzilla-&gt;dbh-&gt;qi;
+  Bugzilla-&gt;dbh-&gt;do("SELECT COUNT(*) FROM $q-&gt;{groups}");
+    </code><br>
+    [% INCLUDE buglink id=1592129 %]
+  </li>
+  <li>[% terms.Bugzilla %] now uses Email::Address::XS instead of Email::Address for managing email address parsing. If you made use of Email::Address directly you should update to use Email::Address::XS instead. [% INCLUDE buglink id=1853138 %]</li>
 </ul>
 
-<h2 id="code_changes">Code Changes Which May Affect Customizations and Extensions</h2>
+<h3 id="point504">Changes since Version 5.0.4</h3>
+
+<p>Because of the weird way we branched to fix the 5.0.5 numbering problem, the
+changes from 5.0.5 and 5.0.6 are included here in case you're upgrading from
+the 5.0.4.x branch.</p>
+
+<h3>5.0.6</h3>
 
 <ul>
-  <li>Support for CVS, Bonsai and LXR has been removed entirely when viewing
-    attachments. This means that the <kbd>cvsroot</kbd>, <kbd>cvsroot_get</kbd>,
-    <kbd>bonsai_url</kbd>, <kbd>lxr_url</kbd> and <kbd>lxr_root</kbd> parameters
-    are all gone, as well as <kbd>cvsbin</kbd> from the <kbd>localconfig</kbd>
-    file.</li>
-  <li>The <kbd>docs_urlbase</kbd> parameter has been removed. If documentation
-    has not been compiled locally, the "Help" links and other documentation links
-    will redirect to <a href="https://bugzilla.readthedocs.org">bugzilla.readthedocs.org</a>
-    automatically.</li>
-  <li>The <kbd>mostfreqthreshold</kbd> parameter has also been removed.</li>
-  <li>All extensions which define new public WebService methods must list them
-    in a <kbd>PUBLIC_METHODS</kbd> constant. Methods which are not listed there
-    will not be accessible remotely.</li>
-  <li><kbd>JSON::XS</kbd> is now used instead of <kbd>Data::Dumper</kbd> for
-    storage on configuration values in <kbd>data/params</kbd>. This should
-    improve performance when loading the file.</li>
-  <li>A new test has been added to check for reserved words in SQL schema.</li>
-  <li><kbd>Pod::Coverage</kbd> is now used to ensure subroutines are documented.</li>
-  <li>Bugzilla code now uses <kbd>use parent</kbd> instead of <kbd>use base</kbd>
-    in all places applicable.</li>
-  <li>A new hook called <kbd>cgi_headers</kbd> has been added to allow
-    customization of the HTTP headers returned.</li>
-  <li>A new hook called <kbd>user_check_account_creation</kbd> has been added
-    to add extra checks before accepting the creation of a new user account.</li>
+<li><strong>Adminstration:</strong> It is now possible to create more than 255
+flags (up to 65535) [% INCLUDE buglink id=1526703 %]</li>
 </ul>
 
-[% INCLUDE global/footer.html.tmpl %]
+<h3>5.0.5</h3>
+
+<ul>
+<li><strong>User Interface:</strong> Email addresses with apostrophes (yes,
+that's legal) no longer break the "Send Mail to [% terms.Bug %] Assignees"
+button on [% terms.bug %] lists. [% INCLUDE buglink id=1226123 %]</li>
+<li><strong>Installation/Upgrading:</strong> If using MySQL, the
+<code>bugs_fulltext</code> table is now InnoDB instead of MyISAM. This may
+cause your upgrade to take a while. [% INCLUDE buglink id=981487 %]</li>
+<li><strong>Developers:</strong> This release reformats the code according to
+the same conventions as the popular Mojolicious product and includes a
+<code>.perltidyrc</code> to do the same.  You may use whatever coding style you
+want, but all files commited to the repo must be reformatted according to those
+rules.</li>
+<li><strong>Developers:</strong> A new hook <code>template_after_create</code>
+was added, which can be used to manipulate the template object before it gets
+rendered. You can use this to define new Template Toolkit virtual methods or
+filters in extensions.
+(<a href="https://github.com/bugzilla/bugzilla/pull/60">PR #60</a>)</li>
+</ul>
+
+<ul>[% INCLUDE global/footer.html.tmpl %]
+
+[% BLOCK buglink %]
+(<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=[%- id FILTER html -%]">[% terms.Bug %] [%+ id FILTER html -%]</a>)
+[% END %]
 
 [% BLOCK db_req %]
   [% SET m = DB_MODULE.$db %]
@@ -487,9 +286,16 @@ You may use whatever coding style you want, but all files commited to the repo m
        [%+ '<span class="req_new">' IF db_new %]v[% m.db_version FILTER html %]
        [% '</span>' IF db_new %]
        </li>
+    [% IF m.db_blklst_str %]
+    <li><strong>but not versions:</strong> [% m.db_blklst_str FILTER html %]</li>
+    [% END %]
     <li><strong>perl module:</strong>
       [%+ m.dbd.module FILTER html %]
-      [%+ '<span class="req_new">' IF dbd_new %]v[% m.dbd.version FILTER html %]
+      [%+ '<span class="req_new">' IF dbd_new %]
+        [% SWITCH m.dbd.version -%]
+        [%   CASE '' %]any version
+        [%   CASE DEFAULT %]v[% m.dbd.version FILTER html %]
+        [% END %]
       [% '</span>' IF dbd_new %]</li>
   </ul>
 [% END %]
diff --git a/template/en/default/setup/strings.txt.pl b/template/en/default/setup/strings.txt.pl
index 0efc378bb..649e3d5f8 100644
--- a/template/en/default/setup/strings.txt.pl
+++ b/template/en/default/setup/strings.txt.pl
@@ -23,7 +23,7 @@ happens when you are not running checksetup.pl as ##root##. To see the
 problem we ran into, run: ##command##
 END
   bad_executable              => 'not a valid executable: ##bin##',
-  blacklisted                 => '(blacklisted)',
+  blocklisted                 => '(blocklisted)',
   bz_schema_exists_before_220 => <<'END',
 You are upgrading from a version before 2.20, but the bz_schema table
 already exists. This means that you restored a mysqldump into the Bugzilla
@@ -56,10 +56,37 @@ Re-run checksetup.pl in interactive mode (without an 'answers' file)
 to continue.
 END
   cpan_bugzilla_home => "WARNING: Using the Bugzilla directory as the CPAN home.",
+  db_blocklisted     => <<END,
+
+Your ##server## v##vers## is blocklisted. Please check the
+release notes for details or try a different database engine
+or version.
+END
   db_enum_setup      => "Setting up choices for standard drop-down fields:",
+  db_maria_on_mysql  => <<END,
+
+You appear to be using the 'mysql' database driver but the
+database engine Bugzilla connected to identifies as
+ MariaDB ##vers##
+MariaDB 10.6 and newer are no longer compatible with the mysql
+database driver. Bugzilla now uses a separate driver for all
+versions of MariaDB.
+
+Please edit localconfig and set:
+
+\$db_driver = 'mariadb';
+
+then re-run checksetup.pl.
+END
   db_schema_init     => "Initializing bz_schema...",
   db_table_new       => "Adding new table ##table##...",
   db_table_setup     => "Creating tables...",
+  db_too_old         => <<END,
+
+Your ##server## v##vers## is too old. Bugzilla requires version
+##want## or later of ##server##. Please download and install a
+newer version.
+END
   done               => 'done.',
   enter_or_ctrl_c    => "Press Enter to continue or Ctrl-C to exit...",
   error_localconfig_read => <<'END',
@@ -335,6 +362,12 @@ EOT
 InnoDB is disabled in your MySQL installation.
 Bugzilla requires InnoDB to be enabled.
 Please enable it and then re-run checksetup.pl.
+END
+  mysql_innodb_settings => <<'END',
+Bugzilla requires the following MySQL InnoDB settings:
+innodb_file_format = Barracuda
+innodb_file_per_table = ON
+innodb_large_prefix = ON
 END
   mysql_index_renaming => <<'END',
 We are about to rename old indexes. The estimated time to complete
@@ -342,14 +375,15 @@ renaming is ##minutes## minutes. You cannot interrupt this action once
 it has begun. If you would like to cancel, press Ctrl-C now...
 (Waiting 45 seconds...)
 END
-  mysql_utf8_conversion => <<'END',
+  mysql_row_format_conversion => "Converting ##table## to row format ##format##.",
+  mysql_utf8_conversion       => <<'END',
 WARNING: We are about to convert your table storage format to UTF-8. This
          allows Bugzilla to correctly store and sort international characters.
          However, if you have any non-UTF-8 data in your database,
          it ***WILL BE DELETED*** by this process. So, before
          you continue with checksetup.pl, if you have any non-UTF-8
          data (or even if you're not sure) you should press Ctrl-C now
-         to interrupt checksetup.pl, and run contrib/recode.pl to make all
+         to interrupt checksetup.pl, and run contrib/recode.pl to convert 
          the data in your database into UTF-8. You should also back up your
          database before continuing. This will affect every single table
          in the database, even non-Bugzilla tables.
diff --git a/testagent.cgi b/testagent.cgi
index d9d5afd1a..7e97f07dc 100755
--- a/testagent.cgi
+++ b/testagent.cgi
@@ -10,7 +10,7 @@
 # are being run instead of shown. This script does not rely on database access
 # or correct params.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/testserver.pl b/testserver.pl
index 3c01a550e..3f8784618 100755
--- a/testserver.pl
+++ b/testserver.pl
@@ -10,7 +10,7 @@
 # as its only argument.  It attempts to troubleshoot as many installation
 # issues as possible.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
@@ -26,13 +26,17 @@ my $datadir = bz_locations()->{'datadir'};
 
 eval "require LWP; require LWP::UserAgent;";
 my $lwp = $@ ? 0 : 1;
+eval "require LWP::Protocol::https;";
+my $lwpssl = $@ ? 0 : 1;
 
-if ((@ARGV != 1) || ($ARGV[0] !~ /^https?:/i)) {
-  say "Usage: $0 <URL to this Bugzilla installation>";
+if ((@ARGV < 1) || (@ARGV > 2) || ($ARGV[0] !~ /^https?:/i) || (defined($ARGV[1]) && $ARGV[1] ne '--self-signed')) {
+  say "Usage: $0 <URL to this Bugzilla installation> [--self-signed]";
   say "e.g.:  $0 http://www.mycompany.com/bugzilla";
+  say "";
+  say "Pass --self-signed to prevent hostname verification of SSL certificates if needed.";
   exit(1);
 }
-
+my $selfsigned = defined($ARGV[1]) ? 1 : 0;
 
 # Try to determine the GID used by the web server.
 my @pscmds
@@ -213,13 +217,25 @@ sub fetch {
   my $url = shift;
   my $rtn;
   if ($lwp) {
-    my $req = HTTP::Request->new(GET => $url);
-    my $ua  = LWP::UserAgent->new;
-    my $res = $ua->request($req);
-    $rtn = ($res->is_success ? $res->content : undef);
+    if ($url =~ /^https:/i && !$lwpssl) {
+      die ("You need LWP::Protocol::https installed to use https with testserver.pl");
+    } else {
+      my $req = HTTP::Request->new(GET => $url);
+      my $ua  = LWP::UserAgent->new;
+      $ua->ssl_opts( verify_hostname => 0 ) if $selfsigned;
+      my $res = $ua->request($req);
+      if (!$res->is_success) {
+        my $errstr = $res->status_line;
+        print $errstr . "\n";
+        if ($errstr =~ m/certificate/) {
+          say "Try passing --self-signed to skip certificate checks."
+        }
+      }
+      $rtn = ($res->is_success ? $res->content : undef);
+    }
   }
   elsif ($url =~ /^https:/i) {
-    die("You need LWP installed to use https with testserver.pl");
+    die("You need LWP (and LWP::Protocol::https, for LWP 6.02 or newer) installed to use https with testserver.pl");
   }
   else {
     my ($host, $port, $file) = ('', 80, '');
diff --git a/token.cgi b/token.cgi
index cb52479b2..4a7f26a89 100755
--- a/token.cgi
+++ b/token.cgi
@@ -6,7 +6,7 @@
 # This Source Code Form is "Incompatible With Secondary Licenses", as
 # defined by the Mozilla Public License, v. 2.0.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/userprefs.cgi b/userprefs.cgi
index 4c2ad1f15..9faada1c2 100755
--- a/userprefs.cgi
+++ b/userprefs.cgi
@@ -6,7 +6,7 @@
 # This Source Code Form is "Incompatible With Secondary Licenses", as
 # defined by the Mozilla Public License, v. 2.0.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
@@ -390,18 +390,19 @@ sub DoPermissions {
   my (@has_bits, @set_bits);
 
   my $groups
-    = $dbh->selectall_arrayref(
-        "SELECT DISTINCT name, description FROM groups WHERE id IN ("
+    = $dbh->selectall_arrayref('SELECT DISTINCT name, description FROM '
+      . $dbh->quote_identifier('groups')
+      . ' WHERE id IN ('
       . $user->groups_as_string
-      . ") ORDER BY name");
+      . ') ORDER BY name');
   foreach my $group (@$groups) {
     my ($nam, $desc) = @$group;
     push(@has_bits, {"desc" => $desc, "name" => $nam});
   }
   $groups = $dbh->selectall_arrayref(
     'SELECT DISTINCT id, name, description
-                                          FROM groups
-                                         ORDER BY name'
+       FROM ' . $dbh->quote_identifier('groups') . '
+      ORDER BY name'
   );
   foreach my $group (@$groups) {
     my ($group_id, $nam, $desc) = @$group;
diff --git a/votes.cgi b/votes.cgi
index 83f36dc6b..6373f2b91 100755
--- a/votes.cgi
+++ b/votes.cgi
@@ -9,7 +9,7 @@
 # This script remains as a backwards-compatibility URL for before
 # the time that Voting was an extension.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/whine.pl b/whine.pl
index 691df886f..b28397c86 100755
--- a/whine.pl
+++ b/whine.pl
@@ -10,7 +10,7 @@
 # Script Initialization
 ################################################################################
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
@@ -250,7 +250,8 @@ sub get_next_event {
           }
         }
         elsif ($mailto_type == MAILTO_GROUP) {
-          my $sth = $dbh->prepare("SELECT name FROM groups " . "WHERE id=?");
+          my $sth = $dbh->prepare(
+            'SELECT name FROM ' . $dbh->quote_identifier('groups') . ' WHERE id = ?');
           $sth->execute($mailto);
           my $groupname = $sth->fetch->[0];
           my $group_id  = Bugzilla::Group::ValidateGroupName($groupname, $owner);
diff --git a/whineatnews.pl b/whineatnews.pl
index 07a74a387..8b785d030 100755
--- a/whineatnews.pl
+++ b/whineatnews.pl
@@ -14,7 +14,7 @@
 # param. (We have NEW and REOPENED in there to keep compatibility with old
 # Bugzillas.)
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/xmlrpc.cgi b/xmlrpc.cgi
index c0931cb41..4b5230013 100755
--- a/xmlrpc.cgi
+++ b/xmlrpc.cgi
@@ -6,7 +6,7 @@
 # This Source Code Form is "Incompatible With Secondary Licenses", as
 # defined by the Mozilla Public License, v. 2.0.
 
-use 5.10.1;
+use 5.14.0;
 use strict;
 use warnings;
 
diff --git a/xt/lib/Bugzilla/Test/Search.pm b/xt/lib/Bugzilla/Test/Search.pm
index 9d8425cf2..f0792b3a6 100644
--- a/xt/lib/Bugzilla/Test/Search.pm
+++ b/xt/lib/Bugzilla/Test/Search.pm
@@ -637,7 +637,7 @@ sub _create_one_bug {
   my $extra_values = $self->_extra_bug_create_values->{$number};
   foreach my $field (qw(comments remaining_time percentage_complete
     keyword_objects everconfirmed dependson blocked
-    groups_in classification actual_time))
+  groups_in classification actual_time))
   {
     $extra_values->{$field} = $bug->$field;
   }
diff --git a/xt/lib/Bugzilla/Test/Search/AndTest.pm b/xt/lib/Bugzilla/Test/Search/AndTest.pm
index 6132c0eb6..8df566836 100644
--- a/xt/lib/Bugzilla/Test/Search/AndTest.pm
+++ b/xt/lib/Bugzilla/Test/Search/AndTest.pm
@@ -8,7 +8,7 @@
 # This test combines two field/operator combinations using AND in
 # a single boolean chart.
 package Bugzilla::Test::Search::AndTest;
-use parent qw(Bugzilla::Test::Search::OrTest);
+use base qw(Bugzilla::Test::Search::OrTest);
 
 use Bugzilla::Test::Search::Constants;
 use List::MoreUtils qw(all);
diff --git a/xt/lib/Bugzilla/Test/Search/Constants.pm b/xt/lib/Bugzilla/Test/Search/Constants.pm
index 84080cfe8..fe5b0a7b7 100644
--- a/xt/lib/Bugzilla/Test/Search/Constants.pm
+++ b/xt/lib/Bugzilla/Test/Search/Constants.pm
@@ -12,7 +12,7 @@
 # More detailed information on each constant is available in the comments
 # in this file.
 package Bugzilla::Test::Search::Constants;
-use parent qw(Exporter);
+use base qw(Exporter);
 use Bugzilla::Constants;
 use Bugzilla::Util qw(generate_random_password);
 
@@ -958,10 +958,10 @@ use constant INJECTION_BROKEN_FIELD => {
     search      => 1,
     db_skip     => ['Pg'],
     operator_ok => [qw(allwords allwordssubstr anywordssubstr casesubstring
-        changedbefore changedafter greaterthan greaterthaneq
-        lessthan lessthaneq notregexp notsubstring
-        nowordssubstr regexp substring anywords
-        notequals nowords equals anyexact)],
+      changedbefore changedafter greaterthan greaterthaneq
+      lessthan lessthaneq notregexp notsubstring
+      nowordssubstr regexp substring anywords
+    notequals nowords equals anyexact)],
   },
 };
 
@@ -1115,7 +1115,7 @@ use constant CUSTOM_SEARCH_TESTS => (
     contains => [1],
     columns  => ['assigned_to'],
     params   => [
-      {f => 'bug_id', o => 'equals', v => '<1>'},
+      {f => 'bug_id',      o => 'equals', v => '<1>'},
       {f => 'OP'},
       {f => 'CP'},
       {f => 'assigned_to', o => 'substr', v => '@'},
@@ -1210,8 +1210,8 @@ use constant CUSTOM_SEARCH_TESTS => (
     params     => [
       {f => 'bug_id', o => 'equals', v => '<3>'},
       {f => 'OP'},
-      {f => 'OP', j => 'OR'},
-      {f => 'bug_id', o => 'equals', v => '<1>'},
+      {f => 'OP',          j => 'OR'},
+      {f => 'bug_id',      o => 'equals', v => '<1>'},
       {f => 'assigned_to', o => 'equals', v => '<2>'},
       {f => 'CP'},
       {f => 'OP', j => 'OR'},
@@ -1231,8 +1231,8 @@ use constant CUSTOM_SEARCH_TESTS => (
     params     => [
       {f => 'bug_id', o => 'equals', v => '<3>'},
       {f => 'OP'},
-      {f => 'OP', j => 'OR'},
-      {f => 'bug_id', o => 'equals', v => '<1>'},
+      {f => 'OP',          j => 'OR'},
+      {f => 'bug_id',      o => 'equals', v => '<1>'},
       {f => 'assigned_to', o => 'equals', v => '<2>'},
       {f => 'CP'},
       {f => 'OP', j => 'OR'},
diff --git a/xt/lib/Bugzilla/Test/Search/CustomTest.pm b/xt/lib/Bugzilla/Test/Search/CustomTest.pm
index 7455d4828..ed548baa0 100644
--- a/xt/lib/Bugzilla/Test/Search/CustomTest.pm
+++ b/xt/lib/Bugzilla/Test/Search/CustomTest.pm
@@ -9,7 +9,7 @@
 # Tests like this are specified in CUSTOM_SEARCH_TESTS in
 # Bugzilla::Test::Search::Constants.
 package Bugzilla::Test::Search::CustomTest;
-use parent qw(Bugzilla::Test::Search::FieldTest);
+use base qw(Bugzilla::Test::Search::FieldTest);
 use strict;
 use warnings;
 
@@ -55,7 +55,7 @@ sub debug_value {
 }
 
 # The tests we know are broken for this operator/field combination.
-sub _known_broken { return {} }
+sub _known_broken                      { return {} }
 sub contains_known_broken              { return undef }
 sub search_known_broken                { return undef }
 sub field_not_yet_implemented          { return undef }
diff --git a/xt/lib/Bugzilla/Test/Search/FieldTestNormal.pm b/xt/lib/Bugzilla/Test/Search/FieldTestNormal.pm
index 101c09053..51e353eb3 100644
--- a/xt/lib/Bugzilla/Test/Search/FieldTestNormal.pm
+++ b/xt/lib/Bugzilla/Test/Search/FieldTestNormal.pm
@@ -10,7 +10,7 @@
 package Bugzilla::Test::Search::FieldTestNormal;
 use strict;
 use warnings;
-use parent qw(Bugzilla::Test::Search::FieldTest);
+use base qw(Bugzilla::Test::Search::FieldTest);
 
 use Scalar::Util qw(blessed);
 
diff --git a/xt/lib/Bugzilla/Test/Search/InjectionTest.pm b/xt/lib/Bugzilla/Test/Search/InjectionTest.pm
index f34acc6bb..090841e87 100644
--- a/xt/lib/Bugzilla/Test/Search/InjectionTest.pm
+++ b/xt/lib/Bugzilla/Test/Search/InjectionTest.pm
@@ -8,7 +8,7 @@
 # This module represents the SQL Injection tests that get run on a single
 # operator/field combination for Bugzilla::Test::Search.
 package Bugzilla::Test::Search::InjectionTest;
-use parent qw(Bugzilla::Test::Search::FieldTest);
+use base qw(Bugzilla::Test::Search::FieldTest);
 
 use strict;
 use warnings;
diff --git a/xt/lib/Bugzilla/Test/Search/NotTest.pm b/xt/lib/Bugzilla/Test/Search/NotTest.pm
index ea0ecc5b2..07367960d 100644
--- a/xt/lib/Bugzilla/Test/Search/NotTest.pm
+++ b/xt/lib/Bugzilla/Test/Search/NotTest.pm
@@ -12,7 +12,7 @@
 # it to OrTest and AndTest, but without Moose there isn't much of an
 # easy way to do that.
 package Bugzilla::Test::Search::NotTest;
-use parent qw(Bugzilla::Test::Search::FieldTest);
+use base qw(Bugzilla::Test::Search::FieldTest);
 use strict;
 use warnings;
 use Bugzilla::Test::Search::Constants;
diff --git a/xt/lib/Bugzilla/Test/Search/OrTest.pm b/xt/lib/Bugzilla/Test/Search/OrTest.pm
index ebb16089d..82b4aac69 100644
--- a/xt/lib/Bugzilla/Test/Search/OrTest.pm
+++ b/xt/lib/Bugzilla/Test/Search/OrTest.pm
@@ -8,7 +8,7 @@
 # This test combines two field/operator combinations using OR in
 # a single boolean chart.
 package Bugzilla::Test::Search::OrTest;
-use parent qw(Bugzilla::Test::Search::FieldTest);
+use base qw(Bugzilla::Test::Search::FieldTest);
 
 use Bugzilla::Test::Search::Constants;
 use List::MoreUtils qw(all any uniq);
