diff --git a/.bzrignore b/.bzrignore
index f30e8f9ae..4b27e5744 100644
--- a/.bzrignore
+++ b/.bzrignore
@@ -1,4 +1,3 @@
-.htaccess
 /lib/*
 /template/en/custom
 /docs/en/html
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 000000000..f0e4ee1f4
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,12 @@
+#### Details
+<!-- Explain what you did -->
+This PR fixes/adds a feature...
+
+#### Additional info
+* [bmo#](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..c9198c50e
--- /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.0.4 ]
+  pull_request:
+    branches: [ 5.0.4 ]
+  # 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 f30e8f9ae..ba98f70c2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,4 @@
-.htaccess
+/**/.htaccess
 /lib/*
 /template/en/custom
 /docs/en/html
diff --git a/.htaccess b/.htaccess
index aec901005..2f009697c 100644
--- a/.htaccess
+++ b/.htaccess
@@ -5,12 +5,7 @@
       Deny from all
     </IfVersion>
     <IfVersion >= 2.4>
-      <IfModule mod_perl.c>
-        Deny from all
-      </IfModule>
-      <IfModule !mod_perl.c>
-        Require all denied
-      </IfModule>
+      Require all denied
     </IfVersion>
   </IfModule>
   <IfModule !mod_version.c>
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
new file mode 100644
index 000000000..7eada0dd6
--- /dev/null
+++ b/.readthedocs.yaml
@@ -0,0 +1,12 @@
+version: 2
+build:
+  os: "ubuntu-20.04"
+  tools:
+    python: "3.10"
+  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/"
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 01ecfcfb4..000000000
--- a/.travis.yml
+++ /dev/null
@@ -1,78 +0,0 @@
-language: perl
-
-addons:
-  postgresql: "9.1"
-
-perl:
-  - 5.10
-  - 5.12
-  - 5.14
-  - 5.16
-
-env:
-  - TEST_SUITE=sanity
-  - TEST_SUITE=docs
-  - TEST_SUITE=webservices DB=mysql
-  - TEST_SUITE=selenium DB=mysql
-  - TEST_SUITE=webservices DB=pg
-  - TEST_SUITE=selenium DB=pg
-
-matrix:
-  exclude:
-    - perl: 5.10
-      env: TEST_SUITE=docs
-    - perl: 5.12
-      env: TEST_SUITE=docs
-    - perl: 5.14
-      env: TEST_SUITE=docs
-    - perl: 5.10
-      env: TEST_SUITE=webservices DB=mysql
-    - perl: 5.10
-      env: TEST_SUITE=webservices DB=pg
-    - perl: 5.10
-      env: TEST_SUITE=selenium DB=mysql
-    - perl: 5.10
-      env: TEST_SUITE=selenium DB=pg
-    - perl: 5.12
-      env: TEST_SUITE=webservices DB=mysql
-    - perl: 5.12
-      env: TEST_SUITE=webservices DB=pg
-    - perl: 5.12
-      env: TEST_SUITE=selenium DB=mysql
-    - perl: 5.12
-      env: TEST_SUITE=selenium DB=pg
-    - perl: 5.14
-      env: TEST_SUITE=webservices DB=mysql
-    - perl: 5.14
-      env: TEST_SUITE=webservices DB=pg
-    - perl: 5.14
-      env: TEST_SUITE=selenium DB=mysql
-    - perl: 5.14
-      env: TEST_SUITE=selenium DB=pg
-
-before_install:
-  - git clone https://github.com/bugzilla/qa.git -b 5.0 qa
-
-install: true
-
-before_script:
-  - mysql -u root mysql -e "GRANT ALL PRIVILEGES ON *.* TO bugs@localhost IDENTIFIED BY 'bugs'; FLUSH PRIVILEGES;"
-  - psql -c "CREATE USER bugs WITH PASSWORD 'bugs' CREATEDB;" -U postgres
-
-script: ./qa/travis.sh
-
-after_failure:
-  - sudo cat /var/log/apache2/error.log
-
-notifications:
-  irc:
-    channels:
-      - "irc.mozilla.org#qa-bugzilla"
-      - "irc.mozilla.org#bugzilla"
-    template:
-      - "Bugzilla %{branch} : %{author} : %{message}"
-      - "Commit Message : %{commit_message}"
-      - "Commit Link : %{compare_url}"
-      - "Build Link : %{build_url}"
-  on_success: change
-  on_failure: always
diff --git a/Bugzilla.pm b/Bugzilla.pm
index e3184fdb8..e8c078cf8 100644
--- a/Bugzilla.pm
+++ b/Bugzilla.pm
@@ -11,6 +11,8 @@ use 5.10.1;
 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.
@@ -82,11 +84,28 @@ sub init_page {
     }
 
     if (${^TAINT}) {
+        my $path = '';
+        if (ON_WINDOWS) {
+            # On Windows, these paths are tainted, preventing
+            # File::Spec::Win32->tmpdir from using them. But we need
+            # a place to temporary store attachments which are uploaded.
+            foreach my $temp (qw(TMPDIR TMP TEMP WINDIR)) {
+                trick_taint($ENV{$temp}) if $ENV{$temp};
+            }
+            # Some DLLs used by Strawberry Perl are also in c\bin,
+            # see https://rt.cpan.org/Public/Bug/Display.html?id=99104
+            if (!ON_ACTIVESTATE) {
+                my $c_path = $path = dirname($^X);
+                $c_path =~ s/\bperl\b(?=\\bin)/c/;
+                $path .= ";$c_path";
+                trick_taint($path);
+            }
+        }
         # Some environment variables are not taint safe
         delete @::ENV{'PATH', 'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};
         # Some modules throw undefined errors (notably File::Spec::Win32) if
         # PATH is undefined.
-        $ENV{'PATH'} = '';
+        $ENV{'PATH'} = $path;
     }
 
     # Because this function is run live from perl "use" commands of
diff --git a/Bugzilla/Attachment.pm b/Bugzilla/Attachment.pm
index 932fb6b17..33183797b 100644
--- a/Bugzilla/Attachment.pm
+++ b/Bugzilla/Attachment.pm
@@ -865,6 +865,8 @@ sub create {
 sub run_create_validators {
     my ($class, $params) = @_;
 
+    $params->{submitter_id} = Bugzilla->user->id || ThrowUserError('invalid_user');
+
     # Let's validate the attachment content first as it may
     # alter some other attachment attributes.
     $params->{data} = $class->_check_data($params);
@@ -872,7 +874,6 @@ sub run_create_validators {
 
     $params->{creation_ts} ||= Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
     $params->{modification_time} = $params->{creation_ts};
-    $params->{submitter_id} = Bugzilla->user->id || ThrowUserError('invalid_user');
 
     return $params;
 }
diff --git a/Bugzilla/Auth/Verify.pm b/Bugzilla/Auth/Verify.pm
index e44fb06ae..9dc83273b 100644
--- a/Bugzilla/Auth/Verify.pm
+++ b/Bugzilla/Auth/Verify.pm
@@ -106,21 +106,24 @@ sub create_or_update_user {
 
     my $user = new Bugzilla::User($user_id);
 
-    # Now that we have a valid User, we need to see if any data has to be
-    # updated.
+    # Now that we have a valid User, we need to see if any data has to be updated.
+    my $changed = 0;
+
     if ($username && lc($user->login) ne lc($username)) {
         validate_email_syntax($username)
           || return { failure => AUTH_ERROR, error => 'auth_invalid_email',
                       details => {addr => $username} };
         $user->set_login($username);
+        $changed = 1;
     }
     if ($real_name && $user->name ne $real_name) {
         # $real_name is more than likely tainted, but we only use it
         # in a placeholder and we never use it after this.
         trick_taint($real_name);
         $user->set_name($real_name);
+        $changed = 1;
     }
-    $user->update();
+    $user->update() if $changed;
 
     return { user => $user };
 }
diff --git a/Bugzilla/Bug.pm b/Bugzilla/Bug.pm
index bfc2fe0d9..8b4493f85 100644
--- a/Bugzilla/Bug.pm
+++ b/Bugzilla/Bug.pm
@@ -305,15 +305,15 @@ sub new {
     my $param = shift;
 
     # Remove leading "#" mark if we've just been passed an id.
-    if (!ref $param && $param =~ /^#(\d+)$/) {
+    if (!ref $param && $param =~ /^#([0-9]+)$/) {
         $param = $1;
     }
 
     # If we get something that looks like a word (not a number),
     # make it the "name" param.
     if (!defined $param
-        || (!ref($param) && $param !~ /^\d+$/)
-        || (ref($param) && $param->{id} !~ /^\d+$/))
+        || (!ref($param) && $param !~ /^[0-9]+$/)
+        || (ref($param) && $param->{id} !~ /^[0-9]+$/))
     {
         if ($param) {
             my $alias = ref($param) ? $param->{id} : $param;
@@ -533,7 +533,7 @@ sub _preload_referenced_bugs {
     my $referenced_bugs = Bugzilla::Bug->new_from_list(\@ref_bug_ids);
     $_->object_cache_set() foreach @$referenced_bugs;
 
-    return $referenced_bug_ids
+    return $referenced_bug_ids;
 }
 
 # Extract bug IDs mentioned in comments. This is much faster than calling quoteUrls().
@@ -556,16 +556,18 @@ sub _extract_bug_ids {
         my $s = $comment->already_wrapped ? qr/\s/ : qr/\h/;
         my $text = $comment->body;
         # Full bug links
-        push @bug_ids, $text =~ /\b$urlbase_re\Qshow_bug.cgi?id=\E(\d+)(?:\#c\d+)?/g;
+        push @bug_ids, $text =~ /\b$urlbase_re\Qshow_bug.cgi?id=\E([0-9]+)(?:\#c[0-9]+)?/g;
         # bug X
-        my $bug_re = qr/\Q$bug_word\E$s*\#?$s*(\d+)/i;
+        my $bug_re = qr/\Q$bug_word\E$s*\#?$s*([0-9]+)/i;
         push @bug_ids, $text =~ /\b$bug_re/g;
         # bugs X, Y, Z
-        my $bugs_re = qr/\Q$bugs_word\E$s*\#?$s*(\d+)(?:$s*,$s*\#?$s*(\d+))+/i;
+        my $bugs_re = qr/\Q$bugs_word\E$s*\#?$s*([0-9]+)(?:$s*,$s*\#?$s*([0-9]+))+/i;
         push @bug_ids, $text =~ /\b$bugs_re/g;
         # Old duplicate markers
-        push @bug_ids, $text =~ /(?<=^\*\*\*\ This\ bug\ has\ been\ marked\ as\ a\ duplicate\ of\ )(\d+)(?=\ \*\*\*\Z)/;
+        push @bug_ids, $text =~ /(?<=^\*\*\*\ This\ bug\ has\ been\ marked\ as\ a\ duplicate\ of\ )([0-9]+)(?=\ \*\*\*\Z)/;
     }
+    # Make sure to filter invalid bug IDs.
+    @bug_ids = grep { $_ < MAX_INT_32 } @bug_ids;
     return [uniq @bug_ids];
 }
 
@@ -2257,7 +2259,7 @@ sub _check_integer_field {
         ThrowUserError("number_not_integer",
                        {field => $field, num => $orig_value});
     }
-    elsif ($value > MAX_INT_32) {
+    elsif (abs($value) > MAX_INT_32) {
         ThrowUserError("number_too_large",
                        {field => $field, num => $orig_value, max_num => MAX_INT_32});
     }
diff --git a/Bugzilla/BugMail.pm b/Bugzilla/BugMail.pm
index 9ebf3ea7d..1aff405d8 100644
--- a/Bugzilla/BugMail.pm
+++ b/Bugzilla/BugMail.pm
@@ -19,7 +19,9 @@ use Bugzilla::Bug;
 use Bugzilla::Comment;
 use Bugzilla::Mailer;
 use Bugzilla::Hook;
+use Bugzilla::MIME;
 
+use Encode qw();
 use Date::Parse;
 use Date::Format;
 use Scalar::Util qw(blessed);
@@ -168,8 +170,8 @@ sub Send {
         }
 
         if ($change->{field_name} eq 'dependson' || $change->{field_name} eq 'blocked') {
-            push @referenced_bug_ids, split(/[\s,]+/, $change->{old});
-            push @referenced_bug_ids, split(/[\s,]+/, $change->{new});
+            push @referenced_bug_ids, split(/[\s,]+/, $change->{old} // '');
+            push @referenced_bug_ids, split(/[\s,]+/, $change->{new} // '');
         }
     }
 
@@ -435,6 +437,7 @@ sub _generate_bugmail {
     my $user = $vars->{to_user};
     my $template = Bugzilla->template_inner($user->setting('lang'));
     my ($msg_text, $msg_html, $msg_header);
+    state $use_utf8 = Bugzilla->params->{'utf8'};
 
     $template->process("email/bugmail-header.txt.tmpl", $vars, \$msg_header)
         || ThrowTemplateError($template->error());
@@ -442,32 +445,38 @@ sub _generate_bugmail {
         || ThrowTemplateError($template->error());
 
     my @parts = (
-        Email::MIME->create(
+        Bugzilla::MIME->create(
             attributes => {
-                content_type => "text/plain",
+                content_type => 'text/plain',
+                charset      => $use_utf8 ? 'UTF-8' : 'iso-8859-1',
+                encoding     => 'quoted-printable',
             },
-            body => $msg_text,
+            body_str => $msg_text,
+            encode_check => Encode::FB_DEFAULT
         )
     );
     if ($user->setting('email_format') eq 'html') {
         $template->process("email/bugmail.html.tmpl", $vars, \$msg_html)
             || ThrowTemplateError($template->error());
-        push @parts, Email::MIME->create(
+        push @parts, Bugzilla::MIME->create(
             attributes => {
-                content_type => "text/html",         
+                content_type => 'text/html',
+                charset      => $use_utf8 ? 'UTF-8' : 'iso-8859-1',
+                encoding     => 'quoted-printable',
             },
-            body => $msg_html,
+            body_str => $msg_html,
+            encode_check => Encode::FB_DEFAULT
         );
     }
 
-    # TT trims the trailing newline, and threadingmarker may be ignored.
-    my $email = new Email::MIME("$msg_header\n");
-    if (scalar(@parts) == 1) {
-        $email->content_type_set($parts[0]->content_type);
-    } else {
+    my $email = Bugzilla::MIME->new($msg_header);
+
+    # 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.
-        $email->charset_set('UTF-8') if Bugzilla->params->{'utf8'};
+        $email->charset_set('UTF-8') if $use_utf8;
     }
     $email->parts_set(\@parts);
     return $email;
diff --git a/Bugzilla/BugUrl.pm b/Bugzilla/BugUrl.pm
index 1e836ca1e..1d75fe8f1 100644
--- a/Bugzilla/BugUrl.pm
+++ b/Bugzilla/BugUrl.pm
@@ -108,8 +108,9 @@ sub _do_list_select {
     my $objects = $class->SUPER::_do_list_select(@_);
 
     foreach my $object (@$objects) {
-        eval "use " . $object->class; die $@ if $@;
-        bless $object, $object->class;
+        eval "use " . $object->class;
+        # If the class cannot be loaded, then we build a generic object.
+        bless $object, ($@ ? 'Bugzilla::BugUrl' : $object->class);
     }
 
     return $objects
diff --git a/Bugzilla/CGI.pm b/Bugzilla/CGI.pm
index 0b8a48697..9b1ff9235 100644
--- a/Bugzilla/CGI.pm
+++ b/Bugzilla/CGI.pm
@@ -66,7 +66,7 @@ sub new {
             # else we will be redirected outside Bugzilla.
             my $script_name = $self->script_name;
             $path_info =~ s/^\Q$script_name\E//;
-            if ($path_info) {
+            if ($script_name && $path_info) {
                 print $self->redirect($self->url(-path => 0, -query => 1));
             }
         }
@@ -283,11 +283,74 @@ sub close_standby_message {
         print $self->multipart_end();
         print $self->multipart_start(-type => $contenttype);
     }
-    else {
+    elsif (!$self->{_header_done}) {
         print $self->header($contenttype);
     }
 }
 
+our $ALLOW_UNSAFE_RESPONSE = 0;
+# responding to text/plain or text/html is safe
+# responding to any request with a referer header is safe
+# some things need to have unsafe responses (attachment.cgi)
+# everything else should get a 403.
+sub _prevent_unsafe_response {
+    my ($self, $headers) = @_;
+    my $safe_content_type_re = qr{
+        ^ (*COMMIT) # COMMIT makes the regex faster
+                    # by preventing back-tracking. see also perldoc pelre.
+        # application/x-javascript, xml, atom+xml, rdf+xml, xml-dtd, and json
+        (?: application/ (?: x(?: -javascript | ml (?: -dtd )? )
+                           | (?: atom | rdf) \+ xml
+                           | json )
+        # text/csv, text/calendar, text/plain, and text/html
+          | text/ (?: c (?: alendar | sv )
+                    | plain
+                    | html )
+        # used for HTTP push responses
+          | multipart/x-mixed-replace)
+    }sx;
+    my $safe_referer_re = do {
+        # Note that urlbase must end with a /.
+        # It almost certainly does, but let's be extra careful.
+        my $urlbase = correct_urlbase();
+        $urlbase =~ s{/$}{};
+        qr{
+            # Begins with literal urlbase
+            ^ (*COMMIT)
+            \Q$urlbase\E
+            # followed by a slash or end of string
+            (?: /
+              | $ )
+        }sx
+    };
+
+    return if $ALLOW_UNSAFE_RESPONSE;
+
+    if (Bugzilla->usage_mode == USAGE_MODE_BROWSER) {
+        # Safe content types are ones that arn't images.
+        # For now let's assume plain text and html are not valid images.
+        my $content_type         = $headers->{'-type'} // $headers->{'-content_type'} // 'text/html';
+        my $is_safe_content_type = $content_type =~ $safe_content_type_re;
+
+        # Safe referers are ones that begin with the urlbase.
+        my $referer         = $self->referer;
+        my $is_safe_referer = $referer && $referer =~ $safe_referer_re;
+
+        if (!$is_safe_referer && !$is_safe_content_type) {
+            print $self->SUPER::header(-type => 'text/html',  -status => '403 Forbidden');
+            if ($content_type ne 'text/html') {
+                print "Untrusted Referer Header\n";
+                if ($ENV{MOD_PERL}) {
+                    my $r = $self->r;
+                    $r->rflush;
+                    $r->status(200);
+                }
+            }
+            exit;
+        }
+    }
+}
+
 # Override header so we can add the cookies in
 sub header {
     my $self = shift;
@@ -302,6 +365,7 @@ sub header {
     else {
         %headers = @_;
     }
+    $self->_prevent_unsafe_response(\%headers);
 
     if ($self->{'_content_disp'}) {
         $headers{'-content_disposition'} = $self->{'_content_disp'};
@@ -356,6 +420,7 @@ sub header {
     Bugzilla::Hook::process('cgi_headers',
         { cgi => $self, headers => \%headers }
     );
+    $self->{_header_done} = 1;
 
     return $self->SUPER::header(%headers) || "";
 }
diff --git a/Bugzilla/Chart.pm b/Bugzilla/Chart.pm
index 3c69006aa..faf7a4785 100644
--- a/Bugzilla/Chart.pm
+++ b/Bugzilla/Chart.pm
@@ -420,11 +420,9 @@ sub dump {
 
     # Make sure we've read in our data
     my $data = $self->data;
-    
+
     require Data::Dumper;
-    say "<pre>Bugzilla::Chart object:";
-    print html_quote(Data::Dumper::Dumper($self));
-    print "</pre>";
+    return Data::Dumper::Dumper($self);
 }
 
 1;
diff --git a/Bugzilla/Component.pm b/Bugzilla/Component.pm
index 9bc0a4493..d5a6ece5d 100644
--- a/Bugzilla/Component.pm
+++ b/Bugzilla/Component.pm
@@ -148,7 +148,8 @@ sub remove_from_db {
     $dbh->bz_start_transaction();
 
     # Products must have at least one component.
-    if (scalar(@{$self->product->components}) == 1) {
+    my @components = @{ $self->product->components };
+    if (scalar(@components) == 1) {
         ThrowUserError('component_is_last', { comp => $self });
     }
 
@@ -165,6 +166,8 @@ sub remove_from_db {
             ThrowUserError('component_has_bugs', {nb => $self->bug_count});
         }
     }
+    # Update the list of components in the product object.
+    $self->product->{components} = [grep { $_->id != $self->id } @components];
     $self->SUPER::remove_from_db();
 
     $dbh->bz_commit_transaction();
diff --git a/Bugzilla/Config.pm b/Bugzilla/Config.pm
index 1e22b5239..458616701 100644
--- a/Bugzilla/Config.pm
+++ b/Bugzilla/Config.pm
@@ -16,10 +16,9 @@ use autodie qw(:default);
 
 use Bugzilla::Constants;
 use Bugzilla::Hook;
-use Bugzilla::Util qw(trick_taint);
+use Bugzilla::Util qw(trick_taint read_text write_text);
 
 use JSON::XS;
-use File::Slurp;
 use File::Temp;
 use File::Basename;
 
@@ -266,27 +265,6 @@ sub update_params {
         $op_file->close;
     }
 
-    if (ON_WINDOWS && !-e SENDMAIL_EXE
-        && $param->{'mail_delivery_method'} eq 'Sendmail')
-    {
-        my $smtp = $answer->{'SMTP_SERVER'};
-        if (!$smtp) {
-            print "\nBugzilla requires an SMTP server to function on",
-                  " Windows.\nPlease enter your SMTP server's hostname: ";
-            $smtp = <STDIN>;
-            chomp $smtp;
-            if ($smtp) {
-                $param->{'smtpserver'} = $smtp;
-             }
-             else {
-                print "\nWarning: No SMTP Server provided, defaulting to",
-                      " localhost\n";
-            }
-        }
-
-        $param->{'mail_delivery_method'} = 'SMTP';
-    }
-
     write_params($param);
 
     if (-e $old_file) {
@@ -305,7 +283,7 @@ sub write_params {
     my $param_file = bz_locations()->{'datadir'} . '/params.json';
 
     my $json_data = JSON::XS->new->canonical->pretty->encode($param_data);
-    write_file($param_file, { binmode => ':utf8', atomic => 1 }, \$json_data);
+    write_text($param_file, $json_data);
 
     # It's not common to edit parameters and loading
     # Bugzilla::Install::Filesystem is slow.
@@ -322,8 +300,8 @@ sub read_param_file {
     my $file = bz_locations()->{'datadir'} . '/params.json';
 
     if (-e $file) {
-        my $data;
-        read_file($file, binmode => ':utf8', buf_ref => \$data);
+        my $data = read_text($file);
+        trick_taint($data);
 
         # If params.json has been manually edited and e.g. some quotes are
         # missing, we don't want JSON::XS to leak the content of the file
@@ -336,7 +314,13 @@ sub read_param_file {
         }
         # JSON::XS doesn't detaint data for us.
         foreach my $key (keys %params) {
-            trick_taint($params{$key}) if defined $params{$key};
+            if (ref($params{$key}) eq "ARRAY") {
+                foreach my $item (@{$params{$key}}) {
+                    trick_taint($item);
+                }
+            } else {
+                trick_taint($params{$key}) if defined $params{$key};
+            }
         }
     }
     elsif ($ENV{'SERVER_SOFTWARE'}) {
diff --git a/Bugzilla/Config/Common.pm b/Bugzilla/Config/Common.pm
index 414894773..bd9b0bf84 100644
--- a/Bugzilla/Config/Common.pm
+++ b/Bugzilla/Config/Common.pm
@@ -45,7 +45,10 @@ sub check_multi {
         return "";
     }
     elsif ($param->{'type'} eq 'm' || $param->{'type'} eq 'o') {
-        foreach my $chkParam (split(',', $value)) {
+        if (ref($value) ne "ARRAY") {
+            $value = [split(',', $value)]
+        }
+        foreach my $chkParam (@$value) {
             unless (scalar(grep {$_ eq $chkParam} (@{$param->{'choices'}}))) {
                 return "Invalid choice '$chkParam' for multi-select list param '$param->{'name'}'";
             }
@@ -249,8 +252,8 @@ sub check_font_file {
     $font = trim($font);
     return '' unless $font;
 
-    if ($font !~ /\.ttf$/) {
-        return "The file must point to a TrueType font file (its extension must be .ttf)"
+    if ($font !~ /\.(ttf|otf)$/) {
+        return "The file must point to a TrueType or OpenType font file (its extension must be .ttf or .otf)"
     }
     if (! -f $font) {
         return "The file '$font' cannot be found. Make sure you typed the full path to the file"
@@ -299,7 +302,7 @@ sub check_mail_delivery_method {
     my $check = check_multi(@_);
     return $check if $check;
     my $mailer = shift;
-    if ($mailer eq 'sendmail' and ON_WINDOWS) {
+    if ($mailer eq 'Sendmail' and ON_WINDOWS) {
         # look for sendmail.exe 
         return "Failed to locate " . SENDMAIL_EXE
             unless -e SENDMAIL_EXE;
@@ -346,6 +349,8 @@ sub check_smtp_server {
     my $host = shift;
     my $port;
 
+    return '' unless $host;
+
     if ($host =~ /:/) {
         ($host, $port) = split(/:/, $host, 2);
         unless ($port && detaint_natural($port)) {
diff --git a/Bugzilla/Constants.pm b/Bugzilla/Constants.pm
index 837ed895d..0e50f82fc 100644
--- a/Bugzilla/Constants.pm
+++ b/Bugzilla/Constants.pm
@@ -200,11 +200,11 @@ use Memoize;
 # CONSTANTS
 #
 # Bugzilla version
-use constant BUGZILLA_VERSION => "5.0";
+use constant BUGZILLA_VERSION => "5.0.4.1";
 
 # 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.
-use constant REST_DOC => "http://www.bugzilla.org/docs/tip/en/html/api/";
+use constant REST_DOC => 'https://bugzilla.readthedocs.org/en/5.0/api/';
 
 # Location of the remote and local XML files to track new releases.
 use constant REMOTE_FILE => 'http://updates.bugzilla.org/bugzilla-update.xml';
@@ -508,16 +508,42 @@ use constant INSTALLATION_MODE_NON_INTERACTIVE => 1;
 use constant DB_MODULE => {
     # MySQL 5.0.15 was the first production 5.0.x release.
     'mysql' => {db => 'Bugzilla::DB::Mysql', db_version => '5.0.15',
-                dbd => { 
+                db_blocklist => ['^[89]\.'],
+                # the following is a "human-readable" version to show in the
+                # release notes
+                db_blklst_str => '>= 8.0',
+                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',
                 },
                 name => 'MySQL'},
+
+    # MariaDB is a drop-in replacement for MySQL and works with Bugzilla
+    'mariadb' => {db => 'Bugzilla::DB::Mysql', db_version => '5.1',
+                  # MariaDB is indistinguishable from MySQL, but skipped 8 and
+                  # 9 so blocklist it anyway in case someone has the driver set
+                  # to mariadb but actually has MySQL.
+                  db_blocklist => ['^[89]\.'],
+                  # no string to show the user on the release notes though.
+                  dbd        => {
+                    package => 'DBD-mysql',
+                    module  => 'DBD::mysql',
+
+                    # Disallow development versions
+                    blocklist => ['_'],
+
+                    # For UTF-8 support. 4.001 makes sure that blobs aren't
+                    # marked as UTF-8.
+                    version => '4.001',
+                  },
+                  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'    => {db => 'Bugzilla::DB::Pg', db_version => '8.03.0000',
diff --git a/Bugzilla/DB.pm b/Bugzilla/DB.pm
index c9eaf6e9e..f59a32feb 100644
--- a/Bugzilla/DB.pm
+++ b/Bugzilla/DB.pm
@@ -204,17 +204,33 @@ sub bz_check_server_version {
     $self->disconnect;
 
     my $sql_want = $db->{db_version};
+    my $sql_dontwant = exists $db->{db_blocklist} ? $db->{db_blocklist} : [];
     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) {
         Bugzilla::Install::Requirements::_checking_for({
             package => $sql_server, wanted => $sql_want,
-            found   => $sql_vers, ok => $version_ok });
+            found   => $sql_vers, 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 <<EOT;
+
+Your $sql_server v$sql_vers is blocklisted. Please check the
+release notes for details or try a different database engine
+or version.
+
+EOT
+    }
     if (!$version_ok) {
         die <<EOT;
 
@@ -363,6 +379,31 @@ sub sql_position {
     return "POSITION($fragment IN $text)";
 }
 
+sub sql_like {
+    my ($self, $fragment, $column) = @_;
+
+    my $quoted = $self->quote($fragment);
+
+    return $self->sql_position($quoted, $column) . " > 0";
+}
+
+sub sql_ilike {
+    my ($self, $fragment, $column) = @_;
+
+    my $quoted = $self->quote($fragment);
+
+    return $self->sql_iposition($quoted, $column) . " > 0";
+}
+
+sub sql_not_ilike {
+    my ($self, $fragment, $column) = @_;
+
+    my $quoted = $self->quote($fragment);
+
+    return $self->sql_iposition($quoted, $column) . " = 0";
+}
+
+
 sub sql_group_by {
     my ($self, $needed_columns, $optional_columns) = @_;
 
@@ -1510,7 +1551,7 @@ __END__
 
 =head1 NAME
 
-Bugzilla::DB - Database access routines, using L<DBI>
+Bugzilla::DB - Database access routines, using L<DBI|https://metacpan.org/pod/DBI>
 
 =head1 SYNOPSIS
 
@@ -1771,7 +1812,7 @@ The constructor should create a DSN from the parameters provided and
 then call C<db_new()> method of its super class to create a new
 class instance. See L<db_new> description in this module. As per
 DBI documentation, all class variables must be prefixed with
-"private_". See L<DBI>.
+"private_". See L<DBI|https://metacpan.org/pod/DBI>.
 
 =back
 
@@ -2014,6 +2055,73 @@ Formatted SQL for substring search (scalar)
 
 Just like L</sql_position>, but case-insensitive.
 
+=item C<sql_like>
+
+=over
+
+=item B<Description>
+
+Outputs SQL to search for an instance of a string (fragment)
+in a table column (column).
+
+Note that the fragment must not be quoted. L</sql_like> will
+quote the fragment itself.
+
+This is a case sensitive search.
+
+Note: This does not necessarily generate an ANSI LIKE statement, but
+could be overridden to do so in a database subclass if required.
+
+=item B<Params>
+
+=over
+
+=item C<$fragment> - the string fragment that we are searching for (scalar)
+
+=item C<$column> - the column to search
+
+=back
+
+=item B<Returns>
+
+Formatted SQL to return results from columns that contain the fragment.
+
+=back
+
+=item C<sql_ilike>
+
+Just like L</sql_like>, but case-insensitive.
+
+=item C<sql_not_ilike>
+
+=over
+
+=item B<Description>
+
+Outputs SQL to search for columns (column) that I<do not> contain
+instances of the string (fragment).
+
+Note that the fragment must not be quoted. L</sql_not_ilike> will
+quote the fragment itself.
+
+This is a case insensitive search.
+
+=item B<Params>
+
+=over
+
+=item C<$fragment> - the string fragment that we are searching for (scalar)
+
+=item C<$column> - the column to search
+
+=back
+
+=item B<Returns>
+
+Formated sql to return results from columns that do not contain the fragment
+
+=back
+
 =item C<sql_group_by>
 
 =over
@@ -2242,7 +2350,8 @@ These methods return information about data in the database.
 Returns the last serial number, usually from a previous INSERT.
 
 Must be executed directly following the relevant INSERT.
-This base implementation uses L<DBI/last_insert_id>. If the
+This base implementation uses DBI's
+L<last_insert_id|https://metacpan.org/pod/DBI#last_insert_id>. If the
 DBD supports it, it is the preffered way to obtain the last
 serial index. If it is not supported, the DB-specific code
 needs to override this function.
@@ -2692,7 +2801,7 @@ our check for implementation of C<new> by derived class useless.
 
 =head1 SEE ALSO
 
-L<DBI>
+L<DBI|https://metacpan.org/pod/DBI>
 
 L<Bugzilla::Constants/DB_MODULE>
 
diff --git a/Bugzilla/DB/Pg.pm b/Bugzilla/DB/Pg.pm
index a950c575f..cbf8d7af1 100644
--- a/Bugzilla/DB/Pg.pm
+++ b/Bugzilla/DB/Pg.pm
@@ -126,6 +126,36 @@ sub sql_position {
     return "POSITION(${fragment}::text IN ${text}::text)";
 }
 
+sub sql_like {
+    my ($self, $fragment, $column, $not) = @_;
+    $not //= '';
+
+    return "${column}::text $not LIKE " . $self->sql_like_escape($fragment) . " ESCAPE '|'";
+}
+
+sub sql_ilike {
+    my ($self, $fragment, $column, $not) = @_;
+    $not //= '';
+
+    return "${column}::text $not ILIKE " . $self->sql_like_escape($fragment) . " ESCAPE '|'";
+}
+
+sub sql_not_ilike {
+    return shift->sql_ilike(@_, 'NOT');
+}
+
+# Escapes any % or _ characters which are special in a LIKE match.
+# Also performs a $dbh->quote to escape any quote characters.
+sub sql_like_escape {
+    my ($self, $fragment) = @_;
+
+    $fragment =~ s/\|/\|\|/g;  # escape the escape character if it appears
+    $fragment =~ s/%/\|%/g;    # percent and underscore are the special match
+    $fragment =~ s/_/\|_/g;    # characters in SQL.
+
+    return $self->quote("%$fragment%");
+}
+
 sub sql_regexp {
     my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_;
     $real_pattern ||= $pattern;
@@ -448,6 +478,39 @@ sub bz_table_list_real {
 
 1;
 
+=head2 Functions
+
+=over
+
+=item C<sql_like_escape>
+
+=over
+
+=item B<Description>
+
+The postgres versions of the sql_like methods use the ANSI SQL LIKE
+statements to perform substring searching.  To prevent issues with
+users attempting to search for strings containing special characters
+associated with LIKE, we escape them out so they don't affect the search
+terms.
+
+=item B<Params>
+
+=over
+
+=item C<$fragment> - The string fragment in need of escaping and quoting
+
+=back
+
+=item B<Returns>
+
+The fragment with any pre existing %,_,| characters escaped out, wrapped in
+percent characters and quoted.
+
+=back
+
+=back
+
 =head1 B<Methods in need of POD>
 
 =over
@@ -462,6 +525,12 @@ sub bz_table_list_real {
 
 =item sql_position
 
+=item sql_like
+
+=item sql_ilike
+
+=item sql_not_ilike
+
 =item sql_limit
 
 =item sql_not_regexp
diff --git a/Bugzilla/DB/Schema/Mysql.pm b/Bugzilla/DB/Schema/Mysql.pm
index 0195fcb06..7ff8ade9f 100644
--- a/Bugzilla/DB/Schema/Mysql.pm
+++ b/Bugzilla/DB/Schema/Mysql.pm
@@ -316,7 +316,7 @@ sub column_info_to_column {
             $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 =~ /^(-)?(\d+)(.\d+)?$/);
+            $default = $dbh->quote($default) if !($default =~ /^(-)?([0-9]+)(\.[0-9]+)?$/);
             $column->{DEFAULT} = $default;
         }
     }
diff --git a/Bugzilla/DB/Sqlite.pm b/Bugzilla/DB/Sqlite.pm
index ddafc1696..a56ed31ad 100644
--- a/Bugzilla/DB/Sqlite.pm
+++ b/Bugzilla/DB/Sqlite.pm
@@ -219,6 +219,7 @@ sub sql_date_format {
     my ($self, $date, $format) = @_;
     $format = "%Y.%m.%d %H:%M:%S" if !$format;
     $format =~ s/\%i/\%M/g;
+    $format =~ s/\%s/\%S/g;
     return "STRFTIME(" . $self->quote($format) . ", $date)";
 }
 
diff --git a/Bugzilla/Group.pm b/Bugzilla/Group.pm
index 07b78e366..f7a50f7f1 100644
--- a/Bugzilla/Group.pm
+++ b/Bugzilla/Group.pm
@@ -383,6 +383,7 @@ sub create {
     my $dbh = Bugzilla->dbh;
 
     my $silently = delete $params->{silently};
+    my $use_in_all_products = delete $params->{use_in_all_products};
     if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE and !$silently) {
         print get_text('install_group_create', { name => $params->{name} }),
               "\n";
@@ -406,6 +407,14 @@ sub create {
         $sth->execute($admin->id, $group->id, GROUP_VISIBLE);
     }
 
+    # Permit all existing products to use the new group if requested.
+    if ($use_in_all_products) {
+        $dbh->do('INSERT INTO group_control_map
+                  (group_id, product_id, membercontrol, othercontrol)
+                  SELECT ?, products.id, ?, ? FROM products',
+                  undef, ($group->id, CONTROLMAPSHOWN, CONTROLMAPNA));
+    }
+
     $group->_rederive_regexp() if $group->user_regexp;
 
     Bugzilla::Hook::process('group_end_of_create', { group => $group });
@@ -524,8 +533,11 @@ provides, in addition to any methods documented below.
 
 Note that in addition to what L<Bugzilla::Object/create($params)>
 normally does, this function also makes the new group be inherited
-by the C<admin> group. That is, the C<admin> group will automatically
-be a member of this group.
+by the C<admin> group and optionally inserts access controls for
+this group into all existing products. That is, the C<admin> group
+will automatically be a member of this group and bugs for all
+products may optionally be restricted to this group by group
+members.
 
 =item C<ValidateGroupName($name, @users)>
 
diff --git a/Bugzilla/Install/DB.pm b/Bugzilla/Install/DB.pm
index 96f14ec0f..ed2539251 100644
--- a/Bugzilla/Install/DB.pm
+++ b/Bugzilla/Install/DB.pm
@@ -729,6 +729,9 @@ sub update_table_definitions {
     # 2014-11-10 dkl@mozilla.com - Bug 1093928
     $dbh->bz_drop_column('longdescs', 'is_markdown');
 
+    # 2015-12-16 LpSolit@gmail.com - Bug 1232578
+    _sanitize_audit_log_table();
+
     ################################################################
     # New --TABLE-- changes should go *** A B O V E *** this point #
     ################################################################
@@ -3914,6 +3917,30 @@ sub _update_alias {
     $dbh->bz_drop_column('bugs', 'alias');
 }
 
+sub _sanitize_audit_log_table {
+    my $dbh = Bugzilla->dbh;
+
+    # Replace hashed passwords by a generic comment.
+    my $class = 'Bugzilla::User';
+    my $field = 'cryptpassword';
+
+    my $hashed_passwd =
+      $dbh->selectcol_arrayref('SELECT added FROM audit_log WHERE class = ? AND field = ?
+                                AND ' . $dbh->sql_not_ilike('hashed_with_', 'added'),
+                                undef, ($class, $field));
+    if (@$hashed_passwd) {
+        say "Sanitizing hashed passwords stored in the 'audit_log' table...";
+        my $sth = $dbh->prepare('UPDATE audit_log SET added = ?
+                                 WHERE class = ? AND field = ? AND added = ?');
+
+        foreach my $passwd (@$hashed_passwd) {
+            my (undef, $sanitized_passwd) =
+              Bugzilla::Object::_sanitize_audit_log($class, $field, [undef, $passwd]);
+            $sth->execute($sanitized_passwd, $class, $field, $passwd);
+        }
+    }
+}
+
 1;
 
 __END__
diff --git a/Bugzilla/Install/Filesystem.pm b/Bugzilla/Install/Filesystem.pm
index cf0a5b0ca..d30ae18dc 100644
--- a/Bugzilla/Install/Filesystem.pm
+++ b/Bugzilla/Install/Filesystem.pm
@@ -31,7 +31,6 @@ use File::Path;
 use File::Basename;
 use File::Copy qw(move);
 use File::Spec;
-use File::Slurp;
 use IO::File;
 use POSIX ();
 
@@ -52,12 +51,7 @@ use constant HT_DEFAULT_DENY => <<EOT;
     Deny from all
   </IfVersion>
   <IfVersion >= 2.4>
-    <IfModule mod_perl.c>
-      Deny from all
-    </IfModule>
-    <IfModule !mod_perl.c>
-      Require all denied
-    </IfModule>
+    Require all denied
   </IfVersion>
 </IfModule>
 <IfModule !mod_version.c>
@@ -359,12 +353,7 @@ EOT
       Allow from all
     </IfVersion>
     <IfVersion >= 2.4>
-      <IfModule mod_perl.c>
-        Allow from all
-      </IfModule>
-      <IfModule !mod_perl.c>
-        Require all granted
-      </IfModule>
+      Require all granted
     </IfVersion>
   </IfModule>
   <IfModule !mod_version.c>
@@ -378,12 +367,7 @@ EOT
     Deny from all
   </IfVersion>
   <IfVersion >= 2.4>
-    <IfModule mod_perl.c>
-      Deny from all
-    </IfModule>
-    <IfModule !mod_perl.c>
-      Require all denied
-    </IfModule>
+    Require all denied
   </IfVersion>
 </IfModule>
 <IfModule !mod_version.c>
@@ -403,14 +387,8 @@ EOT
       Deny from all
     </IfVersion>
     <IfVersion >= 2.4>
-      <IfModule mod_perl.c>
-        Allow from 192.20.225.0/24
-        Deny from all
-      </IfModule>
-      <IfModule !mod_perl.c>
-        Require ip 192.20.225.0/24
-        Require all denied
-      </IfModule>
+      Require ip 192.20.225.0/24
+      Require all denied
     </IfVersion>
   </IfModule>
   <IfModule !mod_version.c>
@@ -426,12 +404,7 @@ EOT
       Allow from all
     </IfVersion>
     <IfVersion >= 2.4>
-      <IfModule mod_perl.c>
-        Allow from all
-      </IfModule>
-      <IfModule !mod_perl.c>
-        Require all granted
-      </IfModule>
+      Require all granted
     </IfVersion>
   </IfModule>
   <IfModule !mod_version.c>
@@ -445,12 +418,7 @@ EOT
     Deny from all
   </IfVersion>
   <IfVersion >= 2.4>
-    <IfModule mod_perl.c>
-      Deny from all
-    </IfModule>
-    <IfModule !mod_perl.c>
-      Require all denied
-    </IfModule>
+    Require all denied
   </IfVersion>
 </IfModule>
 <IfModule !mod_version.c>
@@ -467,12 +435,7 @@ EOT
       Allow from all
     </IfVersion>
     <IfVersion >= 2.4>
-      <IfModule mod_perl.c>
-        Allow from all
-      </IfModule>
-      <IfModule !mod_perl.c>
-        Require all granted
-      </IfModule>
+      Require all granted
     </IfVersion>
   </IfModule>
   <IfModule !mod_version.c>
@@ -486,12 +449,7 @@ EOT
     Deny from all
   </IfVersion>
   <IfVersion >= 2.4>
-    <IfModule mod_perl.c>
-      Deny from all
-    </IfModule>
-    <IfModule !mod_perl.c>
-      Require all denied
-    </IfModule>
+    Require all denied
   </IfVersion>
 </IfModule>
 <IfModule !mod_version.c>
@@ -577,7 +535,7 @@ sub update_filesystem {
 
     # Remove old assets htaccess file to force recreation with correct values.
     if (-e "$assetsdir/.htaccess") {
-        if (read_file("$assetsdir/.htaccess") =~ /<FilesMatch \\\.css\$>/) {
+        if (read_text("$assetsdir/.htaccess") =~ /<FilesMatch \\\.css\$>/) {
             unlink("$assetsdir/.htaccess");
         }
     }
@@ -823,22 +781,21 @@ sub _update_old_charts {
 # to product IDs.
 sub _update_old_mining_filenames {
     my ($miningdir) = @_;
+    my $dbh = Bugzilla->dbh;
     my @conversion_errors;
 
-    require Bugzilla::Product;
-
     # We use a dummy product instance with ID 0, representing all products
     my $product_all = {id => 0, name => '-All-'};
-    bless($product_all, 'Bugzilla::Product');
 
     print "Updating old charting data file names...";
-    my @products = Bugzilla::Product->get_all();
+    my @products = @{ $dbh->selectall_arrayref('SELECT id, name FROM products
+                                                ORDER BY name', {Slice=>{}}) };
     push(@products, $product_all);
     foreach my $product (@products) {
-        if (-e File::Spec->catfile($miningdir, $product->id)) {
+        if (-e File::Spec->catfile($miningdir, $product->{id})) {
             push(@conversion_errors,
                  { product => $product,
-                   message => 'A file named "' . $product->id .
+                   message => 'A file named "' . $product->{id} .
                               '" already exists.' });
         }
     }
@@ -846,8 +803,8 @@ sub _update_old_mining_filenames {
     if (! @conversion_errors) {
         # Renaming mining files should work now without a hitch.
         foreach my $product (@products) {
-            if (! rename(File::Spec->catfile($miningdir, $product->name),
-                         File::Spec->catfile($miningdir, $product->id))) {
+            if (! rename(File::Spec->catfile($miningdir, $product->{name}),
+                         File::Spec->catfile($miningdir, $product->{id}))) {
                 push(@conversion_errors,
                      { product => $product,
                        message => $! });
@@ -863,7 +820,7 @@ sub _update_old_mining_filenames {
         print " FAILED:\n";
         foreach my $error (@conversion_errors) {
             printf "Cannot rename charting data file for product %d (%s): %s\n",
-                   $error->{product}->id, $error->{product}->name,
+                   $error->{product}->{id}, $error->{product}->{name},
                    $error->{message};
         }
         print "You need to empty the \"$miningdir\" directory, then run\n",
diff --git a/Bugzilla/Install/Requirements.pm b/Bugzilla/Install/Requirements.pm
index f723543b8..8d7ccfbc7 100644
--- a/Bugzilla/Install/Requirements.pm
+++ b/Bugzilla/Install/Requirements.pm
@@ -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 = (
@@ -121,10 +121,12 @@ sub REQUIRED_MODULES {
         version => ($^V >= v5.13.3) ? '1.614' : '1.54'
     },
     # 2.24 contains several useful text virtual methods.
+    # 2.28-3.007 are broken, see https://bugzilla.mozilla.org/show_bug.cgi?id=1560873
     {
         package => 'Template-Toolkit',
         module  => 'Template',
-        version => '2.24'
+        version => '2.24',
+        blacklist => ['^2.2[89]$', '^3.00[0-7]$']
     },
     # 1.300011 has a debug mode for SMTP and automatically pass -i to sendmail.
     {
@@ -138,6 +140,11 @@ sub REQUIRED_MODULES {
         # This fixes a memory leak in walk_parts that affected jobqueue.pl.
         version => '1.904'
     },
+    {
+        package => 'Email-Address',
+        module  => 'Email::Address',
+        version => 0,
+    },
     {
         package => 'URI',
         module  => 'URI',
@@ -155,11 +162,6 @@ sub REQUIRED_MODULES {
         module  => 'Math::Random::ISAAC',
         version => '1.0.1',
     },
-    {
-        package => 'File-Slurp',
-        module  => 'File::Slurp',
-        version => '9999.13',
-    },
     {
         package => 'JSON-XS',
         module  => 'JSON::XS',
@@ -291,7 +293,7 @@ sub OPTIONAL_MODULES {
         # throwing warnings with Perl 5.12.
         version => '0.712',
         # SOAP::Transport::HTTP 1.12 is bogus.
-        blacklist => ['^1\.12$'],
+        blocklist => ['^1\.12$'],
         feature => ['xmlrpc'],
     },
     # Since SOAP::Lite 1.0, XMLRPC::Lite is no longer included
@@ -415,7 +417,13 @@ sub OPTIONAL_MODULES {
         module  => 'File::Copy::Recursive',
         version => 0,
         feature => ['documentation'],
-    }
+    },
+    {
+        package => 'File-Which',
+        module  => 'File::Which',
+        version => 0,
+        feature => ['documentation'],
+    },
     );
 
     my $extra_modules = _get_extension_requirements('OPTIONAL_MODULES');
@@ -728,16 +736,16 @@ sub have_vers {
 
     # 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) {
         _checking_for({ 
             package => $package, ok => $vok, wanted => $wanted,
-            found   => $vnum, blacklisted => $blacklisted
+            found   => $vnum, blocklisted => $blocklisted
         });
     }
     
@@ -746,8 +754,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') : '';
 
@@ -774,10 +782,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/JobQueue.pm b/Bugzilla/JobQueue.pm
index d5ceda8e9..6ff85d84f 100644
--- a/Bugzilla/JobQueue.pm
+++ b/Bugzilla/JobQueue.pm
@@ -14,8 +14,8 @@ use warnings;
 use Bugzilla::Constants;
 use Bugzilla::Error;
 use Bugzilla::Install::Util qw(install_string);
+use Bugzilla::Util qw(read_text);
 use File::Basename;
-use File::Slurp;
 use base qw(TheSchwartz);
 use fields qw(_worker_pidfile);
 
@@ -124,7 +124,7 @@ sub subprocess_worker {
             # And poll the PID to detect when the working has finished.
             # We do this instead of system() to allow for the INT signal to
             # interrup us and trigger kill_worker().
-            my $pid = read_file($self->{_worker_pidfile}, err_mode => 'quiet');
+            my $pid = read_text($self->{_worker_pidfile}, err_mode => 'quiet');
             if ($pid) {
                 sleep(3) while(kill(0, $pid));
             }
@@ -139,7 +139,7 @@ sub subprocess_worker {
 sub kill_worker {
     my $self = Bugzilla->job_queue();
     if ($self->{_worker_pidfile} && -e $self->{_worker_pidfile}) {
-        my $worker_pid = read_file($self->{_worker_pidfile});
+        my $worker_pid = read_text($self->{_worker_pidfile});
         if ($worker_pid && kill(0, $worker_pid)) {
             $self->debug("Stopping worker process");
             system "$0 -f -p '" . $self->{_worker_pidfile} . "' stop";
diff --git a/Bugzilla/MIME.pm b/Bugzilla/MIME.pm
new file mode 100644
index 000000000..d6046198f
--- /dev/null
+++ b/Bugzilla/MIME.pm
@@ -0,0 +1,121 @@
+# 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::MIME;
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use parent qw(Email::MIME);
+
+sub new {
+    my ($class, $msg, $args) = @_;
+    state $use_utf8 = Bugzilla->params->{'utf8'};
+
+    # Template-Toolkit trims trailing newlines, which is problematic when
+    # parsing headers.
+    $msg =~ s/\n*$/\n/;
+
+    # Because the encoding headers are not present in our email templates, we
+    # need to treat them as binary UTF-8 when parsing.
+    my ($in_header, $has_type, $has_encoding, $has_body) = (1);
+    foreach my $line (split(/\n/, $msg)) {
+        if ($line eq '') {
+            $in_header = 0;
+            next;
+        }
+        if (!$in_header) {
+            $has_body = 1;
+            last;
+        }
+        $has_type = 1 if $line =~ /^Content-Type:/i;
+        $has_encoding = 1 if $line =~ /^Content-Transfer-Encoding:/i;
+    }
+    if ($has_body) {
+        if (!$has_type && $use_utf8) {
+            $msg = qq#Content-Type: text/plain; charset="UTF-8"\n# . $msg;
+        }
+        if (!$has_encoding) {
+            $msg = qq#Content-Transfer-Encoding: binary\n# . $msg;
+        }
+    }
+    if ($use_utf8 && utf8::is_utf8($msg)) {
+        utf8::encode($msg);
+    }
+
+    # RFC 2822 requires us to have CRLF for our line endings and
+    # Email::MIME doesn't do this for us. We use \015 (CR) and \012 (LF)
+    # directly because Perl translates "\n" depending on what platform
+    # you're running on. See http://perldoc.perl.org/perlport.html#Newlines
+    $msg =~ s/(?:\015+)?\012/\015\012/msg;
+
+    return $class->SUPER::new($msg, $args);
+}
+
+sub as_string {
+    my $self = shift;
+    state $use_utf8 = Bugzilla->params->{'utf8'};
+
+    # We add this header to uniquely identify all email that we
+    # send as coming from this Bugzilla installation.
+    #
+    # We don't use correct_urlbase, because we want this URL to
+    # *always* be the same for this Bugzilla, in every email,
+    # even if the admin changes the "ssl_redirect" parameter some day.
+    $self->header_set('X-Bugzilla-URL', Bugzilla->params->{'urlbase'});
+
+    # We add this header to mark the mail as "auto-generated" and
+    # thus to hopefully avoid auto replies.
+    $self->header_set('Auto-Submitted', 'auto-generated');
+
+    # MIME-Version must be set otherwise some mailsystems ignore the charset
+    $self->header_set('MIME-Version', '1.0') if !$self->header('MIME-Version');
+
+    # Encode the headers correctly.
+    foreach my $header ($self->header_names) {
+        my @values = $self->header($header);
+        map { utf8::decode($_) if defined($_) && !utf8::is_utf8($_) } @values;
+
+        $self->header_str_set($header, @values);
+    }
+
+    # Ensure the character-set and encoding is set correctly on single part
+    # emails.  Multipart emails should have these already set when the parts
+    # are assembled.
+    if (scalar($self->parts) == 1) {
+        $self->charset_set('UTF-8') if $use_utf8;
+        $self->encoding_set('quoted-printable');
+    }
+
+    # Ensure we always return the encoded string
+    my $value = $self->SUPER::as_string();
+    if ($use_utf8 && utf8::is_utf8($value)) {
+        utf8::encode($value);
+    }
+
+    return $value;
+}
+
+1;
+
+__END__
+
+=head1 NAME
+
+Bugzilla::MIME - Wrapper around Email::MIME for unifying MIME related
+workarounds.
+
+=head1 SYNOPSIS
+
+  use Bugzilla::MIME;
+  my $email = Bugzilla::MIME->new($message, $args);
+
+=head1 DESCRIPTION
+
+Bugzilla::MIME subclasses Email::MIME and performs various fixes when parsing
+and generating email.
diff --git a/Bugzilla/Mailer.pm b/Bugzilla/Mailer.pm
index 0b82ded41..d7c23f857 100644
--- a/Bugzilla/Mailer.pm
+++ b/Bugzilla/Mailer.pm
@@ -12,22 +12,85 @@ use strict;
 use warnings;
 
 use parent qw(Exporter);
-@Bugzilla::Mailer::EXPORT = qw(MessageToMTA build_thread_marker);
+@Bugzilla::Mailer::EXPORT = qw(MessageToMTA build_thread_marker generate_email);
 
 use Bugzilla::Constants;
 use Bugzilla::Error;
 use Bugzilla::Hook;
+use Bugzilla::MIME;
 use Bugzilla::Util;
+use Bugzilla::User;
 
+use Encode qw();
 use Date::Format qw(time2str);
 
-use Encode qw(encode);
-use Encode::MIME::Header;
-use Email::MIME;
 use Email::Sender::Simple qw(sendmail);
 use Email::Sender::Transport::SMTP::Persistent;
 use Bugzilla::Sender::Transport::Sendmail;
 
+sub generate_email {
+    my ($vars, $templates) = @_;
+    my ($lang, $email_format, $msg_text, $msg_html, $msg_header);
+    state $use_utf8 = Bugzilla->params->{'utf8'};
+
+    if ($vars->{to_user}) {
+        $lang = $vars->{to_user}->setting('lang');
+        $email_format = $vars->{to_user}->setting('email_format');
+    } else {
+        # If there are users in the CC list who don't have an account,
+        # use the default language for email notifications.
+        $lang = Bugzilla::User->new()->setting('lang');
+        # However we cannot fall back to the default email_format, since
+        # it may be HTML, and many of the includes used in the HTML
+        # template require a valid user object. Instead we fall back to
+        # the plaintext template.
+        $email_format = 'text_only';
+    }
+
+    my $template = Bugzilla->template_inner($lang);
+
+    $template->process($templates->{header}, $vars, \$msg_header)
+        || ThrowTemplateError($template->error());
+    $template->process($templates->{text}, $vars, \$msg_text)
+        || ThrowTemplateError($template->error());
+
+    my @parts = (
+        Bugzilla::MIME->create(
+            attributes => {
+                content_type => 'text/plain',
+                charset      => $use_utf8 ? 'UTF-8' : 'iso-8859-1',
+                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)
+            || ThrowTemplateError($template->error());
+        push @parts, Bugzilla::MIME->create(
+            attributes => {
+                content_type => 'text/html',
+                charset      => $use_utf8 ? 'UTF-8' : 'iso-8859-1',
+                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 {
+        $email->content_type_set('multipart/alternative');
+        # Some mail clients need same encoding for each part, even empty ones.
+        $email->charset_set('UTF-8') if $use_utf8;
+    }
+    $email->parts_set(\@parts);
+    return $email;
+}
+
 sub MessageToMTA {
     my ($msg, $send_now) = (@_);
     my $method = Bugzilla->params->{'mail_delivery_method'};
@@ -41,62 +104,23 @@ sub MessageToMTA {
         return;
     }
 
-    my $email;
-    if (ref $msg) {
-        $email = $msg;
-    }
-    else {
-        # RFC 2822 requires us to have CRLF for our line endings and
-        # Email::MIME doesn't do this for us. We use \015 (CR) and \012 (LF)
-        # directly because Perl translates "\n" depending on what platform
-        # you're running on. See http://perldoc.perl.org/perlport.html#Newlines
-        $msg =~ s/(?:\015+)?\012/\015\012/msg;
-        $email = new Email::MIME($msg);
-    }
+    my $dbh = Bugzilla->dbh;
+
+    my $email = ref($msg) ? $msg : Bugzilla::MIME->new($msg);
 
     # If we're called from within a transaction, we don't want to send the
     # email immediately, in case the transaction is rolled back. Instead we
     # insert it into the mail_staging table, and bz_commit_transaction calls
     # send_staged_mail() after the transaction is committed.
-    if (! $send_now && Bugzilla->dbh->bz_in_transaction()) {
+    if (! $send_now && $dbh->bz_in_transaction()) {
         # The e-mail string may contain tainted values.
         my $string = $email->as_string;
         trick_taint($string);
-        Bugzilla->dbh->do("INSERT INTO mail_staging (message) VALUES(?)", undef, $string);
-        return;
-    }
 
-    # We add this header to uniquely identify all email that we
-    # send as coming from this Bugzilla installation.
-    #
-    # We don't use correct_urlbase, because we want this URL to
-    # *always* be the same for this Bugzilla, in every email,
-    # even if the admin changes the "ssl_redirect" parameter some day.
-    $email->header_set('X-Bugzilla-URL', Bugzilla->params->{'urlbase'});
-
-    # We add this header to mark the mail as "auto-generated" and
-    # thus to hopefully avoid auto replies.
-    $email->header_set('Auto-Submitted', 'auto-generated');
-
-    # MIME-Version must be set otherwise some mailsystems ignore the charset
-    $email->header_set('MIME-Version', '1.0') if !$email->header('MIME-Version');
-
-    # Encode the headers correctly in quoted-printable
-    foreach my $header ($email->header_names) {
-        my @values = $email->header($header);
-        # We don't recode headers that happen multiple times.
-        next if scalar(@values) > 1;
-        if (my $value = $values[0]) {
-            if (Bugzilla->params->{'utf8'} && !utf8::is_utf8($value)) {
-                utf8::decode($value);
-            }
-
-            # avoid excessive line wrapping done by Encode.
-            local $Encode::Encoding{'MIME-Q'}->{'bpl'} = 998;
-
-            my $encoded = encode('MIME-Q', $value);
-            $email->header_set($header, $encoded);
-        }
+        my $sth = $dbh->prepare("INSERT INTO mail_staging (message) VALUES (?)");
+        $sth->bind_param(1, $string, $dbh->BLOB_TYPE);
+        $sth->execute;
+        return;
     }
 
     my $from = $email->header('From');
@@ -143,29 +167,6 @@ sub MessageToMTA {
 
     return if $email->header('to') eq '';
 
-    $email->walk_parts(sub {
-        my ($part) = @_;
-        return if $part->parts > 1; # Top-level
-        my $content_type = $part->content_type || '';
-        $content_type =~ /charset=['"](.+)['"]/;
-        # If no charset is defined or is the default us-ascii,
-        # then we encode the email to UTF-8 if Bugzilla has utf8 enabled.
-        # XXX - This is a hack to workaround bug 723944.
-        if (!$1 || $1 eq 'us-ascii') {
-            my $body = $part->body;
-            if (Bugzilla->params->{'utf8'}) {
-                $part->charset_set('UTF-8');
-                # encoding_set works only with bytes, not with utf8 strings.
-                my $raw = $part->body_raw;
-                if (utf8::is_utf8($raw)) {
-                    utf8::encode($raw);
-                    $part->body_set($raw);
-                }
-            }
-            $part->encoding_set('quoted-printable') if !is_7bit_clean($body);
-        }
-    });
-
     if ($method eq "Test") {
         my $filename = bz_locations()->{'datadir'} . '/mailer.testfile';
         open TESTFILE, '>>', $filename;
@@ -214,17 +215,14 @@ sub build_thread_marker {
 
 sub send_staged_mail {
     my $dbh = Bugzilla->dbh;
-    my @ids;
-    my $emails
-        = $dbh->selectall_arrayref("SELECT id, message FROM mail_staging");
 
-    foreach my $row (@$emails) {
-        MessageToMTA($row->[1]);
-        push(@ids, $row->[0]);
-    }
+    my $emails = $dbh->selectall_arrayref('SELECT id, message FROM mail_staging');
+    my $sth = $dbh->prepare('DELETE FROM mail_staging WHERE id = ?');
 
-    if (@ids) {
-        $dbh->do("DELETE FROM mail_staging WHERE " . $dbh->sql_in('id', \@ids));
+    foreach my $email (@$emails) {
+        my ($id, $message) = @$email;
+        MessageToMTA($message);
+        $sth->execute($id);
     }
 }
 
@@ -240,6 +238,10 @@ Bugzilla::Mailer - Provides methods for sending email
 
 =over
 
+=item C<generate_email>
+
+Generates a multi-part email message, using the supplied list of templates.
+
 =item C<MessageToMTA>
 
 Sends the passed message to the mail transfer agent.
diff --git a/Bugzilla/Migrate.pm b/Bugzilla/Migrate.pm
index 0731d4fed..7865c842d 100644
--- a/Bugzilla/Migrate.pm
+++ b/Bugzilla/Migrate.pm
@@ -403,7 +403,7 @@ sub parse_date {
     }
     my $tz;
     if ($time[6]) {
-        $tz = Bugzilla->local_timezone->offset_as_string($time[6]);
+        $tz = DateTime::TimeZone->offset_as_string($time[6]);
     }
     else {
         $tz = $self->config('timezone');
diff --git a/Bugzilla/Object.pm b/Bugzilla/Object.pm
index 8f25e2b20..d43c8ca34 100644
--- a/Bugzilla/Object.pm
+++ b/Bugzilla/Object.pm
@@ -599,11 +599,29 @@ sub audit_log {
     foreach my $field (keys %$changes) {
         # Skip private changes.
         next if $field =~ /^_/;
-        my ($from, $to) = @{ $changes->{$field} };
+        my ($from, $to) = $self->_sanitize_audit_log($field, $changes->{$field});
         $sth->execute($user_id, $class, $self->id, $field, $from, $to);
     }
 }
 
+sub _sanitize_audit_log {
+    my ($self, $field, $changes) = @_;
+    my $class = ref($self) || $self;
+
+    # Do not store hashed passwords. Only record the algorithm used to encode them.
+    if ($class eq 'Bugzilla::User' && $field eq 'cryptpassword') {
+        foreach my $passwd (@$changes) {
+            next unless $passwd;
+            my $algorithm = 'unknown_algorithm';
+            if ($passwd =~ /{([^}]+)}$/) {
+                $algorithm = $1;
+            }
+            $passwd = "hashed_with_$algorithm";
+        }
+    }
+    return @$changes;
+}
+
 sub flatten_to_hash {
     my $self = shift;
     my $class = blessed($self);
diff --git a/Bugzilla/Product.pm b/Bugzilla/Product.pm
index 30ebc7c6c..0c0cb458d 100644
--- a/Bugzilla/Product.pm
+++ b/Bugzilla/Product.pm
@@ -22,7 +22,6 @@ use Bugzilla::Milestone;
 use Bugzilla::Field;
 use Bugzilla::Status;
 use Bugzilla::Install::Requirements;
-use Bugzilla::Mailer;
 use Bugzilla::Series;
 use Bugzilla::Hook;
 use Bugzilla::FlagType;
diff --git a/Bugzilla/Search.pm b/Bugzilla/Search.pm
index 8097d5fb8..646f949f5 100644
--- a/Bugzilla/Search.pm
+++ b/Bugzilla/Search.pm
@@ -1628,7 +1628,8 @@ sub _special_parse_email {
         my $email = trim($params->{"email$id"});
         next if !$email;
         my $type = $params->{"emailtype$id"} || 'anyexact';
-        $type = "anyexact" if $type eq "exact";
+        # for backward compatibility
+        $type = "equals" if $type eq "exact";
 
         my $or_clause = new Bugzilla::Search::Clause('OR');
         foreach my $field (qw(assigned_to reporter cc qa_contact)) {
@@ -2126,9 +2127,7 @@ sub _substring_terms {
     # split each term on spaces and commas anyway.
     my @words = split(/[\s,]+/, $args->{value});
     @words = grep { defined $_ and $_ ne '' } @words;
-    @words = map { $dbh->quote($_) } @words;
-    my @terms = map { $dbh->sql_iposition($_, $args->{full_field}) . " > 0" }
-                    @words;
+    my @terms = map { $dbh->sql_ilike($_, $args->{full_field}) } @words;
     return @terms;
 }
 
@@ -3154,28 +3153,26 @@ sub _simple_operator {
 
 sub _casesubstring {
     my ($self, $args) = @_;
-    my ($full_field, $quoted) = @$args{qw(full_field quoted)};
+    my ($full_field, $value) = @$args{qw(full_field value)};
     my $dbh = Bugzilla->dbh;
-    
-    $args->{term} = $dbh->sql_position($quoted, $full_field) . " > 0";
+
+    $args->{term} = $dbh->sql_like($value, $full_field);
 }
 
 sub _substring {
     my ($self, $args) = @_;
-    my ($full_field, $quoted) = @$args{qw(full_field quoted)};
+    my ($full_field, $value) = @$args{qw(full_field value)};
     my $dbh = Bugzilla->dbh;
-    
-    # XXX This should probably be changed to just use LIKE
-    $args->{term} = $dbh->sql_iposition($quoted, $full_field) . " > 0";
+
+    $args->{term} = $dbh->sql_ilike($value, $full_field);
 }
 
 sub _notsubstring {
     my ($self, $args) = @_;
-    my ($full_field, $quoted) = @$args{qw(full_field quoted)};
+    my ($full_field, $value) = @$args{qw(full_field value)};
     my $dbh = Bugzilla->dbh;
-    
-    # XXX This should probably be changed to just use NOT LIKE
-    $args->{term} = $dbh->sql_iposition($quoted, $full_field) . " = 0";
+
+    $args->{term} = $dbh->sql_not_ilike($value, $full_field);
 }
 
 sub _regexp {
diff --git a/Bugzilla/Template.pm b/Bugzilla/Template.pm
index 6ac36f783..decffe1e8 100644
--- a/Bugzilla/Template.pm
+++ b/Bugzilla/Template.pm
@@ -32,7 +32,6 @@ use Digest::MD5 qw(md5_hex);
 use File::Basename qw(basename dirname);
 use File::Find;
 use File::Path qw(rmtree mkpath);
-use File::Slurp;
 use File::Spec;
 use IO::Dir;
 use List::MoreUtils qw(firstidx);
@@ -232,7 +231,7 @@ sub quoteUrls {
               ~<a href=\"mailto:$2\">$1$2</a>~igx;
 
     # attachment links
-    $text =~ s~\b(attachment$s*\#?$s*(\d+)(?:$s+\[details\])?)
+    $text =~ s~\b(attachment$s*\#?$s*([0-9]+)(?:$s+\[details\])?)
               ~($things[$count++] = get_attachment_link($2, $1, $user)) &&
                ("\x{FDD2}" . ($count-1) . "\x{FDD3}")
               ~egmxi;
@@ -245,9 +244,9 @@ sub quoteUrls {
     # Also, we can't use $bug_re?$comment_re? because that will match the
     # empty string
     my $bug_word = template_var('terms')->{bug};
-    my $bug_re = qr/\Q$bug_word\E$s*\#?$s*(\d+)/i;
+    my $bug_re = qr/\Q$bug_word\E$s*\#?$s*([0-9]+)/i;
     my $comment_word = template_var('terms')->{comment};
-    my $comment_re = qr/(?:\Q$comment_word\E|comment)$s*\#?$s*(\d+)/i;
+    my $comment_re = qr/(?:\Q$comment_word\E|comment)$s*\#?$s*([0-9]+)/i;
     $text =~ s~\b($bug_re(?:$s*,?$s*$comment_re)?|$comment_re)
               ~ # We have several choices. $1 here is the link, and $2-4 are set
                 # depending on which part matched
@@ -261,29 +260,29 @@ sub quoteUrls {
     my $bugs_word = template_var('terms')->{bugs};
 
     my $bugs_re = qr/\Q$bugs_word\E$s*\#?$s*
-                     \d+(?:$s*,$s*\#?$s*\d+)+/ix;
+                     [0-9]+(?:$s*,$s*\#?$s*[0-9]+)+/ix;
 
     $text =~ s{($bugs_re)}{
         my $match = $1;
-        $match =~ s/((?:#$s*)?(\d+))/get_bug_link($2, $1);/eg;
+        $match =~ s/((?:#$s*)?([0-9]+))/get_bug_link($2, $1);/eg;
         $match;
     }eg;
 
     my $comments_word = template_var('terms')->{comments};
 
     my $comments_re = qr/(?:comments|\Q$comments_word\E)$s*\#?$s*
-                         \d+(?:$s*,$s*\#?$s*\d+)+/ix;
+                         [0-9]+(?:$s*,$s*\#?$s*[0-9]+)+/ix;
 
     $text =~ s{($comments_re)}{
         my $match = $1;
-        $match =~ s|((?:#$s*)?(\d+))|<a href="$current_bugurl#c$2">$1</a>|g;
+        $match =~ s|((?:#$s*)?([0-9]+))|<a href="$current_bugurl#c$2">$1</a>|g;
         $match;
     }eg;
 
     # Old duplicate markers. These don't use $bug_word because they are old
     # and were never customizable.
     $text =~ s~(?<=^\*\*\*\ This\ bug\ has\ been\ marked\ as\ a\ duplicate\ of\ )
-               (\d+)
+               ([0-9]+)
                (?=\ \*\*\*\Z)
               ~get_bug_link($1, $1, { user => $user })
               ~egmx;
@@ -502,7 +501,7 @@ sub _concatenate_css {
         next unless -e "$cgi_path/$files{$source}";
         my $file = $skins_path . '/' . md5_hex($source) . '.css';
         if (!-e $file) {
-            my $content = read_file("$cgi_path/$files{$source}");
+            my $content = read_text("$cgi_path/$files{$source}");
 
             # minify
             $content =~ s{/\*.*?\*/}{}sg;   # comments
@@ -512,7 +511,7 @@ sub _concatenate_css {
             # rewrite urls
             $content =~ s{url\(([^\)]+)\)}{_css_url_rewrite($source, $1)}eig;
 
-            write_file($file, "/* $files{$source} */\n" . $content . "\n");
+            write_text($file, "/* $files{$source} */\n" . $content . "\n");
         }
         push @minified, $file;
     }
@@ -522,9 +521,9 @@ sub _concatenate_css {
     if (!-e $file) {
         my $content = '';
         foreach my $source (@minified) {
-            $content .= read_file($source);
+            $content .= read_text($source);
         }
-        write_file($file, $content);
+        write_text($file, $content);
     }
 
     $file =~ s/^\Q$cgi_path\E\///o;
@@ -563,7 +562,7 @@ sub _concatenate_js {
         next unless -e "$cgi_path/$files{$source}";
         my $file = $skins_path . '/' . md5_hex($source) . '.js';
         if (!-e $file) {
-            my $content = read_file("$cgi_path/$files{$source}");
+            my $content = read_text("$cgi_path/$files{$source}");
 
             # minimal minification
             $content =~ s#/\*.*?\*/##sg;    # block comments
@@ -572,7 +571,7 @@ sub _concatenate_js {
             $content =~ s#\n{2,}#\n#g;      # blank lines
             $content =~ s#(^\s+|\s+$)##g;   # whitespace at the start/end of file
 
-            write_file($file, ";/* $files{$source} */\n" . $content . "\n");
+            write_text($file, ";/* $files{$source} */\n" . $content . "\n");
         }
         push @minified, $file;
     }
@@ -582,9 +581,9 @@ sub _concatenate_js {
     if (!-e $file) {
         my $content = '';
         foreach my $source (@minified) {
-            $content .= read_file($source);
+            $content .= read_text($source);
         }
-        write_file($file, $content);
+        write_text($file, $content);
     }
 
     $file =~ s/^\Q$cgi_path\E\///o;
@@ -865,12 +864,16 @@ sub create {
             },
 
             # In CSV, quotes are doubled, and any value containing a quote or a
-            # comma is enclosed in quotes. If a field starts with an equals
-            # sign, it is proceed by a space.
+            # comma is enclosed in quotes.
+            # If a field starts with either "=", "+", "-" or "@", it is preceded
+            # by a space to prevent stupid formula execution from Excel & co.
             csv => sub
             {
                 my ($var) = @_;
-                $var = ' ' . $var if substr($var, 0, 1) eq '=';
+                $var = ' ' . $var if $var =~ /^[+=@-]/;
+                # backslash is not special to CSV, but it can be used to confuse some browsers...
+                # so we do not allow it to happen. We only do this for logged-in users.
+                $var =~ s/\\/\x{FF3C}/g if Bugzilla->user->id;
                 $var =~ s/\"/\"\"/g;
                 if ($var !~ /^-?(\d+\.)?\d*$/) {
                     $var = "\"$var\"";
@@ -1158,6 +1161,17 @@ sub create {
                 return \@optional;
             },
             'default_authorizer' => sub { return Bugzilla::Auth->new() },
+
+            'login_not_email' => sub {
+                my $params = Bugzilla->params;
+                my $cache = Bugzilla->request_cache;
+
+                return $cache->{login_not_email} //=
+                  ($params->{emailsuffix}
+                     || ($params->{user_verify_class} =~ /LDAP/ && $params->{LDAPmailattribute})
+                     || ($params->{user_verify_class} =~ /RADIUS/ && $params->{RADIUS_email_suffix}))
+                  ? 1 : 0;
+            },
         },
     };
     # Use a per-process provider to cache compiled templates in memory across
diff --git a/Bugzilla/Token.pm b/Bugzilla/Token.pm
index 84d86b8c6..28122e818 100644
--- a/Bugzilla/Token.pm
+++ b/Bugzilla/Token.pm
@@ -28,6 +28,8 @@ use parent qw(Exporter);
                               check_token_data delete_token
                               issue_hash_token check_hash_token);
 
+use constant SEND_NOW => 1;
+
 ################################################################################
 # Public Functions
 ################################################################################
@@ -84,43 +86,49 @@ sub issue_new_user_account_token {
     # who made the request, and so it is reasonable to send the email in the same
     # language used to view the "Create a New Account" page (we cannot use their
     # user prefs as the user has no account yet!).
-    MessageToMTA($message);
+    MessageToMTA($message, SEND_NOW);
 }
 
 sub IssueEmailChangeToken {
-    my ($user, $new_email) = @_;
-    my $email_suffix = Bugzilla->params->{'emailsuffix'};
-    my $old_email = $user->login;
-
-    my ($token, $token_ts) = _create_token($user->id, 'emailold', $old_email . ":" . $new_email);
+    my $new_email = shift;
+    my $user = Bugzilla->user;
 
-    my $newtoken = _create_token($user->id, 'emailnew', $old_email . ":" . $new_email);
+    my ($token, $token_ts) = _create_token($user->id, 'emailold', $user->login . ":$new_email");
+    my $newtoken = _create_token($user->id, 'emailnew', $user->login . ":$new_email");
 
     # Mail the user the token along with instructions for using it.
 
     my $template = Bugzilla->template_inner($user->setting('lang'));
     my $vars = {};
 
-    $vars->{'oldemailaddress'} = $old_email . $email_suffix;
-    $vars->{'newemailaddress'} = $new_email . $email_suffix;
+    $vars->{'newemailaddress'} = $new_email . Bugzilla->params->{'emailsuffix'};
     $vars->{'expiration_ts'} = ctime($token_ts + MAX_TOKEN_AGE * 86400);
-    $vars->{'token'} = $token;
-    $vars->{'emailaddress'} = $old_email . $email_suffix;
+
+    # First send an email to the new address. If this one doesn't exist,
+    # then the whole process must stop immediately. This means the email must
+    # be sent immediately and must not be stored in the queue.
+    $vars->{'token'} = $newtoken;
 
     my $message;
-    $template->process("account/email/change-old.txt.tmpl", $vars, \$message)
+    $template->process('account/email/change-new.txt.tmpl', $vars, \$message)
       || ThrowTemplateError($template->error());
 
-    MessageToMTA($message);
+    MessageToMTA($message, SEND_NOW);
 
-    $vars->{'token'} = $newtoken;
-    $vars->{'emailaddress'} = $new_email . $email_suffix;
+    # If we come here, then the new address exists. We now email the current
+    # address, but we don't want to stop the process if it no longer exists,
+    # to give a chance to the user to confirm the email address change.
+    $vars->{'token'} = $token;
 
-    $message = "";
-    $template->process("account/email/change-new.txt.tmpl", $vars, \$message)
+    $message = '';
+    $template->process('account/email/change-old.txt.tmpl', $vars, \$message)
       || ThrowTemplateError($template->error());
 
-    MessageToMTA($message);
+    eval { MessageToMTA($message, SEND_NOW); };
+
+    # Give the user a chance to cancel the process even if he never got
+    # the email above. The token is required.
+    return $token;
 }
 
 # Generates a random token, adds it to the tokens table, and sends it
@@ -507,17 +515,15 @@ Bugzilla::Token - Provides different routines to manage tokens.
  Returns:     Nothing. It throws an error if the same user made the same
               request in the last few minutes.
 
-=item C<sub IssueEmailChangeToken($user, $new_email)>
+=item C<sub IssueEmailChangeToken($new_email)>
 
  Description: Sends two distinct tokens per email to the old and new email
               addresses to confirm the email address change for the given
               user. These tokens remain valid for the next MAX_TOKEN_AGE days.
 
- Params:      $user      - User object of the user requesting a new
-                           email address.
-              $new_email - The new email address of the user.
+ Params:      $new_email - The new email address of the user.
 
- Returns:     Nothing.
+ Returns:     The token to cancel the request.
 
 =item C<IssuePasswordToken($user)>
 
diff --git a/Bugzilla/Update.pm b/Bugzilla/Update.pm
index 72a7108a8..1c092ebe4 100644
--- a/Bugzilla/Update.pm
+++ b/Bugzilla/Update.pm
@@ -49,14 +49,15 @@ 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);
     }
 
     # On which branch is the current installation running?
     my @current_version =
-        (BUGZILLA_VERSION =~ m/^(\d+)\.(\d+)(?:(rc|\.)(\d+))?\+?$/);
+        (BUGZILLA_VERSION =~ m/^(\d+)\.(\d+)(?:\.(\d+))?(?:(rc|\.)(\d+))?\+?$/);
 
     my @release;
     if (Bugzilla->params->{'upgrade_notification'} eq 'development_snapshot') {
@@ -68,17 +69,60 @@ 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.
+        # This is the 5.0.4 branch and it won't branch again so just hardcode this.
+        my $branch_version = '5.0.4';
+
+        # 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') {
         # 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];
+        # This is the 5.0.4 branch and it won't branch again so just hardcode this.
+        my $branch_version = '5.0.4';
 
         # 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') {
@@ -97,12 +141,12 @@ sub get_notifications {
     # Only notify the administrator if the latest version available
     # is newer than the current one.
     my @new_version =
-        ($release[0]->{'latest_ver'} =~ m/^(\d+)\.(\d+)(?:(rc|\.)(\d+))?\+?$/);
+        ($release[0]->{'latest_ver'} =~ m/^(\d+)\.(\d+)(?:\.(\d+))?(?:(rc|\.)(\d+))?\+?$/);
 
     # We convert release candidates 'rc' to integers (rc ? 0 : 1) in order
     # to compare versions easily.
-    $current_version[2] = ($current_version[2] && $current_version[2] eq 'rc') ? 0 : 1;
-    $new_version[2] = ($new_version[2] && $new_version[2] eq 'rc') ? 0 : 1;
+    @current_version = map { s/^(?:rc|)$/0/; s/^\.$/1/; $_; } @current_version;
+    @new_version = map { s/^(?:rc|)$/0/; s/^\.$/1/; $_; } @new_version;
 
     my $is_newer = _compare_versions(\@current_version, \@new_version);
     return ($is_newer == 1) ? {'data' => $release[0]} : undef;
diff --git a/Bugzilla/User.pm b/Bugzilla/User.pm
index e63be93dd..e8f4aa77f 100644
--- a/Bugzilla/User.pm
+++ b/Bugzilla/User.pm
@@ -132,7 +132,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 {
@@ -922,9 +934,10 @@ sub groups {
 }
 
 sub last_visited {
-    my ($self) = @_;
+    my ($self, $ids) = @_;
 
-    return Bugzilla::BugUserLastVisit->match({ user_id => $self->id });
+    return Bugzilla::BugUserLastVisit->match({ user_id => $self->id,
+                                               $ids ? ( bug_id => $ids ) : () });
 }
 
 sub is_involved_in_bug {
@@ -2403,6 +2416,9 @@ sub check_account_creation_enabled {
 
 sub check_and_send_account_creation_confirmation {
     my ($self, $login) = @_;
+    my $dbh = Bugzilla->dbh;
+
+    $dbh->bz_start_transaction;
 
     $login = $self->check_login_name($login);
     my $creation_regexp = Bugzilla->params->{'createemailregexp'};
@@ -2417,6 +2433,8 @@ sub check_and_send_account_creation_confirmation {
     # Create and send a token for this new account.
     require Bugzilla::Token;
     Bugzilla::Token::issue_new_user_account_token($login);
+
+    $dbh->bz_commit_transaction;
 }
 
 # This is used in a few performance-critical areas where we don't want to
diff --git a/Bugzilla/User/Setting.pm b/Bugzilla/User/Setting.pm
index ea3bbfb54..aece3b7de 100644
--- a/Bugzilla/User/Setting.pm
+++ b/Bugzilla/User/Setting.pm
@@ -127,7 +127,7 @@ sub add_setting {
     my $exists = _setting_exists($name);
     return if ($exists && !$force_check);
 
-    ($name && $default_value)
+    ($name && length( $default_value // '' ))
       ||  ThrowCodeError("setting_info_invalid");
 
     if ($exists) {
diff --git a/Bugzilla/UserAgent.pm b/Bugzilla/UserAgent.pm
index 6ceb9d3c5..14637038c 100644
--- a/Bugzilla/UserAgent.pm
+++ b/Bugzilla/UserAgent.pm
@@ -106,6 +106,7 @@ use constant OS_MAP => (
     qr/\(.*Android.*\)/ => ["Android"],
     # Windows
     qr/\(.*Windows XP.*\)/ => ["Windows XP"],
+    qr/\(.*Windows NT 10\.0.*\)/ => ["Windows 10"],
     qr/\(.*Windows NT 6\.4.*\)/ => ["Windows 10"],
     qr/\(.*Windows NT 6\.3.*\)/ => ["Windows 8.1"],
     qr/\(.*Windows NT 6\.2.*\)/ => ["Windows 8"],
diff --git a/Bugzilla/Util.pm b/Bugzilla/Util.pm
index 670f5f8f2..5ceef4ef7 100644
--- a/Bugzilla/Util.pm
+++ b/Bugzilla/Util.pm
@@ -24,7 +24,7 @@ use parent qw(Exporter);
                              validate_email_syntax check_email_syntax clean_text
                              get_text template_var display_value disable_utf8
                              detect_encoding email_filter
-                             join_activity_entries);
+                             join_activity_entries read_text write_text);
 
 use Bugzilla::Constants;
 use Bugzilla::RNG qw(irand);
@@ -39,6 +39,8 @@ use Scalar::Util qw(tainted blessed);
 use Text::Wrap;
 use Encode qw(encode decode resolve_alias);
 use Encode::Guess;
+use File::Basename qw(dirname);
+use File::Temp qw(tempfile);
 
 sub trick_taint {
     require Carp;
@@ -49,13 +51,13 @@ sub trick_taint {
 }
 
 sub detaint_natural {
-    my $match = $_[0] =~ /^(\d+)$/;
+    my $match = $_[0] =~ /^([0-9]+)$/;
     $_[0] = $match ? int($1) : undef;
     return (defined($_[0]));
 }
 
 sub detaint_signed {
-    my $match = $_[0] =~ /^([-+]?\d+)$/;
+    my $match = $_[0] =~ /^([-+]?[0-9]+)$/;
     # The "int()" call removes any leading plus sign.
     $_[0] = $match ? int($1) : undef;
     return (defined($_[0]));
@@ -106,6 +108,29 @@ sub html_quote {
     return $var;
 }
 
+sub read_text {
+    my ($filename) = @_;
+    open my $fh, '<:encoding(utf-8)', $filename;
+    local $/ = undef;
+    my $content = <$fh>;
+    close $fh;
+    return $content;
+}
+
+sub write_text {
+    my ($filename, $content) = @_;
+    my ($tmp_fh, $tmp_filename) = tempfile('.tmp.XXXXXXXXXX',
+        DIR    => dirname($filename),
+        UNLINK => 0,
+    );
+    binmode $tmp_fh, ':encoding(utf-8)';
+    print $tmp_fh $content;
+    close $tmp_fh;
+    # File::Temp tries for secure files, but File::Slurp used the umask.
+    chmod(0666 & ~umask, $tmp_filename);
+    rename $tmp_filename, $filename;
+}
+
 sub html_light_quote {
     my ($text) = @_;
     # admin/table.html.tmpl calls |FILTER html_light| many times.
@@ -588,7 +613,7 @@ sub datetime_from {
         second => defined($time[0]) ? int($time[0]) : undef,
         # If a timezone was specified, use it. Otherwise, use the
         # local timezone.
-        time_zone => Bugzilla->local_timezone->offset_as_string($time[6]) 
+        time_zone => DateTime::TimeZone->offset_as_string($time[6])
                      || Bugzilla->local_timezone,
     );
 
@@ -676,12 +701,18 @@ sub validate_email_syntax {
     # 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.
-    my $ret = ($addr =~ /$match/ && $email !~ /\P{ASCII}/ && $email =~ /^$addr_spec$/);
-    if ($ret) {
+    # We set the max length to 127 to ensure addresses aren't truncated when
+    # inserted into the tokens.eventdata field.
+    if ($addr =~ /$match/
+        && $email !~ /\P{ASCII}/
+        && $email =~ /^$addr_spec$/
+        && length($email) <= 127)
+    {
         # We assume these checks to suffice to consider the address untainted.
         trick_taint($_[0]);
+        return 1;
     }
-    return $ret ? 1 : 0;
+    return 0;
 }
 
 sub check_email_syntax {
@@ -1232,18 +1263,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/WebService/Bug.pm b/Bugzilla/WebService/Bug.pm
index 127ea40bb..b07d3cb01 100644
--- a/Bugzilla/WebService/Bug.pm
+++ b/Bugzilla/WebService/Bug.pm
@@ -1133,6 +1133,10 @@ sub update_comment_tags {
                           { function => 'Bug.update_comment_tags',
                             param    => 'comment_id' });
 
+    ThrowCodeError('param_integer_required', { function => 'Bug.update_comment_tags',
+                                               param => 'comment_id' })
+      unless $comment_id =~ /^[0-9]+$/;
+
     my $comment = Bugzilla::Comment->new($comment_id)
         || return [];
     $comment->bug->check_is_visible();
@@ -2639,7 +2643,7 @@ from the Bugzilla database to fetch. If it contains any non-numeric
 characters, it is considered to be a bug alias instead, and the data bug 
 with that alias will be loaded.
 
-item C<new_since>
+=item C<new_since>
 
 C<dateTime> If specified, the method will only return changes I<newer>
 than this time.
diff --git a/Bugzilla/WebService/BugUserLastVisit.pm b/Bugzilla/WebService/BugUserLastVisit.pm
index 19a56ff46..56e91ec31 100644
--- a/Bugzilla/WebService/BugUserLastVisit.pm
+++ b/Bugzilla/WebService/BugUserLastVisit.pm
@@ -36,7 +36,7 @@ sub update {
     # Cache permissions for bugs. This highly reduces the number of calls to the
     # DB.  visible_bugs() is only able to handle bug IDs, so we have to skip
     # aliases.
-    $user->visible_bugs([grep /^[0-9]$/, @$ids]);
+    $user->visible_bugs([grep /^[0-9]+$/, @$ids]);
 
     $dbh->bz_start_transaction();
     my @results;
@@ -52,7 +52,7 @@ sub update {
         push(
             @results,
             $self->_bug_user_last_visit_to_hash(
-                $bug, $last_visit_ts, $params
+                $bug->id, $last_visit_ts, $params
             ));
     }
     $dbh->bz_commit_transaction();
@@ -67,27 +67,23 @@ sub get {
 
     $user->login(LOGIN_REQUIRED);
 
+    my @last_visits;
     if ($ids) {
         # Cache permissions for bugs. This highly reduces the number of calls to
         # the DB.  visible_bugs() is only able to handle bug IDs, so we have to
         # skip aliases.
-        $user->visible_bugs([grep /^[0-9]$/, @$ids]);
-    }
-
-    my @last_visits = @{ $user->last_visited };
+        $user->visible_bugs([grep /^[0-9]+$/, @$ids]);
 
-    if ($ids) {
-        # remove bugs that we are not interested in if ids is passed in.
-        my %id_set = map { ($_ => 1) } @$ids;
-        @last_visits = grep { $id_set{ $_->bug_id } } @last_visits;
+        my %last_visit  = map { $_->bug_id => $_->last_visit_ts } @{ $user->last_visited($ids) };
+        @last_visits = map { $self->_bug_user_last_visit_to_hash($_->id, $last_visit{$_}, $params) } @$ids;
+    }
+    else {
+        @last_visits = map {
+            $self->_bug_user_last_visit_to_hash($_->bug_id, $_->last_visit_ts, $params)
+        } @{ $user->last_visited };
     }
 
-    return [
-        map {
-            $self->_bug_user_last_visit_to_hash($_->bug_id, $_->last_visit_ts,
-                $params)
-        } @last_visits
-    ];
+    return \@last_visits;
 }
 
 sub _bug_user_last_visit_to_hash {
diff --git a/Bugzilla/WebService/Constants.pm b/Bugzilla/WebService/Constants.pm
index 0bdd3517e..557a996f8 100644
--- a/Bugzilla/WebService/Constants.pm
+++ b/Bugzilla/WebService/Constants.pm
@@ -67,6 +67,8 @@ use constant WS_ERROR_CODE => {
     number_too_large            => 54,
     number_too_small            => 55,
     illegal_date                => 56,
+    param_integer_required      => 57,
+    param_scalar_array_required => 58,
     # Bug errors usually occupy the 100-200 range.
     improper_bug_id_field_value => 100,
     bug_id_does_not_exist       => 101,
diff --git a/Bugzilla/WebService/FlagType.pm b/Bugzilla/WebService/FlagType.pm
index 9723d4735..9d7cce037 100644
--- a/Bugzilla/WebService/FlagType.pm
+++ b/Bugzilla/WebService/FlagType.pm
@@ -61,11 +61,9 @@ sub get {
 
 sub create {
     my ($self, $params) = @_;
+    my $user = Bugzilla->login(LOGIN_REQUIRED);
 
-    my $dbh = Bugzilla->dbh;
-    my $user = Bugzilla->user;
-
-    Bugzilla->user->in_group('editcomponents')
+    $user->in_group('editcomponents')
         || scalar(@{$user->get_products_by_permission('editcomponents')})
         || ThrowUserError("auth_failure", { group => "editcomponents",
                                          action => "add",
@@ -121,11 +119,9 @@ sub create {
 
 sub update {
     my ($self, $params) = @_;
-
     my $dbh = Bugzilla->dbh;
-    my $user = Bugzilla->user;
+    my $user = Bugzilla->login(LOGIN_REQUIRED);
 
-    Bugzilla->login(LOGIN_REQUIRED);
     $user->in_group('editcomponents')
         || scalar(@{$user->get_products_by_permission('editcomponents')})
         || ThrowUserError("auth_failure", { group  => "editcomponents",
diff --git a/Bugzilla/WebService/Product.pm b/Bugzilla/WebService/Product.pm
index f38972bc1..94348a161 100644
--- a/Bugzilla/WebService/Product.pm
+++ b/Bugzilla/WebService/Product.pm
@@ -55,8 +55,6 @@ use constant FIELD_MAP => {
 # Add aliases here for method name compatibility #
 ##################################################
 
-BEGIN { *get_products = \&get }
-
 # Get the ids of the products the user can search
 sub get_selectable_products {
     Bugzilla->switch_to_shadow_db();
@@ -498,8 +496,6 @@ Returns a list of information about the products passed to it.
 
 B<Note>: You must at least specify one of C<ids> or C<names>.
 
-B<Note>: Can also be called as "get_products" for compatibilty with Bugzilla 3.0 API.
-
 =item B<REST>
 
 To return information about a specific groups of products such as
@@ -992,11 +988,3 @@ You must define a default milestone.
 =back
 
 =back
-
-=head1 B<Methods in need of POD>
-
-=over
-
-=item get_products
-
-=back
diff --git a/Bugzilla/WebService/Server/XMLRPC.pm b/Bugzilla/WebService/Server/XMLRPC.pm
index 98a0ee405..8deb253ad 100644
--- a/Bugzilla/WebService/Server/XMLRPC.pm
+++ b/Bugzilla/WebService/Server/XMLRPC.pm
@@ -63,6 +63,12 @@ sub make_response {
     my $self = shift;
     my $cgi = Bugzilla->cgi;
 
+    # Fix various problems with IIS.
+    if ($ENV{'SERVER_SOFTWARE'} =~ /IIS/) {
+        $ENV{CONTENT_LENGTH} = 0;
+        binmode(STDOUT, ':bytes');
+    }
+
     $self->SUPER::make_response(@_);
 
     # XMLRPC::Transport::HTTP::CGI doesn't know about Bugzilla carrying around
@@ -107,6 +113,8 @@ sub handle_login {
     if (none { $_ eq $method } $class->PUBLIC_METHODS) {
         ThrowCodeError('unknown_method', { method => $full_method });
     }
+
+    $ENV{CONTENT_LENGTH} = 0 if $ENV{'SERVER_SOFTWARE'} =~ /IIS/;
     $self->SUPER::handle_login($class, $method, $full_method);
     return;
 }
diff --git a/Bugzilla/WebService/Util.pm b/Bugzilla/WebService/Util.pm
index a0a51a8de..a879c0e0d 100644
--- a/Bugzilla/WebService/Util.pm
+++ b/Bugzilla/WebService/Util.pm
@@ -16,6 +16,7 @@ use Bugzilla::FlagType;
 use Bugzilla::Error;
 
 use Storable qw(dclone);
+use List::MoreUtils qw(any none);
 
 use parent qw(Exporter);
 
@@ -219,15 +220,20 @@ sub validate  {
     # sent any parameters at all, and we're getting @keys where
     # $params should be.
     return ($self, undef) if (defined $params and !ref $params);
-    
+
+    my @id_params = qw(ids comment_ids);
     # If @keys is not empty then we convert any named 
     # parameters that have scalar values to arrayrefs
     # that match.
     foreach my $key (@keys) {
         if (exists $params->{$key}) {
-            $params->{$key} = ref $params->{$key} 
-                              ? $params->{$key} 
-                              : [ $params->{$key} ];
+            $params->{$key} = [ $params->{$key} ] unless ref $params->{$key};
+
+            if (any { $key eq $_ } @id_params) {
+                my $ids = $params->{$key};
+                ThrowCodeError('param_scalar_array_required', { param => $key })
+                  unless ref($ids) eq 'ARRAY' && none { ref $_ } @$ids;
+            }
         }
     }
 
diff --git a/Build.PL b/Build.PL
index 024a56024..0a7c1059a 100644
--- a/Build.PL
+++ b/Build.PL
@@ -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/README b/README
index e68afd8e4..335631e5d 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
 =============
 
diff --git a/attachment.cgi b/attachment.cgi
index 5db8f5909..4cd9229fb 100755
--- a/attachment.cgi
+++ b/attachment.cgi
@@ -26,6 +26,7 @@ use Bugzilla::Attachment::PatchReader;
 use Bugzilla::Token;
 
 use Encode qw(encode find_encoding);
+use Encode::MIME::Header; # Required to alter Encode::Encoding{'MIME-Q'}.
 
 # For most scripts we don't make $cgi and $template global variables. But
 # when preparing Bugzilla for mod_perl, this script used these
@@ -34,6 +35,7 @@ use Encode qw(encode find_encoding);
 local our $cgi = Bugzilla->cgi;
 local our $template = Bugzilla->template;
 local our $vars = {};
+local $Bugzilla::CGI::ALLOW_UNSAFE_RESPONSE = 1;
 
 # All calls to this script should contain an "action" variable whose
 # value determines what the user wants to do.  The code below checks
diff --git a/chart.cgi b/chart.cgi
index 00b0b8ee5..18ab87e5e 100755
--- a/chart.cgi
+++ b/chart.cgi
@@ -96,6 +96,13 @@ $user->in_group(Bugzilla->params->{"chartgroup"})
 # Only admins may create public queries
 $user->in_group('admin') || $cgi->delete('public');
 
+if ($cgi->param('debug')
+    && Bugzilla->params->{debug_group}
+    && Bugzilla->user->in_group(Bugzilla->params->{debug_group})
+    ) {
+    $vars->{'debug'} = 1;
+}
+
 # All these actions relate to chart construction.
 if ($action =~ /^(assemble|add|remove|sum|subscribe|unsubscribe)$/) {
     # These two need to be done before the creation of the Chart object, so
@@ -305,11 +312,19 @@ sub plot {
     $vars->{'chart'} = new Bugzilla::Chart($cgi);
 
     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
-    if ($cgi->param('debug')) {
-        print $cgi->header();
-        $vars->{'chart'}->dump();
+    if (exists $vars->{'debug'}) {
+        # Bug 1439260 - if we're using debug mode, always use the HTML template
+        # which has proper filters in it. Debug forces an HTML content type
+        # anyway, and can cause XSS if we're not filtering the output.
+        $format = $template->get_format("reports/chart", "", "html");
+        $vars->{'debug_dump'} = $vars->{'chart'}->dump();
     }
 
     print $cgi->header($format->{'ctype'});
@@ -351,7 +366,9 @@ sub view {
 
     # If we have having problems with bad data, we can set debug=1 to dump
     # the data structure.
-    $chart->dump() if $cgi->param('debug');
+    if (exists $vars->{'debug'}) {
+        $vars->{'debug_dump'} = $chart->dump();
+    }
 
     $template->process("reports/create-chart.html.tmpl", $vars)
       || ThrowTemplateError($template->error());
diff --git a/checksetup.pl b/checksetup.pl
index cf124b8e2..5dda0df6f 100755
--- a/checksetup.pl
+++ b/checksetup.pl
@@ -458,8 +458,6 @@ The format of that file is as follows:
  $answer{'ADMIN_PASSWORD'} = 'fooey';
  $answer{'ADMIN_REALNAME'} = 'Joel Peshkin';
 
- $answer{'SMTP_SERVER'} = 'mail.mydomain.net';
-
  $answer{'NO_PAUSE'} = 1
 
 C<NO_PAUSE> means "never stop and prompt the user to hit Enter to continue,
diff --git a/contrib/bugzilla-submit/README b/contrib/bugzilla-submit/README
index f9e74b9d4..9d9811bde 100644
--- a/contrib/bugzilla-submit/README
+++ b/contrib/bugzilla-submit/README
@@ -19,9 +19,8 @@ Its only requirement is Python 2.3 or higher; you should have the
 Usage Notes
 -----------
 
-* Please constrain testing to your own installation of Bugzilla, or use
-* http://landfill.bugzilla.org/ for testing purposes -- opening test
-* bugs on production instances of Bugzilla is definitely not a good idea
+* Please constrain testing to your own installation of Bugzilla, opening test
+* bugs on production instances of Bugzilla is definitely not a good idea.
 
 Run "bugzilla-submit --help" for a description of the possible options.
 
diff --git a/contrib/bugzilla-submit/bugzilla-submit.xml b/contrib/bugzilla-submit/bugzilla-submit.xml
index 2cfdc9209..297e690cc 100755
--- a/contrib/bugzilla-submit/bugzilla-submit.xml
+++ b/contrib/bugzilla-submit/bugzilla-submit.xml
@@ -201,12 +201,12 @@ password field is the right password.  The URL in the machine field
 must be enclosed in double quotes.</para>
 
 <para>For example, if your Bugzilla instance is at
-"http://landfill.bugzilla.org/bztest/", and your login and password
+"https://bugzilla.example.org/bztest/", and your login and password
 there are "john@doe.com" and "foo", respectively, your
 <filename>.netrc</filename> entry should look something like:</para>
 
 <screen>
-    machine "http://landfill.bugzilla.org/bztest/"
+    machine "https://bugzilla.example.org/bztest/"
     login john@doe.com
     password foo
 
diff --git a/contrib/jb2bz.py b/contrib/jb2bz.py
index 85f95423a..170e82d70 100755
--- a/contrib/jb2bz.py
+++ b/contrib/jb2bz.py
@@ -17,8 +17,8 @@ This code requires a recent version of Andy Dustman's MySQLdb interface,
 Share and enjoy.
 """
 
-import rfc822, mimetools, multifile, mimetypes, email.utils
-import sys, re, glob, StringIO, os, stat, time
+import email, mimetypes, email.utils
+import sys, re, glob, os, stat, time
 import MySQLdb, getopt
 
 # mimetypes doesn't include everything we might encounter, yet.
@@ -30,7 +30,7 @@ if not mimetypes.encodings_map.has_key('.bz2'):
 
 bug_status='CONFIRMED'
 component="default"
-version=""
+version="unspecified"
 product="" # this is required, the rest of these are defaulted as above
 
 """
@@ -89,10 +89,24 @@ def process_notes_file(current, fname):
 def process_reply_file(current, fname):
     new_note = {}
     reply = open(fname, "r")
-    msg = rfc822.Message(reply)
-    new_note['text'] = "%s\n%s" % (msg['From'], msg.fp.read())
-    new_note['timestamp'] = email.utils.parsedate_tz(msg['Date'])
-    current["notes"].append(new_note)
+    msg = email.message_from_file(reply)
+
+    # Add any attachments that may have been in a followup or reply
+    msgtype = msg.get_content_maintype()
+    if msgtype == "multipart":
+        for part in msg.walk():
+            new_note = {}
+            if part.get_filename() is None:
+                if part.get_content_type() == "text/plain":
+                    new_note['timestamp'] = time.gmtime(email.utils.mktime_tz(email.utils.parsedate_tz(msg['Date'])))
+                    new_note['text'] = "%s\n%s" % (msg['From'], part.get_payload())
+                    current["notes"].append(new_note)
+            else:
+                maybe_add_attachment(part, current)
+    else:
+        new_note['text'] = "%s\n%s" % (msg['From'], msg.get_payload())
+        new_note['timestamp'] = time.gmtime(email.utils.mktime_tz(email.utils.parsedate_tz(msg['Date'])))
+        current["notes"].append(new_note)
 
 def add_notes(current):
     """Add any notes that have been recorded for the current bug."""
@@ -104,51 +118,48 @@ def add_notes(current):
     for f in glob.glob("%d.followup.*" % current['number']):
         process_reply_file(current, f)
 
-def maybe_add_attachment(current, file, submsg):
+def maybe_add_attachment(submsg, current):
     """Adds the attachment to the current record"""
-    cd = submsg["Content-Disposition"]
-    m = re.search(r'filename="([^"]+)"', cd)
-    if m == None:
+    attachment_filename = submsg.get_filename()
+    if attachment_filename is None:
         return
-    attachment_filename = m.group(1)
-    if (submsg.gettype() == 'application/octet-stream'):
+
+    if (submsg.get_content_type() == 'application/octet-stream'):
         # try get a more specific content-type for this attachment
-        type, encoding = mimetypes.guess_type(m.group(1))
-        if type == None:
-            type = submsg.gettype()
+        mtype, encoding = mimetypes.guess_type(attachment_filename)
+        if mtype == None:
+            mtype = submsg.get_content_type()
     else:
-        type = submsg.gettype()
+        mtype = submsg.get_content_type()
 
-    try:
-        data = StringIO.StringIO()
-        mimetools.decode(file, data, submsg.getencoding())
-    except:
+    if mtype == 'application/x-pkcs7-signature':
+        return
+
+    if mtype == 'application/pkcs7-signature':
+         return
+
+    if mtype == 'application/pgp-signature':
         return
 
-    current['attachments'].append( ( attachment_filename, type, data.getvalue() ) )
+    if mtype == 'message/rfc822':
+        return
 
-def process_mime_body(current, file, submsg):
-    data = StringIO.StringIO()
     try:
-        mimetools.decode(file, data, submsg.getencoding())
-        current['description'] = data.getvalue()
+        data = submsg.get_payload(decode=True)
     except:
         return
 
+    current['attachments'].append( ( attachment_filename, mtype, data ) )
+
 def process_text_plain(msg, current):
-    current['description'] = msg.fp.read()
-
-def process_multi_part(file, msg, current):
-    mf = multifile.MultiFile(file)
-    mf.push(msg.getparam("boundary"))
-    while mf.next():
-        submsg = mimetools.Message(file)
-        if submsg.has_key("Content-Disposition"):
-            maybe_add_attachment(current, mf, submsg)
+    current['description'] = msg.get_payload()
+
+def process_multi_part(msg, current):
+    for part in msg.walk():
+        if part.get_filename() is None:
+            process_text_plain(part, current)
         else:
-            # This is the message body itself (always?), so process
-            # accordingly
-            process_mime_body(current, mf, submsg)
+            maybe_add_attachment(part, current)
 
 def process_jitterbug(filename):
     current = {}
@@ -158,39 +169,37 @@ def process_jitterbug(filename):
     current['description'] = ''
     current['date-reported'] = ()
     current['short-description'] = ''
-    
-    print "Processing: %d" % current['number']
 
-    file = open(filename, "r")
-    create_date = os.fstat(file.fileno())
-    msg = mimetools.Message(file)
+    print "Processing: %d" % current['number']
 
-    msgtype = msg.gettype()
+    mfile = open(filename, "r")
+    create_date = os.fstat(mfile.fileno())
+    msg = email.message_from_file(mfile)
 
-    add_notes(current)
-    current['date-reported'] = email.utils.parsedate_tz(msg['Date'])
+    current['date-reported'] = time.gmtime(email.utils.mktime_tz(email.utils.parsedate_tz(msg['Date'])))
     if current['date-reported'] is None:
        current['date-reported'] = time.gmtime(create_date[stat.ST_MTIME])
 
     if current['date-reported'][0] < 1900:
        current['date-reported'] = time.gmtime(create_date[stat.ST_MTIME])
 
-    if msg.getparam('Subject') is not None: 
+    if msg.has_key('Subject') is not False:
         current['short-description'] = msg['Subject']
     else:
         current['short-description'] = "Unknown"
 
-    if msgtype[:5] == 'text/':
+    msgtype = msg.get_content_maintype()
+    if msgtype == 'text':
         process_text_plain(msg, current)
-    elif msgtype[:5] == 'text':
-        process_text_plain(msg, current)
-    elif msgtype[:10] == "multipart/":
-        process_multi_part(file, msg, current)
+    elif msgtype == "multipart":
+        process_multi_part(msg, current)
     else:
         # Huh? This should never happen.
         print "Unknown content-type: %s" % msgtype
         sys.exit(1)
 
+    add_notes(current)
+
     # At this point we have processed the message: we have all of the notes and
     # attachments stored, so it's time to add things to the database.
     # The schema for JitterBug 2.14 can be found at:
@@ -219,7 +228,9 @@ def process_jitterbug(filename):
     try:
         cursor.execute( "INSERT INTO bugs SET " \
                         "bug_id=%s," \
+                        "priority='---'," \
                         "bug_severity='normal',"  \
+                        "op_sys='All'," \
                         "bug_status=%s," \
                         "creation_ts=%s,"  \
                         "delta_ts=%s,"  \
@@ -242,7 +253,7 @@ def process_jitterbug(filename):
                           version,
                           component,
                           resolution] )
-    
+
         # This is the initial long description associated with the bug report
         cursor.execute( "INSERT INTO longdescs SET " \
                         "bug_id=%s," \
@@ -253,7 +264,7 @@ def process_jitterbug(filename):
                           reporter,
                           time.strftime("%Y-%m-%d %H:%M:%S", current['date-reported'][:9]),
                           current['description'] ] )
-    
+
         # Add whatever notes are associated with this defect
         for n in current['notes']:
                             cursor.execute( "INSERT INTO longdescs SET " \
@@ -265,15 +276,15 @@ def process_jitterbug(filename):
                              reporter,
                              time.strftime("%Y-%m-%d %H:%M:%S", n['timestamp'][:9]),
                              n['text']])
-    
+
         # add attachments associated with this defect
         for a in current['attachments']:
             cursor.execute( "INSERT INTO attachments SET " \
-                            "bug_id=%s, creation_ts=%s, description='', mimetype=%s," \
+                            "bug_id=%s, creation_ts=%s, description=%s, mimetype=%s," \
                             "filename=%s, submitter_id=%s",
                             [ current['number'],
                               time.strftime("%Y-%m-%d %H:%M:%S", current['date-reported'][:9]),
-                              a[1], a[0], reporter ])
+                              a[0], a[1], a[0], reporter ])
             cursor.execute( "INSERT INTO attach_data SET " \
                             "id=LAST_INSERT_ID(), thedata=%s",
                             [ a[2] ])
diff --git a/editclassifications.cgi b/editclassifications.cgi
index ea4b139da..640b8b8cd 100755
--- a/editclassifications.cgi
+++ b/editclassifications.cgi
@@ -27,7 +27,6 @@ local our $vars = {};
 
 sub LoadTemplate {
     my $action = shift;
-    my $cgi = Bugzilla->cgi;
     my $template = Bugzilla->template;
 
     $vars->{'classifications'} = [Bugzilla::Classification->get_all]
@@ -38,7 +37,6 @@ sub LoadTemplate {
 
     $action =~ /(\w+)/;
     $action = $1;
-    print $cgi->header();
     $template->process("admin/classifications/$action.html.tmpl", $vars)
       || ThrowTemplateError($template->error());
     exit;
diff --git a/editflagtypes.cgi b/editflagtypes.cgi
index d848d250a..71f7cb655 100755
--- a/editflagtypes.cgi
+++ b/editflagtypes.cgi
@@ -436,17 +436,30 @@ sub get_products_and_components {
 
     my @products;
     if ($user->in_group('editcomponents')) {
-        @products = Bugzilla::Product->get_all;
+        if (Bugzilla->params->{useclassification}) {
+            # We want products grouped by classifications.
+            @products = map { @{ $_->products } } Bugzilla::Classification->get_all;
+        }
+        else {
+            @products = Bugzilla::Product->get_all;
+        }
     }
     else {
         @products = @{$user->get_products_by_permission('editcomponents')};
+
+        if (Bugzilla->params->{useclassification}) {
+            my %class;
+            push(@{$class{$_->classification_id}}, $_) foreach @products;
+
+            # Let's sort the list by classifications.
+            @products = ();
+            push(@products, @{$class{$_->id} || []}) foreach Bugzilla::Classification->get_all;
+        }
     }
-    # We require all unique component names.
+
     my %components;
     foreach my $product (@products) {
-        foreach my $component (@{$product->components}) {
-            $components{$component->name} = 1;
-        }
+        $components{$_->name} = 1 foreach @{$product->components};
     }
     $vars->{'products'} = \@products;
     $vars->{'components'} = [sort(keys %components)];
diff --git a/editgroups.cgi b/editgroups.cgi
index 287ac1114..f2c915556 100755
--- a/editgroups.cgi
+++ b/editgroups.cgi
@@ -135,8 +135,7 @@ sub get_current_and_available {
 unless ($action) {
     my @groups = Bugzilla::Group->get_all;
     $vars->{'groups'} = \@groups;
-    
-    print $cgi->header();
+
     $template->process("admin/groups/list.html.tmpl", $vars)
       || ThrowTemplateError($template->error());
     exit;
@@ -155,12 +154,10 @@ if ($action eq 'changeform') {
 
     get_current_and_available($group, $vars);
     $vars->{'group'} = $group;
-    $vars->{'token'}       = issue_session_token('edit_group');
+    $vars->{'token'} = issue_session_token('edit_group');
 
-    print $cgi->header();
     $template->process("admin/groups/edit.html.tmpl", $vars)
       || ThrowTemplateError($template->error());
-
     exit;
 }
 
@@ -172,10 +169,9 @@ if ($action eq 'changeform') {
 
 if ($action eq 'add') {
     $vars->{'token'} = issue_session_token('add_group');
-    print $cgi->header();
+
     $template->process("admin/groups/create.html.tmpl", $vars)
       || ThrowTemplateError($template->error());
-    
     exit;
 }
 
@@ -194,15 +190,9 @@ if ($action eq 'new') {
         isactive    => scalar $cgi->param('isactive'),
         icon_url    => scalar $cgi->param('icon_url'),
         isbuggroup  => 1,
+        use_in_all_products => scalar $cgi->param('insertnew'),
     });
 
-    # Permit all existing products to use the new group if makeproductgroups.
-    if ($cgi->param('insertnew')) {
-        $dbh->do('INSERT INTO group_control_map
-                  (group_id, product_id, membercontrol, othercontrol)
-                  SELECT ?, products.id, ?, ? FROM products',
-                  undef, ($group->id, CONTROLMAPSHOWN, CONTROLMAPNA));
-    }
     delete_token($token);
 
     $vars->{'message'} = 'group_created';
@@ -210,7 +200,6 @@ if ($action eq 'new') {
     get_current_and_available($group, $vars);
     $vars->{'token'} = issue_session_token('edit_group');
 
-    print $cgi->header();
     $template->process("admin/groups/edit.html.tmpl", $vars)
       || ThrowTemplateError($template->error());
     exit;
@@ -234,10 +223,8 @@ if ($action eq 'del') {
     $vars->{'group'} = $group;
     $vars->{'token'} = issue_session_token('delete_group');
 
-    print $cgi->header();
     $template->process("admin/groups/delete.html.tmpl", $vars)
       || ThrowTemplateError($template->error());
-    
     exit;
 }
 
@@ -261,7 +248,6 @@ if ($action eq 'delete') {
     $vars->{'message'} = 'group_deleted';
     $vars->{'groups'} = [Bugzilla::Group->get_all];
 
-    print $cgi->header();
     $template->process("admin/groups/list.html.tmpl", $vars)
       || ThrowTemplateError($template->error());
     exit;
@@ -283,7 +269,6 @@ if ($action eq 'postchanges') {
     $vars->{'changes'} = $changes;
     $vars->{'token'} = issue_session_token('edit_group');
 
-    print $cgi->header();
     $template->process("admin/groups/edit.html.tmpl", $vars)
       || ThrowTemplateError($template->error());
     exit;
@@ -294,6 +279,7 @@ if ($action eq 'confirm_remove') {
     $vars->{'group'} = $group;
     $vars->{'regexp'} = CheckGroupRegexp($cgi->param('regexp'));
     $vars->{'token'} = issue_session_token('remove_group_members');
+
     $template->process('admin/groups/confirm-remove.html.tmpl', $vars)
         || ThrowTemplateError($template->error());
     exit;
@@ -332,10 +318,8 @@ if ($action eq 'remove_regexp') {
     $vars->{'group'} = $group->name;
     $vars->{'groups'} = [Bugzilla::Group->get_all];
 
-    print $cgi->header();
     $template->process("admin/groups/list.html.tmpl", $vars)
       || ThrowTemplateError($template->error());
-
     exit;
 }
 
diff --git a/editkeywords.cgi b/editkeywords.cgi
index 41496f362..01f30dbed 100755
--- a/editkeywords.cgi
+++ b/editkeywords.cgi
@@ -24,10 +24,6 @@ my $dbh = Bugzilla->dbh;
 my $template = Bugzilla->template;
 my $vars = {};
 
-#
-# Preliminary checks:
-#
-
 my $user = Bugzilla->login(LOGIN_REQUIRED);
 
 print $cgi->header();
@@ -47,22 +43,16 @@ $vars->{'action'} = $action;
 if ($action eq "") {
     $vars->{'keywords'} = Bugzilla::Keyword->get_all_with_bug_count();
 
-    print $cgi->header();
     $template->process("admin/keywords/list.html.tmpl", $vars)
       || ThrowTemplateError($template->error());
-
     exit;
 }
-    
 
 if ($action eq 'add') {
     $vars->{'token'} = issue_session_token('add_keyword');
 
-    print $cgi->header();
-
     $template->process("admin/keywords/create.html.tmpl", $vars)
       || ThrowTemplateError($template->error());
-
     exit;
 }
 
@@ -79,8 +69,6 @@ if ($action eq 'new') {
 
     delete_token($token);
 
-    print $cgi->header();
-
     $vars->{'message'} = 'keyword_created';
     $vars->{'name'} = $keyword->name;
     $vars->{'keywords'} = Bugzilla::Keyword->get_all_with_bug_count();
@@ -104,7 +92,6 @@ if ($action eq 'edit') {
     $vars->{'keyword'} = $keyword;
     $vars->{'token'} = issue_session_token('edit_keyword');
 
-    print $cgi->header();
     $template->process("admin/keywords/edit.html.tmpl", $vars)
       || ThrowTemplateError($template->error());
     exit;
@@ -128,8 +115,6 @@ if ($action eq 'update') {
 
     delete_token($token);
 
-    print $cgi->header();
-
     $vars->{'message'} = 'keyword_updated';
     $vars->{'keyword'} = $keyword;
     $vars->{'changes'} = $changes;
@@ -147,7 +132,6 @@ if ($action eq 'del') {
     $vars->{'keyword'} = $keyword;
     $vars->{'token'} = issue_session_token('delete_keyword');
 
-    print $cgi->header();
     $template->process("admin/keywords/confirm-delete.html.tmpl", $vars)
       || ThrowTemplateError($template->error());
     exit;
@@ -162,8 +146,6 @@ if ($action eq 'delete') {
 
     delete_token($token);
 
-    print $cgi->header();
-
     $vars->{'message'} = 'keyword_deleted';
     $vars->{'keyword'} = $keyword;
     $vars->{'keywords'} = Bugzilla::Keyword->get_all_with_bug_count();
diff --git a/editusers.cgi b/editusers.cgi
index a38195a4b..37665b12d 100755
--- a/editusers.cgi
+++ b/editusers.cgi
@@ -70,7 +70,8 @@ if ($action eq 'search') {
     my $matchstr      = trim($cgi->param('matchstr'));
     my $matchtype     = $cgi->param('matchtype');
     my $grouprestrict = $cgi->param('grouprestrict') || '0';
-    my $is_enabled    = scalar $cgi->param('is_enabled');
+    # 0 = disabled only, 1 = enabled only, 2 = everyone
+    my $is_enabled    = $cgi->param('is_enabled') // 2;
     my $query = 'SELECT DISTINCT userid, login_name, realname, is_enabled, ' .
                 $dbh->sql_date_format('last_seen_date', '%Y-%m-%d') . ' AS last_seen_date ' .
                 'FROM profiles';
@@ -163,11 +164,12 @@ if ($action eq 'search') {
         }
 
         detaint_natural($is_enabled);
-        if ($is_enabled == 0 || $is_enabled == 1) {
+        if ($is_enabled && ($is_enabled == 0 || $is_enabled == 1)) {
             $query .= " $nextCondition profiles.is_enabled = ?";
             $nextCondition = 'AND';
             push(@bindValues, $is_enabled);
         }
+
         $query .= ' ORDER BY profiles.login_name';
 
         $vars->{'users'} = $dbh->selectall_arrayref($query,
diff --git a/extensions/Voting/Extension.pm b/extensions/Voting/Extension.pm
index 8009f16bb..b125933ce 100644
--- a/extensions/Voting/Extension.pm
+++ b/extensions/Voting/Extension.pm
@@ -610,7 +610,7 @@ sub _update_votes {
         $sth_getVotes->execute($id);
         my $v = $sth_getVotes->fetchrow_array || 0;
         $sth_updateVotes->execute($v, $id);
-
+        $bugs{$id}->{votes} = $v if $bugs{$id};
         my $confirmed = _confirm_if_vote_confirmed($bugs{$id} || $id);
         push (@updated_bugs, $id) if $confirmed;
     }
diff --git a/js/field.js b/js/field.js
index 2193c40eb..e4744f5e0 100644
--- a/js/field.js
+++ b/js/field.js
@@ -506,7 +506,8 @@ function handleVisControllerValueChange(e, args) {
     var controller = args[1];
     var values = args[2];
 
-    var label_container = 
+    var field = document.getElementById(controlled_id);
+    var label_container =
         document.getElementById('field_label_' + controlled_id);
     var field_container =
         document.getElementById('field_container_' + controlled_id);
@@ -521,10 +522,45 @@ function handleVisControllerValueChange(e, args) {
     if (selected) {
         YAHOO.util.Dom.removeClass(label_container, 'bz_hidden_field');
         YAHOO.util.Dom.removeClass(field_container, 'bz_hidden_field');
+        /* If a custom field such as a textarea field contains some text, then
+         * its content is visible by default as a readonly field (assuming that
+         * the field is displayed). But if such a custom field contains no text,
+         * then it's not displayed at all and an (edit) link is displayed instead.
+         * This is problematic if the custom field is mandatory, because at least
+         * Firefox complains that you must enter a value, but is unable to point
+         * to the custom field because this one is hidden, and so the user has
+         * to guess what the web browser is talking about, which is confusing.
+         * So in that case, we display the custom field automatically instead of
+         * the (edit) link, so that the user can enter some text in it.
+         */
+        var field_readonly = document.getElementById(controlled_id + '_readonly');
+
+        if (!field_readonly) {
+            var field_input = document.getElementById(controlled_id + '_input');
+            var edit_container =
+                document.getElementById(controlled_id + '_edit_container');
+
+            if (field_input) {
+                YAHOO.util.Dom.removeClass(field_input, 'bz_default_hidden');
+            }
+            if (edit_container) {
+                YAHOO.util.Dom.addClass(edit_container, 'bz_hidden_field');
+            }
+        }
+        // Restore the 'required' attribute for mandatory fields.
+        if (field.getAttribute('data-required') == "true") {
+            field.setAttribute('required', 'true');
+            field.setAttribute('aria-required', 'true');
+        }
     }
     else {
         YAHOO.util.Dom.addClass(label_container, 'bz_hidden_field');
         YAHOO.util.Dom.addClass(field_container, 'bz_hidden_field');
+        // A hidden field must never be required, because the user cannot set it.
+        if (field.getAttribute('data-required') == "true") {
+            field.removeAttribute('required');
+            field.removeAttribute('aria-required');
+        }
     }
 }
 
diff --git a/js/productform.js b/js/productform.js
index d5e24d306..0f669aa26 100644
--- a/js/productform.js
+++ b/js/productform.js
@@ -10,16 +10,10 @@
 // collection of javascript arrays containing strings.
 
 /**
- * Reads the selected products and updates component, version and milestone
- * lists accordingly.
+ * Reads the selected products and updates the component list accordingly.
  *
  * @param  product    Select element that contains products.
- * @param  component  Select element that contains components. Can be null if
- *                    there is no such element to update.
- * @param  version    Select element that contains versions. Can be null if
- *                    there is no such element to update.
- * @param  milestone  Select element that contains milestones. Can be null if
- *                    there is no such element to update.
+ * @param  component  Select element that contains components.
  * @param  anyval     Value to use for a special "Any" list item. Can be null
  *                    to not use any. If used must and will be first item in
  *                    the select element.
@@ -27,21 +21,15 @@
  * @global cpts       Array of arrays, indexed by product name. The subarrays
  *                    contain a list of components to be fed to the respective
  *                    select element.
- * @global vers       Array of arrays, indexed by product name. The subarrays
- *                    contain a list of versions to be fed to the respective
- *                    select element.
- * @global tms        Array of arrays, indexed by product name. The subarrays
- *                    contain a list of milestones to be fed to the respective
- *                    select element.
  * @global first_load Boolean; true if this is the first time this page loads
  *                    or false if not.
  * @global last_sel   Array that contains last list of products so we know what
  *                    has changed, and optimize for additions.
  */
-function selectProduct(product, component, version, milestone, anyval) {
+function selectProduct(product, component, anyval) {
     // This is to avoid handling events that occur before the form
     // itself is ready, which could happen in buggy browsers.
-    if (!product)
+    if (!product || !component)
         return;
 
     // Do nothing if no products are defined. This is to avoid the
@@ -78,15 +66,8 @@ function selectProduct(product, component, version, milestone, anyval) {
     var findall = (product.selectedIndex == -1
                    || (anyval != null && product.options[0].selected));
 
-    if (useclassification) {
-        // Update index based on the complete product array.
-        sel = get_selection(product, findall, true, anyval);
-        for (var i=0; i<sel.length; i++)
-           sel[i] = prods[sel[i]];
-    }
-    else {
-        sel = get_selection(product, findall, false, anyval);
-    }
+    sel = get_selection(product, findall, false, anyval);
+
     if (!findall) {
         // Save sel for the next invocation of selectProduct().
         var tmp = sel;
@@ -103,23 +84,9 @@ function selectProduct(product, component, version, milestone, anyval) {
     }
 
     // Do the actual fill/update.
-    if (component) {
-        var saved_cpts = get_selection(component, false, true, null);
-        updateSelect(cpts, sel, component, merging, anyval);
-        restoreSelection(component, saved_cpts);
-    }
-
-    if (version) {
-        var saved_vers = get_selection(version, false, true, null);
-        updateSelect(vers, sel, version, merging, anyval);
-        restoreSelection(version, saved_vers);
-    }
-
-    if (milestone) {
-        var saved_tms = get_selection(milestone, false, true, null);
-        updateSelect(tms, sel, milestone, merging, anyval);
-        restoreSelection(milestone, saved_tms);
-    }
+    var saved_cpts = get_selection(component, false, true, null);
+    updateSelect(cpts, sel, component, merging, anyval);
+    restoreSelection(component, saved_cpts);
 }
 
 /**
diff --git a/mod_perl.pl b/mod_perl.pl
index bdce744ca..f0de2e553 100644
--- a/mod_perl.pl
+++ b/mod_perl.pl
@@ -75,7 +75,7 @@ PerlChildInitHandler "sub { Bugzilla::RNG::srand(); srand(); }"
     PerlCleanupHandler  Apache2::SizeLimit Bugzilla::ModPerl::CleanupHandler
     PerlOptions +ParseHeaders
     Options +ExecCGI
-    AllowOverride Limit FileInfo Indexes Options
+    AllowOverride All
     DirectoryIndex index.cgi index.html
 </Directory>
 EOT
diff --git a/process_bug.cgi b/process_bug.cgi
index b3ed2d36a..0b0ecd64e 100755
--- a/process_bug.cgi
+++ b/process_bug.cgi
@@ -305,9 +305,10 @@ if (defined $cgi->param('id')) {
 
 my %is_private;
 foreach my $field (grep(/^defined_isprivate/, $cgi->param())) {
-    $field =~ /(\d+)$/;
-    my $comment_id = $1;
-    $is_private{$comment_id} = $cgi->param("isprivate_$comment_id");
+    if ($field =~ /(\d+)$/) {
+        my $comment_id = $1;
+        $is_private{$comment_id} = $cgi->param("isprivate_$comment_id");
+    }
 }
 $set_all_fields{comment_is_private} = \%is_private;
 
@@ -348,7 +349,7 @@ if (defined $cgi->param('id')) {
 
     # Tags can only be set to one bug at once.
     if (should_set('tag')) {
-        my @new_tags = split(/[\s,]+/, $cgi->param('tag'));
+        my @new_tags = grep { trim($_) } split(/,/, $cgi->param('tag'));
         my ($tags_removed, $tags_added) = diff_arrays($first_bug->tags, \@new_tags);
         $first_bug->remove_tag($_) foreach @$tags_removed;
         $first_bug->add_tag($_) foreach @$tags_added;
diff --git a/report.cgi b/report.cgi
index 2a8317d7a..d5f471ef0 100755
--- a/report.cgi
+++ b/report.cgi
@@ -359,19 +359,22 @@ my $format = $template->get_format("reports/report", $formatparam,
 # 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');
+if (exists $vars->{'debug'}) {
+    # Bug 1439260 - if we're using debug mode, always use the HTML template
+    # which has proper filters in it. Debug forces an HTML content type
+    # anyway, and can cause XSS if we're not filtering the output.
+    $format = $template->get_format("reports/report", $formatparam, "html");
+}
 
 $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')) {
+if (exists $vars->{'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>";
+    $vars->{'debug_hash'} = Data::Dumper::Dumper(%data);
+    $vars->{'debug_array'} = Data::Dumper::Dumper(@image_data);
 }
 
 # All formats point to the same section of the documentation.
diff --git a/request.cgi b/request.cgi
index 566781a0c..347cb7a44 100755
--- a/request.cgi
+++ b/request.cgi
@@ -305,13 +305,22 @@ sub queue {
     $vars->{'requests'} = \@requests;
     $vars->{'types'} = \@types;
 
-    my %components;
-    foreach my $prod (@{$user->get_selectable_products}) {
-        foreach my $comp (@{$prod->components}) {
-            $components{$comp->name} = 1;
+    # This code is needed to populate the Product and Component select fields.
+    my ($products, %components);
+    if (Bugzilla->params->{useclassification}) {
+        foreach my $class (@{$user->get_selectable_classifications}) {
+            push @$products, @{$user->get_selectable_products($class->id)};
         }
     }
-    $vars->{'components'} = [ sort { $a cmp $b } keys %components ];
+    else {
+        $products = $user->get_selectable_products;
+    }
+
+    foreach my $product (@$products) {
+        $components{$_->name} = 1 foreach @{$product->components};
+    }
+    $vars->{'products'} = $products;
+    $vars->{'components'} = [ sort keys %components ];
 
     $vars->{'urlquerypart'} = $cgi->canonicalise_query('ctype');
 
diff --git a/showdependencygraph.cgi b/showdependencygraph.cgi
index e3e54c4d8..7b2d2f55d 100755
--- a/showdependencygraph.cgi
+++ b/showdependencygraph.cgi
@@ -55,13 +55,19 @@ sub CreateImagemap {
             $default = qq{<area alt="" shape="default" href="$1">\n};
         }
 
-        if ($line =~ /^rectangle \((.*),(.*)\) \((.*),(.*)\) (http[^ ]*) (\d+)(\\n.*)?$/) {
+        if ($line =~ /^rectangle \((\d+),(\d+)\) \((\d+),(\d+)\) (http[^ ]*) (\d+)(?:\\n.*)?$/) {
             my ($leftx, $rightx, $topy, $bottomy, $url, $bugid) = ($1, $3, $2, $4, $5, $6);
 
             # Pick up bugid from the mapdata label field. Getting the title from
             # bugtitle hash instead of mapdata allows us to get the summary even
             # when showsummary is off, and also gives us status and resolution.
-            my $bugtitle = html_quote(clean_text($bugtitles{$bugid}));
+            # This text is safe; it has already been escaped.
+            my $bugtitle = $bugtitles{$bugid};
+
+            # The URL is supposed to be safe, because it's built manually.
+            # But in case someone manages to inject code, it's safer to escape it.
+            $url = html_quote($url);
+
             $map .= qq{<area alt="bug $bugid" name="bug$bugid" shape="rect" } .
                     qq{title="$bugtitle" href="$url" } .
                     qq{coords="$leftx,$topy,$rightx,$bottomy">\n};
@@ -180,13 +186,16 @@ foreach my $k (@bug_ids) {
     # Retrieve bug information from the database
     my ($stat, $resolution, $summary) = $dbh->selectrow_array($sth, undef, $k);
 
-    # Resolution and summary are shown only if user can see the bug
-    if (!$user->can_see_bug($k)) {
+    $vars->{'short_desc'} = $summary if ($k eq $cgi->param('id'));
+
+    # The bug summary is shown only if the user can see the bug.
+    if ($user->can_see_bug($k)) {
+        $summary = html_quote(clean_text($summary));
+    }
+    else {
         $summary = '';
     }
 
-    $vars->{'short_desc'} = $summary if ($k eq $cgi->param('id'));
-
     my @params;
 
     if ($summary ne "" && $cgi->param('showsummary')) {
@@ -195,6 +204,9 @@ foreach my $k (@bug_ids) {
             utf8::encode($summary) if utf8::is_utf8($summary);
         }
         $summary =~ s/([\\\"])/\\$1/g;
+        # Newlines must be escaped too, to not break the .map file
+        # and to prevent code injection.
+        $summary =~ s/\n/\\n/g;
         push(@params, qq{label="$k\\n$summary"});
     }
 
diff --git a/skins/standard/global.css b/skins/standard/global.css
index 8662540d1..be285984d 100644
--- a/skins/standard/global.css
+++ b/skins/standard/global.css
@@ -655,7 +655,7 @@ input.required, select.required, span.required_explanation {
     list-style-type: none;
 }
 
-.field_textarea_readonly {
+pre.field_textarea_readonly {
     margin: 2px;
     padding: 4px;
     overflow: auto;
@@ -663,9 +663,6 @@ input.required, select.required, span.required_explanation {
     max-width: 30em;
     max-height: 7em;
     border: 1px solid #CCC;
-}
-
-.field_textarea_readonly pre {
     font-family: monospace;
     white-space: pre-wrap;
 }
diff --git a/t/002goodperl.t b/t/002goodperl.t
index d1858361f..8fcd1a343 100644
--- a/t/002goodperl.t
+++ b/t/002goodperl.t
@@ -14,7 +14,7 @@ use 5.10.1;
 use strict;
 use warnings;
 
-use lib 't';
+use lib qw(. lib t);
 
 use Support::Files;
 
diff --git a/t/003safesys.t b/t/003safesys.t
index 443f96415..e1d04bed8 100644
--- a/t/003safesys.t
+++ b/t/003safesys.t
@@ -14,7 +14,7 @@ use 5.10.1;
 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 0a6f0e0aa..f336441ac 100644
--- a/t/004template.t
+++ b/t/004template.t
@@ -13,7 +13,7 @@ use 5.10.1;
 use strict;
 use warnings;
 
-use lib 't';
+use lib qw(. lib t);
 
 use Support::Templates;
 
@@ -114,10 +114,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 b6de8cee3..b7f10cae2 100644
--- a/t/005whitespace.t
+++ b/t/005whitespace.t
@@ -13,7 +13,7 @@ use 5.10.1;
 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 24e00242d..ea298c5f6 100644
--- a/t/006spellcheck.t
+++ b/t/006spellcheck.t
@@ -14,7 +14,7 @@ use 5.10.1;
 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 66c2df032..6f5ea8b72 100644
--- a/t/007util.t
+++ b/t/007util.t
@@ -13,7 +13,7 @@ use 5.10.1;
 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/009bugwords.t b/t/009bugwords.t
index e36651edb..5b0bf1f94 100644
--- a/t/009bugwords.t
+++ b/t/009bugwords.t
@@ -19,7 +19,7 @@ use 5.10.1;
 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 afd29a652..e0e990172 100644
--- a/t/010dependencies.t
+++ b/t/010dependencies.t
@@ -69,7 +69,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 cba9111d1..fd37faa7c 100644
--- a/t/011pod.t
+++ b/t/011pod.t
@@ -14,7 +14,7 @@ use 5.10.1;
 use strict;
 use warnings;
 
-use lib 't';
+use lib qw(. lib t);
 
 use Support::Files;
 use Pod::Checker;
@@ -35,6 +35,7 @@ use constant SUB_WHITELIST => (
     'Bugzilla::JobQueue' => qr/(?:^work_once|work_until_done|subprocess_worker)$/,
     'Bugzilla::Search'   => qr/^SPECIAL_PARSING$/,
     'Bugzilla::Template' => qr/^field_name$/,
+    'Bugzilla::MIME'     => qr/^as_string$/,
 );
 
 # These modules do not need to be documented, generally because they
diff --git a/taskgraph.json b/taskgraph.json
index 6d507fb20..ba1d1f3e0 100644
--- a/taskgraph.json
+++ b/taskgraph.json
@@ -10,14 +10,15 @@
       "reruns": 3,
       "maxRunTime": 3000,
       "task": {
+        "expires": "2018-02-18T17:33:38.806Z",
         "metadata": {
           "name": "Basic Sanity Tests"
         },
-        "provisionerId": "aws-provisioner",
-        "workerType": "gaia",
+        "provisionerId": "aws-provisioner-v1",
+        "workerType": "b2gtest",
         "payload": {
-          "image": "dklawren/docker-bugzilla",
-          "command": ["/runtests.sh"],
+          "image": "bugzilla/bugzilla-ci",
+          "command": ["runtests.sh"],
           "env": {
             "TEST_SUITE": "sanity"
           },
@@ -25,16 +26,19 @@
             "public/runtests_log": {
               "type": "file",
               "path": "/runtests.log",
-              "expires": "2016-02-17T17:33:38.806Z"
+              "expires": "2018-02-17T17:33:38.806Z"
             }
           }
         },
         "extra": {
           "treeherder": {
+            "symbol": "San",
             "machine": {
-              "platform": "5.0"
+                "platform": "linux64"
             },
-            "symbol": "San"
+            "build": {
+                "platform": "linux64"
+            }
           }
         }
       }
@@ -43,14 +47,15 @@
       "reruns": 3,
       "maxRunTime": 3000,
       "task": {
+        "expires": "2018-02-18T17:33:38.806Z",
         "metadata": {
           "name": "Documentation Build Test"
         },
-        "provisionerId": "aws-provisioner",
-        "workerType": "gaia",
+        "provisionerId": "aws-provisioner-v1",
+        "workerType": "b2gtest",
         "payload": {
-          "image": "dklawren/docker-bugzilla",
-          "command": ["/runtests.sh"],
+          "image": "bugzilla/bugzilla-ci",
+          "command": ["runtests.sh"],
           "env": {
             "TEST_SUITE": "docs"
           },
@@ -58,16 +63,19 @@
             "public/runtests_log": {
               "type": "file",
               "path": "/runtests.log",
-              "expires": "2016-02-17T17:33:38.806Z"
+              "expires": "2018-02-17T17:33:38.806Z"
             }
           }
         },
         "extra": {
           "treeherder": {
+            "symbol": "Doc",
             "machine": {
-              "platform": "5.0"
+                "platform": "linux64"
             },
-            "symbol": "Doc"
+            "build": {
+                "platform": "linux64"
+            }
           }
         }
       }
@@ -76,14 +84,15 @@
       "reruns": 3,
       "maxRunTime": 7200,
       "task": {
+        "expires": "2018-02-18T17:33:38.806Z",
         "metadata": {
           "name": "WebService API Tests (MySQL)"
         },
-        "provisionerId": "aws-provisioner",
-        "workerType": "gaia",
+        "provisionerId": "aws-provisioner-v1",
+        "workerType": "b2gtest",
         "payload": {
-          "image": "dklawren/docker-bugzilla",
-          "command": ["/runtests.sh"],
+          "image": "bugzilla/bugzilla-ci",
+          "command": ["runtests.sh"],
           "env": {
             "TEST_SUITE": "webservices"
           },
@@ -91,21 +100,24 @@
             "public/runtests_log": {
               "type": "file",
               "path": "/runtests.log",
-              "expires": "2016-02-17T17:33:38.806Z"
+              "expires": "2018-02-17T17:33:38.806Z"
             },
             "public/httpd_error_log": {
               "type": "file",
               "path": "/var/log/httpd/error_log",
-              "expires": "2016-02-17T17:33:38.806Z"
+              "expires": "2018-02-17T17:33:38.806Z"
             }
           }
         },
         "extra": {
           "treeherder": {
+            "symbol": "API",
             "machine": {
-              "platform": "5.0"
+                "platform": "linux64"
             },
-            "symbol": "API"
+            "build": {
+                "platform": "linux64"
+            }
           }
         }
       }
@@ -114,41 +126,45 @@
       "reruns": 3,
       "maxRunTime": 7200,
       "task": {
+        "expires": "2018-02-18T17:33:38.806Z",
         "metadata": {
           "name": "Selenium Tests (MySQL)"
         },
-        "provisionerId": "aws-provisioner",
-        "workerType": "gaia",
+        "provisionerId": "aws-provisioner-v1",
+        "workerType": "b2gtest",
         "payload": {
-          "image": "dklawren/docker-bugzilla",
-          "command": ["/runtests.sh"],
+          "image": "bugzilla/bugzilla-ci",
+          "command": ["runtests.sh"],
           "env": {
             "TEST_SUITE": "selenium"
           },
           "artifacts": {
             "public/runtests_log": {
               "type": "file",
-              "path": "/runtests.log",
-              "expires": "2016-02-17T17:33:38.806Z"
+              "path": "/tmp/runtests.log",
+              "expires": "2018-02-17T17:33:38.806Z"
             },
             "public/httpd_error_log": {
               "type": "file",
               "path": "/var/log/httpd/error_log",
-              "expires": "2016-02-17T17:33:38.806Z"
+              "expires": "2018-02-17T17:33:38.806Z"
             },
             "public/selenium_log": {
               "type": "file",
-              "path": "/selenium.log",
-              "expires": "2016-02-17T17:33:38.806Z"
+              "path": "/tmp/selenium.log",
+              "expires": "2018-02-17T17:33:38.806Z"
             }
           }
         },
         "extra": {
           "treeherder": {
+            "symbol": "Sel",
             "machine": {
-              "platform": "5.0"
+                "platform": "linux64"
             },
-            "symbol": "Sel"
+            "build": {
+                "platform": "linux64"
+            }
           }
         }
       }
@@ -157,36 +173,41 @@
       "reruns": 3,
       "maxRunTime": 7200,
       "task": {
+        "expires": "2018-02-18T17:33:38.806Z",
         "metadata": {
           "name": "WebService API Tests (Pg)"
         },
-        "provisionerId": "aws-provisioner",
-        "workerType": "gaia",
+        "provisionerId": "aws-provisioner-v1",
+        "workerType": "b2gtest",
         "payload": {
-          "image": "dklawren/docker-bugzilla:pgsql",
-          "command": ["/runtests.sh"],
+          "image": "bugzilla/bugzilla-ci",
+          "command": ["runtests.sh"],
           "env": {
+            "BUGS_DB_DRIVER": "pg",
             "TEST_SUITE": "webservices"
           },
           "artifacts": {
             "public/runtests_log": {
               "type": "file",
-              "path": "/runtests.log",
-              "expires": "2016-02-17T17:33:38.806Z"
+              "path": "/tmp/runtests.log",
+              "expires": "2018-02-17T17:33:38.806Z"
             },
             "public/httpd_error_log": {
               "type": "file",
               "path": "/var/log/httpd/error_log",
-              "expires": "2016-02-17T17:33:38.806Z"
+              "expires": "2018-02-17T17:33:38.806Z"
             }
           }
         },
         "extra": {
           "treeherder": {
+            "symbol": "API-Pg",
             "machine": {
-              "platform": "5.0"
+                "platform": "linux64"
             },
-            "symbol": "API-Pg"
+            "build": {
+                "platform": "linux64"
+            }
           }
         }
       }
@@ -195,41 +216,46 @@
       "reruns": 3,
       "maxRunTime": 7200,
       "task": {
+        "expires": "2018-02-18T17:33:38.806Z",
         "metadata": {
           "name": "Selenium Tests (Pg)"
         },
-        "provisionerId": "aws-provisioner",
-        "workerType": "gaia",
+        "provisionerId": "aws-provisioner-v1",
+        "workerType": "b2gtest",
         "payload": {
-          "image": "dklawren/docker-bugzilla:pgsql",
-          "command": ["/runtests.sh"],
+          "image": "bugzilla/bugzilla-ci",
+          "command": ["runtests.sh"],
           "env": {
+            "BUGS_DB_DRIVER": "pg",
             "TEST_SUITE": "selenium"
           },
           "artifacts": {
             "public/runtests_log": {
               "type": "file",
-              "path": "/runtests.log",
-              "expires": "2016-02-17T17:33:38.806Z"
+              "path": "/tmp/runtests.log",
+              "expires": "2018-02-17T17:33:38.806Z"
             },
             "public/httpd_error_log": {
               "type": "file",
               "path": "/var/log/httpd/error_log",
-              "expires": "2016-02-17T17:33:38.806Z"
+              "expires": "2018-02-17T17:33:38.806Z"
             },
             "public/selenium_log": {
               "type": "file",
-              "path": "/selenium.log",
-              "expires": "2016-02-17T17:33:38.806Z"
+              "path": "/tmp/selenium.log",
+              "expires": "2018-02-17T17:33:38.806Z"
             }
           }
         },
         "extra": {
           "treeherder": {
+            "symbol": "Sel-Pg",
             "machine": {
-              "platform": "5.0"
+                "platform": "linux64"
             },
-            "symbol": "Sel-Pg"
+            "build": {
+                "platform": "linux64"
+            }
           }
         }
       }
diff --git a/template/en/default/account/auth/login-small.html.tmpl b/template/en/default/account/auth/login-small.html.tmpl
index 790c135bf..a1a074372 100644
--- a/template/en/default/account/auth/login-small.html.tmpl
+++ b/template/en/default/account/auth/login-small.html.tmpl
@@ -27,14 +27,12 @@
   <a id="login_link[% qs_suffix %]" href="[% script_url FILTER html %]"
      onclick="return show_mini_login_form('[% qs_suffix %]')">Log In</a>
 
-  [% Hook.process('additional_methods') %]
-
   <form action="[% login_target FILTER html %]" method="POST"
         class="mini_login bz_default_hidden"
         id="mini_login[% qs_suffix FILTER html %]">
     <input id="Bugzilla_login[% qs_suffix FILTER html %]" required
            name="Bugzilla_login" class="bz_login"
-      [% IF Param('emailsuffix') %]
+      [% IF login_not_email %]
         placeholder="Login"
       [% ELSE %]
         type="email" placeholder="Email Address"
@@ -56,24 +54,28 @@
     <a href="#" onclick="return hide_mini_login_form('[% qs_suffix %]')">[x]</a>
   </form>
 </li>
-<li id="forgot_container[% qs_suffix %]">
-  <span class="separator">| </span>
-  <a id="forgot_link[% qs_suffix %]" href="[% script_url FILTER html %]#forgot"
-     onclick="return show_forgot_form('[% qs_suffix %]')">Forgot Password</a>
-  <form action="token.cgi" method="post" id="forgot_form[% qs_suffix %]"
-        class="mini_forgot bz_default_hidden">
-    <label for="login[% qs_suffix FILTER html %]">Login:</label>
-    <input name="loginname" size="20" id="login[% qs_suffix FILTER html %]" required
-      [% IF Param('emailsuffix') %]
-        placeholder="Your Login"
-      [% ELSE %]
-        type="email" placeholder="Your Email Address"
-      [% END %]>
-    <input id="forgot_button[% qs_suffix %]" value="Reset Password" 
-           type="submit">
-    <input type="hidden" name="a" value="reqpw">
-    <input type="hidden" id="token[% qs_suffix FILTER html %]" name="token"
-           value="[% issue_hash_token(['reqpw']) FILTER html %]">
-    <a href="#" onclick="return hide_forgot_form('[% qs_suffix %]')">[x]</a>
-  </form>
-</li>
+
+[% Hook.process('additional_methods') %]
+
+[% IF user.authorizer.can_change_password %]
+  <li id="forgot_container[% qs_suffix %]">
+    <span class="separator">| </span>
+    <a id="forgot_link[% qs_suffix %]" href="[% script_url FILTER html %]#forgot"
+       onclick="return show_forgot_form('[% qs_suffix %]')">Forgot Password</a>
+    <form action="token.cgi" method="post" id="forgot_form[% qs_suffix %]"
+          class="mini_forgot bz_default_hidden">
+      <label for="login[% qs_suffix FILTER html %]">Login:</label>
+      <input name="loginname" size="20" id="login[% qs_suffix FILTER html %]" required
+        [% IF login_not_email %]
+          placeholder="Your Login"
+        [% ELSE %]
+          type="email" placeholder="Your Email Address"
+        [% END %]>
+      <input id="forgot_button[% qs_suffix %]" value="Reset Password" type="submit">
+      <input type="hidden" name="a" value="reqpw">
+      <input type="hidden" id="token[% qs_suffix FILTER html %]" name="token"
+             value="[% issue_hash_token(['reqpw']) FILTER html %]">
+      <a href="#" onclick="return hide_forgot_form('[% qs_suffix %]')">[x]</a>
+    </form>
+  </li>
+[% END %]
diff --git a/template/en/default/account/auth/login.html.tmpl b/template/en/default/account/auth/login.html.tmpl
index 0af84542d..32ab14c9e 100644
--- a/template/en/default/account/auth/login.html.tmpl
+++ b/template/en/default/account/auth/login.html.tmpl
@@ -30,7 +30,7 @@
     <tr>
       <th>
         <label for="Bugzilla_login">
-          [% IF Param('emailsuffix') %]
+          [% IF login_not_email %]
             Login:
           [% ELSE %]
             Your Email Address:
@@ -39,7 +39,7 @@
       </th>
       <td>
         <input size="35" id="Bugzilla_login" name="Bugzilla_login"
-               [%- ' type="email"' UNLESS Param('emailsuffix') %] autofocus required>
+               [%- ' type="email"' UNLESS login_not_email %] autofocus required>
         [% Param('emailsuffix') FILTER html %]
       </td>
     </tr>
diff --git a/template/en/default/account/email/change-new.txt.tmpl b/template/en/default/account/email/change-new.txt.tmpl
index a6c54a773..f32abd80d 100644
--- a/template/en/default/account/email/change-new.txt.tmpl
+++ b/template/en/default/account/email/change-new.txt.tmpl
@@ -7,12 +7,12 @@
   #%]
 
 From: [% Param('mailfrom') %]
-To: [% emailaddress %]
+To: [% newemailaddress %]
 Subject: [% terms.Bugzilla %] Change Email Address Request
 X-Bugzilla-Type: admin
 
 [%+ terms.Bugzilla %] has received a request to change the email address
-for the account [% oldemailaddress %] to your address.
+for the account [% user.email %] to your address.
 
 To confirm the change, visit the following link:
 
diff --git a/template/en/default/account/email/change-old.txt.tmpl b/template/en/default/account/email/change-old.txt.tmpl
index 24b90a2b0..400004c5a 100644
--- a/template/en/default/account/email/change-old.txt.tmpl
+++ b/template/en/default/account/email/change-old.txt.tmpl
@@ -7,7 +7,7 @@
   #%]
 
 From: [% Param('mailfrom') %]
-To: [% emailaddress %]
+To: [% user.email %]
 Subject: [% terms.Bugzilla %] Change Email Address Request
 Importance: High
 X-MSMail-Priority: High
diff --git a/template/en/default/account/prefs/apikey.html.tmpl b/template/en/default/account/prefs/apikey.html.tmpl
index bed3f3809..71d4ac854 100644
--- a/template/en/default/account/prefs/apikey.html.tmpl
+++ b/template/en/default/account/prefs/apikey.html.tmpl
@@ -16,7 +16,7 @@
   one API key if required. Each API key has an optional description which can help
   you record what each key is used for. Documentation on how to log in is
   available from
-  <a href="[% docs_urlbase FILTER html %]api/Bugzilla/WebService.html#LOGGING_IN">here</a>.
+  <a href="[% docs_urlbase FILTER html %]api/core/v1/general.html#authentication">here</a>.
 </p>
 
 <h3>Existing API keys</h3>
diff --git a/template/en/default/account/prefs/prefs.html.tmpl b/template/en/default/account/prefs/prefs.html.tmpl
index 8875eb26f..5de0bf422 100644
--- a/template/en/default/account/prefs/prefs.html.tmpl
+++ b/template/en/default/account/prefs/prefs.html.tmpl
@@ -76,6 +76,10 @@
         In order to confirm your request, we have sent an email to your
         new email address. As a precaution, an email has also been sent
         to your old address allowing you to cancel this change if needed.
+        If you don't receive the email, you can
+        <a href="token.cgi?t=[% email_token FILTER uri %]&amp;a=cxlem">
+        cancel the email address change</a> from here if you wish (especially
+        if you mistyped the new email address).
       </p>
     [% END %]
   </div>
diff --git a/template/en/default/admin/admin.html.tmpl b/template/en/default/admin/admin.html.tmpl
index 7e3e5a157..ed41d7309 100644
--- a/template/en/default/admin/admin.html.tmpl
+++ b/template/en/default/admin/admin.html.tmpl
@@ -118,12 +118,6 @@
         and time, and get the result of these queries directly per email. This is a
         good way to create reminders and to keep track of the activity in your installation.</dd>
 
-        [% IF Param('use_mailer_queue') %]
-          [% class = user.in_group('admin') ? "" : "forbidden" %]
-          <dt id="view_job_queue" class="[% class %]"><a href="view_job_queue.cgi">Job Queue</a></dt>
-          <dd class="[% class %]">View the queue of undelivered/deferred jobs/emails.</dd>
-        [% END %]
-
         <dt id="release_notes">
           <a href="page.cgi?id=release-notes.html">Release Notes</a></dt>
         <dd>Detailed information related to this release of Bugzilla.</dd>
diff --git a/template/en/default/admin/flag-type/edit.html.tmpl b/template/en/default/admin/flag-type/edit.html.tmpl
index 6c0e0cb78..7505ebd8c 100644
--- a/template/en/default/admin/flag-type/edit.html.tmpl
+++ b/template/en/default/admin/flag-type/edit.html.tmpl
@@ -23,7 +23,7 @@
   title = title
   style_urls = ['skins/standard/admin.css']
   onload="var f = document.forms['flagtype_properties'];
-          selectProduct(f.product, f.component, null, null, '__Any__');"
+          selectProduct(f.product, f.component, '__Any__');"
   javascript_urls=["js/productform.js"]
   doc_section = "administering/flags.html"
 %]
@@ -92,7 +92,7 @@
                 id       => "product"
                 name     => "product"
                 add      => "__Any__"
-                onchange => "selectProduct(this, this.form.component, null, null, '__Any__');"
+                onchange => "selectProduct(this, this.form.component, '__Any__');"
                 products => products
               %]<br>
               <select name="component">
diff --git a/template/en/default/admin/flag-type/list.html.tmpl b/template/en/default/admin/flag-type/list.html.tmpl
index b91b49199..b1665c3b0 100644
--- a/template/en/default/admin/flag-type/list.html.tmpl
+++ b/template/en/default/admin/flag-type/list.html.tmpl
@@ -11,7 +11,7 @@
 [% PROCESS global/header.html.tmpl
   title = 'Administer Flag Types'
   style_urls = ['skins/standard/admin.css']
-  onload="var f = document.flagtype_form; selectProduct(f.product, f.component, null, null, '__All__');"
+  onload="var f = document.flagtype_form; selectProduct(f.product, f.component, '__All__');"
   javascript_urls=["js/productform.js"]
   doc_section = "administering/flags.html"
 %]
@@ -43,7 +43,7 @@
       id       => "product"
       name     => "product"
       add      => "__Any__"
-      onchange => "selectProduct(this, this.form.component, null, null, '__Any__');"
+      onchange => "selectProduct(this, this.form.component, '__Any__');"
       products => products
     %]
   </div>
diff --git a/template/en/default/admin/groups/list.html.tmpl b/template/en/default/admin/groups/list.html.tmpl
index 673c053cb..796872e1d 100644
--- a/template/en/default/admin/groups/list.html.tmpl
+++ b/template/en/default/admin/groups/list.html.tmpl
@@ -40,11 +40,11 @@
    }
    {name               => 'is_active_bug_group'
     heading            => "Use For $terms.Bugs"
-    align              => 'center'
+    class              => 'center'
    }
    {name               => 'type'
     heading            => 'Type'
-    align              => 'center'
+    class              => 'center'
    }
    {name               => 'action'
     heading            => 'Action'
diff --git a/template/en/default/admin/params/common.html.tmpl b/template/en/default/admin/params/common.html.tmpl
index db3bde777..fd9fd3ed1 100644
--- a/template/en/default/admin/params/common.html.tmpl
+++ b/template/en/default/admin/params/common.html.tmpl
@@ -32,7 +32,7 @@
         <label for="[% param.name FILTER html %]-off">Off</label>
       [% ELSIF param.type == "m" %]
         [% boxSize = 5 %]
-        [% boxSize = param.choices.size IF param.choices.size < 5 %]
+        [% SET boxSize = param.choices.size IF param.choices.size < 5 %]
 
         <select multiple="multiple" size="[% boxSize FILTER html %]"
                 name="[% param.name FILTER html %]" id="[% param.name FILTER html %]">
@@ -48,7 +48,7 @@
                name="[% param.name FILTER html %]"
                value="[% Param(param.name) FILTER html %]"><br>
         [% boxSize = 7 %]
-        [% boxSize = 3 + param.choices.size IF param.choices.size < 7 %]
+        [% SET boxSize = 3 + param.choices.size IF param.choices.size < 7 %]
         [% plist = Param(param.name).split(',') %]
 
         <table id="table_[% param.name FILTER html %]" class="bz_default_hidden">
diff --git a/template/en/default/admin/params/dependencygraph.html.tmpl b/template/en/default/admin/params/dependencygraph.html.tmpl
index 64a334a20..6ef25807c 100644
--- a/template/en/default/admin/params/dependencygraph.html.tmpl
+++ b/template/en/default/admin/params/dependencygraph.html.tmpl
@@ -34,10 +34,10 @@
                  for other systems the needed measures may vary. You can run <kbd>checksetup.pl</kbd>
                  to recreate the .htaccess file if it has been lost.",
 
-  font_file => "You can specify the full path to a TrueType font file which will be used " _
-               "to display text (labels, legends, ...) in charts and graphical reports. " _
-               "To support as many languages as possible, we recommend to specify a TrueType " _
-               "font such as <a href=\"http://unifoundry.com/unifont.html\">Unifont</a> " _
+  font_file => "You can specify the full path to a TrueType or OpenType font file which will " _
+               "be used to display text (labels, legends, ...) in charts and graphical reports. " _
+               "To support as many languages as possible, we recommend to specify a font " _
+               "such as <a href=\"http://unifoundry.com/unifont.html\">Unifont</a> " _
                "which supports all printable characters in the Basic Multilingual Plane. " _
                "If you leave this parameter empty, a default font will be used, but " _
                "its support is limited to English characters only and so other characters " _
diff --git a/template/en/default/admin/settings/edit.html.tmpl b/template/en/default/admin/settings/edit.html.tmpl
index 86fb79e7b..75766fcc2 100644
--- a/template/en/default/admin/settings/edit.html.tmpl
+++ b/template/en/default/admin/settings/edit.html.tmpl
@@ -44,7 +44,7 @@
           <th>Enabled</th>
         </tr>
 
-        [% FOREACH name = settings.keys %]
+        [% FOREACH name = settings.keys.sort %]
           [% checkbox_name = name _ '-enabled' %]
           <tr>
             <td>
diff --git a/template/en/default/admin/table.html.tmpl b/template/en/default/admin/table.html.tmpl
index 993181c7f..2c8eb5fe0 100644
--- a/template/en/default/admin/table.html.tmpl
+++ b/template/en/default/admin/table.html.tmpl
@@ -69,7 +69,7 @@
   <tr class="column_header">
     [% FOREACH c = columns %]
       [%# Default to align left for headers %]
-      <th class="[% (c.align || 'left') FILTER css_class_quote %]">
+      <th class="[% (c.class || 'left') FILTER css_class_quote %]">
         [% c.heading FILTER html %]
       </th>
     [% END %]
diff --git a/template/en/default/bug/comments.html.tmpl b/template/en/default/bug/comments.html.tmpl
index f4f79ec5c..931716f76 100644
--- a/template/en/default/bug/comments.html.tmpl
+++ b/template/en/default/bug/comments.html.tmpl
@@ -47,6 +47,7 @@
 [% DEFAULT mode = "show" %]
 [% user_cache = template_cache.users %]
 [% can_edit_comments = bug.check_can_change_field('longdesc', 0, 1) %]
+[% can_tag_comments = feature_enabled('jsonrpc') AND user.can_tag_comments %]
 
 <!-- This auto-sizes the comments and positions the collapse/expand links 
      to the right. -->
@@ -120,7 +121,7 @@
               <span class="bz_collapsed_actions">
             [% END %]
             [% IF can_edit_comments %]
-              [% IF user.can_tag_comments %]
+              [% IF can_tag_comments %]
                 [<a href="#"
                     onclick="YAHOO.bugzilla.commentTagging.toggle([% comment.id %], [% comment.count %]);return false">tag</a>]
               [% END %]
diff --git a/template/en/default/bug/edit.html.tmpl b/template/en/default/bug/edit.html.tmpl
index 90151161e..b8abe6bc5 100644
--- a/template/en/default/bug/edit.html.tmpl
+++ b/template/en/default/bug/edit.html.tmpl
@@ -229,20 +229,12 @@
     [%#  PRODUCT  #%]
     [%#############%]
     <tr>
-      [% INCLUDE "bug/field-label.html.tmpl"
-         field = bug_fields.product
-         desc_url = 'describecomponents.cgi'
-         value = bug.product
-        %]
-      <td class="field_value" id="field_container_product">
-        [% INCLUDE "global/product-select.html.tmpl"
-           id    => "product"
-           name  => "product"
-           value => bug.product
-           products => bug.choices.product
-           isselect => bug.check_can_change_field('product', 0, 1)
-         %]
-       </td>
+      [% INCLUDE bug/field.html.tmpl
+          bug = bug, field = bug_fields.product, value = bug.product
+          override_legal_values = bug.choices.product
+          desc_url = "describecomponents.cgi"
+          editable = bug.check_can_change_field('product', 0, 1)
+      %]
     </tr>
 
     [%# Classification is here so that it can be used in value controllers
diff --git a/template/en/default/bug/field.html.tmpl b/template/en/default/bug/field.html.tmpl
index f1ddf633f..cdf15fbd8 100644
--- a/template/en/default/bug/field.html.tmpl
+++ b/template/en/default/bug/field.html.tmpl
@@ -42,14 +42,21 @@
                name="[% field.name FILTER html %]"
                value="[% value FILTER html %]" size="40"
                maxlength="[% constants.MAX_FREETEXT_LENGTH FILTER none %]"
-               [% IF field.type == constants.FIELD_TYPE_INTEGER %] pattern="\d+[% IF dontchange %]|[% dontchange FILTER html %][% END %]" [% END %]
-               [% ' aria-required="true" required' IF field.is_mandatory %]>
+               [% IF field.type == constants.FIELD_TYPE_INTEGER %]
+                 pattern="-?\d+[% IF dontchange %]|[% dontchange FILTER html %][% END %]"
+                 title="The value must be a valid positive or negative integer"
+               [% END %]
+               [% IF field.is_mandatory %]
+                 data-required="true" [% 'aria-required="true" required' UNLESS field_hidden %]
+               [% END %]>
     [% CASE [constants.FIELD_TYPE_DATETIME, constants.FIELD_TYPE_DATE] %]
       [% size = (field.type == constants.FIELD_TYPE_DATE) ? 10 : 20 %]
       <input name="[% field.name FILTER html %]" size="[% size FILTER none %]"
              id="[% field.name FILTER html %]"
              value="[% value FILTER html %]"
-             [% ' aria-required="true" required' IF field.is_mandatory %]
+             [% IF field.is_mandatory %]
+               data-required="true" [% 'aria-required="true" required' UNLESS field_hidden %]
+             [% END %]
              onchange="updateCalendarFromField(this)">
       <button type="button" class="calendar_button"
               id="button_calendar_[% field.name FILTER html %]"
@@ -67,8 +74,9 @@
         <span id="[% field.name FILTER html %]_input_area">
           <input name="[% field.name FILTER html %]" id="[% field.name FILTER html %]"
                  value="[% value FILTER html %]" size="7"
-                 [% ' aria-required="true" required' IF field.is_mandatory %]>
-
+                 [% IF field.is_mandatory %]
+                   data-required="true" [% 'aria-required="true" required' UNLESS field_hidden %]
+                 [% END %]>
         </span>
 
         [% IF value %]  
@@ -86,18 +94,25 @@
         </script>
     [% CASE [ constants.FIELD_TYPE_SINGLE_SELECT 
               constants.FIELD_TYPE_MULTI_SELECT ] %]
+      [%# The 'product' field needs its own template if classifications are enabled. %]
+      [% IF field.name == "product" AND Param('useclassification') %]
+        [% INCLUDE "global/product-select.html.tmpl"
+                   id = "product", name = "product", value = value
+                   products = override_legal_values %]
+      [% ELSE %]
         <input type="hidden" id="[% field.name FILTER html %]_dirty">
         <select id="[% field.name FILTER html %]" 
                 name="[% field.name FILTER html %]" 
                 [% IF field.type == constants.FIELD_TYPE_MULTI_SELECT %]
-                    [% SET field_size = 5 %]
-                    [% IF field.legal_values.size < 5 %]
-                        [% SET field_size = field.legal_values.size %]
-                    [% END %]
-                    size="[% field_size FILTER html %]" multiple="multiple"
-                    [% ' aria-required="true" required' IF field.is_mandatory %]
-                [% END %]
-                >
+                  [% SET field_size = 5 %]
+                  [% IF field.legal_values.size < 5 %]
+                    [% SET field_size = field.legal_values.size %]
+                  [% END %]
+                  size="[% field_size FILTER html %]" multiple="multiple"
+                  [% IF field.is_mandatory %]
+                    data-required="true" [% 'aria-required="true" required' UNLESS field_hidden %]
+                  [% END %]
+                [% END %]>
           [% IF allow_dont_change %]
             <option value="[% dontchange FILTER html %]"
                    [% ' selected="selected"' IF value == dontchange %]>
@@ -135,22 +150,24 @@
         [% IF field.type == constants.FIELD_TYPE_MULTI_SELECT %]
           <input type="hidden" name="defined_[% field.name FILTER html %]">
         [% END %]
+      [% END %]
 
-        <script type="text/javascript">
+      <script type="text/javascript">
         <!--
           initHidingOptionsForIE('[% field.name FILTER js %]');
           [%+ INCLUDE "bug/field-events.js.tmpl"
                       field = field, product = bug.product_obj %]
         //-->
-        </script>
+      </script>
 
-     [% CASE constants.FIELD_TYPE_TEXTAREA %]
+    [% CASE constants.FIELD_TYPE_TEXTAREA %]
        <div id="[% field.name FILTER html %]_edit_container" class="bz_default_hidden">
          <div>
              (<a href="#" id="[% field.name FILTER html %]_edit_action">edit</a>)
          </div>
          [% IF value %]
-           <pre class="field_textarea_readonly">[% value FILTER html %]</pre>
+           <pre id="[% field.name FILTER html %]_readonly"
+                class="field_textarea_readonly">[% value FILTER html %]</pre>
          [% END %]
        </div>
        <div id="[% field.name FILTER html %]_input">
@@ -167,7 +184,7 @@
                            '',
                            true);
        </script>
-     [% CASE constants.FIELD_TYPE_BUG_URLS %]
+    [% CASE constants.FIELD_TYPE_BUG_URLS %]
        [% IF bug.id && value.size %]
          <ul class="bug_urls">
          [% FOREACH bug_url = value %]
diff --git a/template/en/default/bug/summarize-time.html.tmpl b/template/en/default/bug/summarize-time.html.tmpl
index 8bfe3c1c6..9613eebf1 100644
--- a/template/en/default/bug/summarize-time.html.tmpl
+++ b/template/en/default/bug/summarize-time.html.tmpl
@@ -267,6 +267,7 @@
       <button type="button" class="calendar_button" id="button_calendar_start_date"
               onclick="showCalendar('start_date')"><span>Calendar</span>
       </button>
+      <div id="con_calendar_start_date"></div>
       <span class="label">
         and <label accesskey="e" for="end_date"><u>e</u>nding</label>:
       </span>
@@ -276,6 +277,7 @@
       <button type="button" class="calendar_button" id="button_calendar_end_date"
               onclick="showCalendar('end_date')"><span>Calendar</span>
       </button>
+      <div id="con_calendar_end_date"></div>
       <br>
       <span class="bz_info">(Dates are optional, and in YYYY-MM-DD format)</span>
     </td>
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/flag/list.html.tmpl b/template/en/default/flag/list.html.tmpl
index 169cf8a51..778c3e142 100644
--- a/template/en/default/flag/list.html.tmpl
+++ b/template/en/default/flag/list.html.tmpl
@@ -121,8 +121,8 @@
                 class="flag_select flag_type-[% type.id %]"
                 [% IF !can_edit_flag %] disabled="disabled"[% END %]>
         [%# Only display statuses the user is allowed to set. %]
-        [% IF edit_multiple_bugs %]
-          <option value="--do_not_change--">--do_not_change--</option>
+        [% IF edit_multiple_bugs && dontchange %]
+          <option value="[% dontchange FILTER html %]">[% dontchange FILTER html %]</option>
         [% END %]
         [% IF !flag || (can_edit_flag && user.can_request_flag(type)) || flag.setter_id == user.id %]
           <option value="X" label="X"></option>
diff --git a/template/en/default/global/code-error.html.tmpl b/template/en/default/global/code-error.html.tmpl
index d73d75e13..830a7e7f6 100644
--- a/template/en/default/global/code-error.html.tmpl
+++ b/template/en/default/global/code-error.html.tmpl
@@ -49,7 +49,7 @@
     the error [% bug.error FILTER html %].
     
   [% ELSIF error == "chart_data_not_generated" %]
-    [% admindocslinks = {'extraconfig.html' => 'Setting up Charting'} %]
+    [% docslinks = {'using/reports-and-charts.html' => 'Setting up Charting'} %]
     [% IF product.id %]
       Charts for the <em>[% product.name FILTER html %]</em> product are not
       available yet because no charting data has been collected for it since it
@@ -290,6 +290,14 @@
     a <code>[% param FILTER html %]</code> argument, and that
     argument was not set.
 
+  [% ELSIF error == "param_integer_required" %]
+    The function <code>[% function FILTER html %]</code> requires
+    that <code>[% param FILTER html %]</code> be an integer.
+
+  [% ELSIF error == "param_scalar_array_required" %]
+    The <code>[% param FILTER html %]</code> parameter must be an array of scalars
+    (integers and/or strings).
+
   [% ELSIF error == "params_required" %]
     [% title = "Missing Parameter" %]
     The function <code>[% function FILTER html %]</code> requires
diff --git a/template/en/default/global/docslinks.html.tmpl b/template/en/default/global/docslinks.html.tmpl
index e33aa3b17..94502aa1f 100644
--- a/template/en/default/global/docslinks.html.tmpl
+++ b/template/en/default/global/docslinks.html.tmpl
@@ -7,29 +7,29 @@
   #%]
 
 [%# INTERFACE:
-  # docslinks: hash. Hash keys will be used as text of the documentation links,
-  #            hash values will be used as links to the document, relative to
+  # docslinks: hash. Hash values will be used as text of the documentation links,
+  #            hash keys will be used as links to the document, relative to
   #            the main Bugzilla documentation directory.
   #            Example: If you want a 'FAQ' link to point to, the "faq-general"
   #            named anchor on faq.html, assign
-  #               { 'FAQ' => "faq.html#faq-general" }
+  #               { "faq.html#faq-general" => 'FAQ' }
   #            to docslinks.
   #            You may only link to sections by their given ID; it is not allowed
   #            to link to a section which is not given an ID (thus getting
   #            assigned an automatically generated ID). Otherwise, the link
   #            would break on a recompilation of the documentation.
   # admindocslinks: hash. Same as docslinks, but will only be displayed to
-  #                 members of the admin group.
+  #                 members of the 'editcomponents' group.
   #%]
 
-[% IF docslinks.keys.size || (admindocslinks.keys.size && user.in_group('admin')) %]
+[% IF docslinks.keys.size || (admindocslinks.keys.size && user.in_group('editcomponents')) %]
   <div id="docslinks">
     <h2>Related documentation</h2>
     <ul>
-      [% IF user.in_group('admin') %]
-        [% PROCESS docslinkslist docstype = admindocslinks %]
+      [% IF user.in_group('editcomponents') %]
+        [% PROCESS docslinkslist docstype = admindocslinks admin = 1 %]
       [% END %]
-      [% PROCESS docslinkslist docstype = docslinks %]
+      [% PROCESS docslinkslist docstype = docslinks admin = 0 %]
     </ul>
   </div>
 [% END %]
@@ -37,7 +37,7 @@
 [% BLOCK docslinkslist %]
   [% FOREACH docslink = docstype.keys %]
     <li>
-      <a href="[% docs_urlbase FILTER html %]
+      <a href="[% docs_urlbase FILTER html %][% "administering/" IF admin %]
                [% docslink FILTER none %]">[% docstype.$docslink FILTER html %]</a>
     </li>
   [% END %]
diff --git a/template/en/default/global/header.html.tmpl b/template/en/default/global/header.html.tmpl
index e461e3746..0d6849ddb 100644
--- a/template/en/default/global/header.html.tmpl
+++ b/template/en/default/global/header.html.tmpl
@@ -32,8 +32,10 @@
   header_addl_info = ""
   onload = ""
   style_urls = []
+  javascript_urls = []
   yui = []
   generate_api_token = 0
+  favicon_url = "images/favicon.ico"
 %]
 
 [% SET yui_css = {
@@ -87,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) %]
@@ -207,7 +211,7 @@
     [%# Required for the 'Autodiscovery' feature in Firefox 2 and IE 7. %]
     <link rel="search" type="application/opensearchdescription+xml"
                        title="[% terms.Bugzilla %]" href="./search_plugin.cgi">
-    <link rel="shortcut icon" href="images/favicon.ico" >
+    <link rel="shortcut icon" href="[% favicon_url FILTER html %]">
     [% Hook.process("additional_header") %]
   </head>
 
diff --git a/template/en/default/global/js-products.html.tmpl b/template/en/default/global/js-products.html.tmpl
index e40a3e3ae..940a5e64d 100644
--- a/template/en/default/global/js-products.html.tmpl
+++ b/template/en/default/global/js-products.html.tmpl
@@ -8,14 +8,13 @@
 
 [%# The javascript block gets used in header.html.tmpl. %]
 [% javascript = BLOCK %]
-  var useclassification = false; // No classification level in use
   var first_load = true; // Is this the first time we load the page?
   var last_sel = []; // Caches last selection
   var cpts = new Array();
+
   [% n = 1 %]
   [% FOREACH prod = products %]
-    cpts['[% n %]'] = [
-      [%- FOREACH comp = prod.components %]'[% comp.name FILTER js %]'[% ", " UNLESS loop.last %] [%- END -%] ];
+    cpts['[% n %]'] = [[% FOREACH comp = prod.components %]'[% comp.name FILTER js %]'[% ", " UNLESS loop.last %] [%- END -%]];
     [% n = n+1 %]
   [% END %]
 [% END %]
diff --git a/template/en/default/global/messages.html.tmpl b/template/en/default/global/messages.html.tmpl
index 3a8aa1ada..bc8fe5b38 100644
--- a/template/en/default/global/messages.html.tmpl
+++ b/template/en/default/global/messages.html.tmpl
@@ -879,9 +879,16 @@
     [% title = "$terms.Bugzilla is Down" %]
     [% Param("shutdownhtml") %]
     [% IF userid %]
-      <p>For security reasons, you have been logged out automatically.
-      The cookie that was remembering your login is now gone.
+      <p>
+        For security reasons, you have been logged out automatically.
+        The cookie that was remembering your login is now gone.
+      </p>
     [% END %]
+    <p>
+      If you are an administrator, you can
+      <a href="editparams.cgi?section=general">reactivate [% terms.Bugzilla %]</a>
+      by clearing the <kbd>shutdownhtml</kbd> parameter.
+    </p>
 
   [% ELSIF message_tag == "term" %]
     [% terms.$term FILTER html %]
diff --git a/template/en/default/global/tabs.html.tmpl b/template/en/default/global/tabs.html.tmpl
index 9cf5a897b..511640477 100644
--- a/template/en/default/global/tabs.html.tmpl
+++ b/template/en/default/global/tabs.html.tmpl
@@ -25,7 +25,7 @@
             [% tab.label FILTER html %]</td>
         [% ELSE %]
           <td id="tab_[% tab.name FILTER html %]" class="clickable_area"
-              onClick="document.location='[% tab.link FILTER html %]'">
+              onClick="document.location='[% tab.link FILTER js FILTER html %]'">
             <a href="[% tab.link FILTER html %]">[% tab.label FILTER html %]</a>
           </td>
         [% END %]
diff --git a/template/en/default/global/textarea.html.tmpl b/template/en/default/global/textarea.html.tmpl
index f3e23dd3f..313329579 100644
--- a/template/en/default/global/textarea.html.tmpl
+++ b/template/en/default/global/textarea.html.tmpl
@@ -27,6 +27,8 @@
   # defaultcontent: (optional) Default content for the textarea.
   # mandatory:      (optional) Boolean specifying whether or not the textarea
   #                 is mandatory.
+  # field_hidden:   (optional) True if the field is hidden. Only useful if
+  #                 the field is mandatory.
   #%]
 
 <textarea [% IF name %]name="[% name FILTER html %]"[% END %]
@@ -46,7 +48,7 @@
             onFocus="this.rows=[% maxrows FILTER html %]"
           [% END %]
           [% IF mandatory %]
-            aria-required="true" required
+            data-required="true" [% 'aria-required="true" required' UNLESS field_hidden %]
           [% END %]
           [% IF onchange %]
             onchange="[% onchange FILTER html %]"
diff --git a/template/en/default/global/user-error.html.tmpl b/template/en/default/global/user-error.html.tmpl
index a0ae25c08..69afaf46a 100644
--- a/template/en/default/global/user-error.html.tmpl
+++ b/template/en/default/global/user-error.html.tmpl
@@ -94,7 +94,7 @@
 
   [% ELSIF error == "alias_too_long" %]
     [% title = "Alias Too Long" %]
-    [% terms.Bug %] aliases cannot be longer than 20 characters.
+    [% terms.Bug %] aliases cannot be longer than 40 characters.
     Please choose a shorter alias.
 
   [% ELSIF error == "api_key_not_valid" %]
@@ -119,7 +119,7 @@
 
   [% ELSIF error == "auth_failure" %]
     [% title = "Authorization Required" %]
-    [% admindocslinks = {'groups.html' => 'Group Security'} %]
+    [% admindocslinks = {'groups.html#groups' => 'Group Security'} %]
     Sorry,
     [% IF group %]
       you aren't a member of the '[% group FILTER html %]' group,
@@ -257,12 +257,12 @@
 
   [% ELSIF error == "bug_access_denied" %]
     [% title = "$terms.Bug Access Denied" %]
-    [% admindocslinks = {'groups.html' => 'Group Security'} %]
+    [% admindocslinks = {'groups.html#groups' => 'Group Security'} %]
     You are not authorized to access [% terms.bug %] #[% bug_id FILTER html %].
 
   [% ELSIF error == "bug_access_query" %]
     [% title = "$terms.Bug Access Denied" %]
-    [% docslinks = {'myaccount.html' => 'Creating an account'} %]
+    [% docslinks = {'administering/users.html#creating-new-users' => 'Creating an account'} %]
     You are not authorized to access [% terms.bug %] #[% bug_id FILTER html %].
     To see this [% terms.bug %], you must
     first <a href="show_bug.cgi?id=
@@ -303,8 +303,8 @@
 
   [% ELSIF error == "buglist_parameters_required" %]
     [% title = "Parameters Required" %]
-    [% docslinks = {'query.html' => "Searching for $terms.bugs",
-                    'query.html#list' => "$terms.Bug lists"} %]
+    [% docslinks = {'using/finding.html' => "Searching for $terms.bugs",
+                    'using/finding.html#bug-lists' => "$terms.Bug lists"} %]
     You may not search, or create saved searches, without any search terms.
 
   [% ELSIF error == "cc_remove_denied" %]
@@ -497,7 +497,7 @@
   
   [% ELSIF error == "entry_access_denied" %]
     [% title = "Permission Denied" %]
-    [% admindocslinks = {'groups.html' => 'Group Security'} %]
+    [% admindocslinks = {'categorization.html#product-group-controls' => 'Group Security'} %]
     Sorry, either the product <em>[% product FILTER html %]</em>
     does not exist or you aren't authorized to
     enter [% terms.abug %] into it.
@@ -695,10 +695,9 @@
 
   [% ELSIF error == "flag_requestee_unauthorized" %]
     [% title = "Flag Requestee Not Authorized" %]
-    [% admindocslinks = {'flags-overview.html#flags-admin'  => 'Administering Flags',
-                         'groups.html' => 'Group Security'} %]
-    [% docslinks = {'flags-overview.html' => 'An overview on Flags',
-                    'flags.html' => 'Using Flags'} %]
+    [% admindocslinks = {'flags.html' => 'Administering Flags',
+                         'categorization.html#assigning-group-controls-to-products' => 'Group Security'} %]
+    [% docslinks = {'using/understanding.html#flags' => 'Using Flags'} %]
 
     You asked [% requestee.identity FILTER html %]
     for <code>[% flag_type.name FILTER html %]</code> on [% terms.bug %] 
@@ -712,10 +711,9 @@
 
   [% ELSIF error == "flag_requestee_unauthorized_attachment" %]
     [% title = "Flag Requestee Not Authorized" %]
-    [% admindocslinks = {'flags-overview.html#flags-admin'  => 'Administering Flags',
-                         'groups.html' => 'Group Security'} %]
-    [% docslinks = {'flags-overview.html' => 'An overview on Flags',
-                    'flags.html' => 'Using Flags'} %]
+    [% admindocslinks = {'flags.html' => 'Administering Flags',
+                         'categorization.html#assigning-group-controls-to-products' => 'Group Security'} %]
+    [% docslinks = {'using/understanding.html#flags' => 'Using Flags'} %]
 
     You asked [% requestee.identity FILTER html %]
     for <code>[% flag_type.name FILTER html %]</code> on 
@@ -745,7 +743,7 @@
 
   [% ELSIF error == "flag_type_cc_list_invalid" %]
     [% title = "Flag Type CC List Invalid" %]
-    [% admindocslinks = {'flags-overview.html#flags-admin'  => 'Administering Flags'} %]
+    [% admindocslinks = {'flags.html' => 'Administering Flags'} %]
     The CC list [% cc_list FILTER html %] must be less than 200 characters long.
 
   [% ELSIF error == "flag_type_component_without_product" %]
@@ -754,12 +752,12 @@
 
   [% ELSIF error == "flag_type_description_invalid" %]
     [% title = "Flag Type Description Invalid" %]
-    [% admindocslinks = {'flags-overview.html#flags-admin'  => 'Administering Flags'} %]
+    [% admindocslinks = {'flags.html' => 'Administering Flags'} %]
     You must enter a description for this flag type.
 
   [% ELSIF error == "flag_type_name_invalid" %]
     [% title = "Flag Type Name Invalid" %]
-    [% admindocslinks = {'flags-overview.html#flags-admin'  => 'Administering Flags'} %]
+    [% admindocslinks = {'flags.html' => 'Administering Flags'} %]
     The name <em>[% name FILTER html %]</em> must be 1-50 characters long
     and must not contain any spaces or commas.
 
@@ -779,17 +777,15 @@
     You must specify the type id value to update or add a flag.
 
   [% ELSIF error == "flag_type_not_multiplicable" %]
-    [% docslinks = {'flags-overview.html' => 'An overview on Flags',
-                    'flags.html' => 'Using Flags'} %]
+    [% docslinks = {'using/understanding.html#flags' => 'Using Flags'} %]
     You cannot have several <em>[% type.name FILTER html %]</em> flags
     for this [% IF attachment %] attachment [% ELSE %] [%+ terms.bug %] [% END %].
 
   [% ELSIF error == "flag_update_denied" %]
     [% title = "Flag Modification Denied" %]
-    [% admindocslinks = {'flags-overview.html#flags-admin'  => 'Administering Flags',
-                         'groups.html' => 'Group Security'} %]
-    [% docslinks = {'flags-overview.html' => 'An overview on Flags',
-                    'flags.html' => 'Using Flags'} %]
+    [% admindocslinks = {'flags.html' => 'Administering Flags',
+                         'categorization.html#assigning-group-controls-to-products' => 'Group Security'} %]
+    [% docslinks = {'using/understanding.html#flags' => 'Using Flags'} %]
     You tried to [% IF status == "+" %] grant [% ELSIF status == "-" %] deny
     [% ELSIF status == "X" %] clear [% ELSE %] request [% END %]
     <code>[% name FILTER html %]
@@ -947,7 +943,7 @@
     
   [% ELSIF error == "illegal_group_control_combination" %]
     [% title = "Your Group Control Combination Is Illegal" %]
-    [% admindocslinks = {'groups.html' => 'Assigning Group Controls to Products'} %]
+    [% admindocslinks = {'categorization.html#assigning-group-controls-to-products' => 'Assigning Group Controls to Products'} %]
     Your group control combination for group &quot;
     [% groupname FILTER html %]&quot; is illegal.
 
@@ -964,13 +960,13 @@
     &lt;, &gt;, &amp;.
 
   [% ELSIF error == "illegal_series_creation" %]
-    [% admindocslinks = {'groups.html' => 'Group security'} %]
-    [% docslinks = {'reporting.html' => 'Reporting'} %]
+    [% admindocslinks = {'categorization.html#assigning-group-controls-to-products' => 'Group security'} %]
+    [% docslinks = {'using/reports-and-charts.html' => 'Reporting'} %]
     You are not authorized to create series.
         
   [% ELSIF error == "illegal_series_edit" %]
-    [% admindocslinks = {'groups.html' => 'Group security'} %]
-    [% docslinks = {'reporting.html' => 'Reporting'} %]
+    [% admindocslinks = {'categorization.html#assigning-group-controls-to-products' => 'Group security'} %]
+    [% docslinks = {'using/reports-and-charts.html' => 'Reporting'} %]
     You are not authorized to edit this series. To do this, you must either
     be its creator, or an administrator.
 
@@ -987,7 +983,7 @@
     The error was: [% dberror FILTER html %].
         
   [% ELSIF error == "insufficient_data_points" %]
-    [% docslinks = {'reporting.html' => 'Reporting'} %]
+    [% docslinks = {'using/reports-and-charts.html' => 'Reporting'} %]
     We don't have enough data points to make a graph (yet).
         
   [% ELSIF error == "invalid_attach_id" %]
@@ -1192,8 +1188,8 @@
 
   [% ELSIF error == "milestone_already_exists" %]
     [% title = "Milestone Already Exists" %]
-    [% admindocslinks = {'products.html' => 'Administering products',
-                         'milestones.html' => 'About Milestones'} %]
+    [% admindocslinks = {'categorization.html#products'   => 'Administering products',
+                         'categorization.html#milestones' => 'About Milestones'} %]
     The milestone '[% name FILTER html %]' already exists for product '
     [%- product FILTER html %]'.
 
@@ -1203,8 +1199,8 @@
 
   [% ELSIF error == "milestone_is_default" %]
     [% title = "Default milestone not deletable" %]
-    [% admindocslinks = {'products.html' => 'Administering products',
-                         'milestones.html' => 'About Milestones'} %]
+    [% admindocslinks = {'categorization.html#products'   => 'Administering products',
+                         'categorization.html#milestones' => 'About Milestones'} %]
     Sorry, but [% milestone.name FILTER html %] is the default milestone
     for the '[% milestone.product.name FILTER html %]' product, and so
     it cannot be deleted.
@@ -1251,8 +1247,8 @@
 
   [% ELSIF error == "missing_component" %]
     [% title = "Missing Component" %]
-    [% admindocslinks = {'products.html'   => 'Administering products',
-                         'components.html' => 'Creating a component'} %]
+    [% admindocslinks = {'categorization.html#products'   => 'Administering products',
+                         'categorization.html#milestones' => 'Creating a component'} %]
     Sorry, the product <em>[% product.name FILTER html %]</em>
     has to have at least one active component in order for you to
     enter [% terms.abug %] into it.<br>
@@ -1275,26 +1271,26 @@
     Sorry, I seem to have lost the cookie that recorded
     the results of your last search. I'm afraid you will have to start
     again from the <a href="query.cgi">search page</a>.
-          
+
   [% ELSIF error == "missing_datasets" %]
     [% title = "No Datasets Selected" %]
-    [% docslinks = {'reporting.html' => 'Reporting'} %]
+    [% docslinks = {'using/reports-and-charts.html' => 'Reporting'} %]
     You must specify one or more datasets to plot.
-    
+
   [% ELSIF error == "missing_frequency" %]
     [% title = "Missing Frequency" %]
-    [% docslinks = {'reporting.html' => 'Reporting'} %]
+    [% docslinks = {'using/reports-and-charts.html' => 'Reporting'} %]
     You did not specify a valid frequency for this series.
-                
+
   [% ELSIF error == "missing_name" %]
     [% title = "Missing Name" %]
-    [% docslinks = {'reporting.html' => 'Reporting'} %]
+    [% docslinks = {'using/reports-and-charts.html' => 'Reporting'} %]
     You did not specify a name for this series.
-                
+
   [% ELSIF error == "missing_query" %]
     [% title = "Missing Search" %]
-    [% docslinks = {'query.html' => "Searching for $terms.bugs",
-                    'query.html#list' => "$terms.Bug lists"} %]
+    [% docslinks = {'using/finding.html' => "Searching for $terms.bugs",
+                    'using/finding.html#bug-lists' => "$terms.Bug lists"} %]
     The search named <em>[% name FILTER html %]</em>
     [% IF sharer_id && sharer_id != user.id %]
       has not been made visible to you.
@@ -1317,7 +1313,7 @@
                 
   [% ELSIF error == "missing_version" %]
     [% title = "Missing Version" %]
-    [% admindocslinks = {'versions.html' => 'Defining versions'} %]
+    [% admindocslinks = {'categorization.html#versions' => 'Defining versions'} %]
     Sorry, the product <em>[% product.name FILTER html %]</em>
     has to have at least one active version in order for you to
     enter [% terms.abug %] into it.<br>
@@ -1335,7 +1331,7 @@
 
   [% ELSIF error == "need_quip" %]
     [% title = "Quip Required" %]
-    [% docslinks = {'quips.html' => 'About quips'} %]
+    [% docslinks = {'administering/quips.html' => 'About quips'} %]
     Please enter a quip in the text field.
 
   [% ELSIF error == "new_password_missing" %]
@@ -1344,7 +1340,7 @@
 
   [% ELSIF error == "no_axes_defined" %]
     [% title = "No Axes Defined" %]
-    [% docslinks = {'reporting.html' => 'Reporting'} %]
+    [% docslinks = {'using/reports-and-charts.html' => 'Reporting'} %]
     You didn't define any axes to plot.
 
   [% ELSIF error == "no_bugs_chosen" %]
@@ -1383,9 +1379,11 @@
 
   [% ELSIF error == "no_products" %]
     [% title = "No Products" %]
-    [% admindocslinks = {'products.html'   => 'Setting up a product',
-                         'components.html' => 'Adding components to products',
-                         'groups.html'     => 'Groups security'} %]
+    [% admindocslinks = {
+      'categorization.html#creating-new-products'  => 'Setting up a product',
+      'categorization.html#components'             => 'Adding components to products',
+      'categorization.html#product-group-controls' => 'Groups security'
+    } %]
     Either no products have been defined to enter [% terms.bugs %] against or you have not
     been given access to any.
 
@@ -1501,12 +1499,12 @@
 
   [% ELSIF error == "product_name_already_in_use" %]
     [% title = "Product name already exists" %]
-    [% admindocslinks = {'products.html' => 'Administering products'} %]
+    [% admindocslinks = {'categorization.html#products' => 'Administering products'} %]
     The product name '[% product FILTER html %]' already exists.
   
   [% ELSIF error == "product_name_diff_in_case" %]
     [% title = "Product name differs only in case" %]
-    [% admindocslinks = {'products.html' => 'Administering products'} %]
+    [% admindocslinks = {'categorization.html#products' => 'Administering products'} %]
     The product name '[% product FILTER html %]' differs from existing 
     product '[% existing_product FILTER html %]' only in case.
 
@@ -1517,8 +1515,8 @@
 
   [% ELSIF error == "product_must_define_defaultmilestone" %]
     [% title = "Must define new default milestone" %]
-    [% admindocslinks = {'products.html' => 'Administering products',
-                         'milestones.html' => 'About Milestones'} %]
+    [% admindocslinks = {'categorization.html#products'   => 'Administering products',
+                         'categorization.html#milestones' => 'About Milestones'} %]
     You must <a href="editmilestones.cgi?action=add&amp;product=[% product FILTER uri %]">
     create the milestone '[% milestone FILTER html %]'</a> before
     it can be made the default milestone for product '[% product FILTER html %]'.
@@ -1529,38 +1527,38 @@
 
   [% ELSIF error == "product_blank_name" %]
     [% title = "Blank Product Name Not Allowed" %]
-    [% admindocslinks = {'products.html' => 'Administering products'} %]
+    [% admindocslinks = {'categorization.html#products' => 'Administering products'} %]
     You must enter a name for the product.
   
   [% ELSIF error == "product_disabled" %]
     [% title = BLOCK %]Product closed for [% terms.Bug %] Entry[% END %]
-    [% admindocslinks = {'products.html' => 'Administering products'} %]
+    [% admindocslinks = {'categorization.html#products' => 'Administering products'} %]
     Sorry, entering [% terms.abug %] into the
     product <em>[% product.name FILTER html %]</em> has been disabled.
 
   [% ELSIF error == "product_edit_denied" %]
     [% title = "Product Edit Access Denied" %]
-    [% admindocslinks = {'products.html' => 'Administering products',
-                         'groups.html'   => 'Group security'} %]
+    [% admindocslinks = {'categorization.html#products' => 'Administering products',
+                         'categorization.html#assigning-group-controls-to-products' => 'Group security'} %]
     You are not permitted to edit [% terms.bugs %] in product 
     [%+ product FILTER html %].
 
   [% ELSIF error == "product_has_bugs" %]
     [% title = BLOCK %]Product has [% terms.Bugs %][% END %]
-    [% admindocslinks = {'products.html' => 'Administering products'} %]
+    [% admindocslinks = {'categorization.html#products' => 'Administering products'} %]
     There are [% nb FILTER html %] [%+ terms.bugs %] entered for this product!
     You must move those [% terms.bugs %] to another product before you
     can delete this one.
 
   [% ELSIF error == "product_must_have_description" %]
     [% title = "Product needs Description" %]
-    [% admindocslinks = {'products.html' => 'Administering products'} %]
+    [% admindocslinks = {'categorization.html#products' => 'Administering products'} %]
     You must enter a description for this product.
 
   [% ELSIF error == "product_must_have_version" %]
     [% title = "Product needs Version" %]
-    [% admindocslinks = {'products.html' => 'Administering products',
-                         'versions.html' => 'Administering versions'} %]
+    [% admindocslinks = {'categorization.html#products' => 'Administering products',
+                         'categorization.html#versions' => 'Administering versions'} %]
     You must enter a valid version to create a new product.
 
   [% ELSIF error == "product_unknown_component" %]
@@ -1574,7 +1572,7 @@
 
   [% ELSIF error == "query_name_missing" %]
     [% title = "No Search Name Specified" %]
-    [% docslinks = {'query.html#list' => "$terms.Bug lists"} %]
+    [% docslinks = {'using/finding.html#bug-lists' => "$terms.Bug lists"} %]
     You must enter a name for your search.
 
   [% ELSIF error == "query_name_too_long" %]
@@ -1683,7 +1681,7 @@
 
   [% ELSIF error == "saved_search_used_by_whines" %]
     [% title = "Saved Search In Use" %]
-    [% docslinks = {'whining.html' => 'About Whining'} %]
+    [% docslinks = {'administering/whining.html' => 'About Whining'} %]
     The saved search <em>[% search_name FILTER html %]</em> is being used 
     by <a href="editwhines.cgi">Whining events</a> with the following subjects:
     [%+ subjects FILTER html %]
@@ -1717,7 +1715,7 @@
 
   [% ELSIF error == "series_already_exists" %]
     [% title = "Series Already Exists" %]
-    [% docslinks = {'reporting.html' => 'Reporting'} %]
+    [% docslinks = {'using/reports-and-charts.html' => 'Reporting'} %]
       A series named <em>[% series.category FILTER html %] /
       [%+ series.subcategory FILTER html %] / 
       [%+ series.name FILTER html %]</em>
@@ -1820,7 +1818,7 @@
 
   [% ELSIF error == "version_already_exists" %]
     [% title = "Version Already Exists" %]
-    [% admindocslinks = {'versions.html' => 'Administering versions'} %]
+    [% admindocslinks = {'categorization.html#versions' => 'Administering versions'} %]
     The version '[% name FILTER html %]' already exists for product '
     [%- product FILTER html %]'.
 
@@ -1841,12 +1839,12 @@
 
   [% ELSIF error == "users_deletion_disabled" %]
     [% title = "Deletion not activated" %]
-    [% admindocslinks = {'useradmin.html' => 'User administration'} %]
+    [% admindocslinks = {'users.html#deleting-users' => 'User administration'} %]
     Sorry, the deletion of user accounts is not allowed.
 
   [% ELSIF error == "user_has_responsibility" %]
     [% title = "Can't Delete User Account" %]
-    [% admindocslinks = {'useradmin.html' => 'User administration'} %]
+    [% admindocslinks = {'users.html#deleting-users' => 'User administration'} %]
     The user you want to delete is set up as the default [% terms.bug %]
     assignee
     [% IF Param('useqacontact') %]
@@ -1867,7 +1865,7 @@
 
   [% ELSIF error == "user_login_required" %]
     [% title = "Login Name Required" %]
-    [% admindocslinks = {'useradmin.html' => 'User administration'} %]
+    [% admindocslinks = {'users.html' => 'User administration'} %]
     You must enter a login name for the new user.
 
   [% ELSIF error == "user_match_failed" %]
diff --git a/template/en/default/index.html.tmpl b/template/en/default/index.html.tmpl
index 84a5b7d5c..d0ccc61e7 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
diff --git a/template/en/default/list/list.html.tmpl b/template/en/default/list/list.html.tmpl
index 65065bf32..d34a454e0 100644
--- a/template/en/default/list/list.html.tmpl
+++ b/template/en/default/list/list.html.tmpl
@@ -237,7 +237,7 @@
         ><img title="iCal Format" alt="iCal" src="images/ical.png" height="24" width="24"></a>
         <button type="button" id="change_columns"
                 onclick="document.location='colchange.cgi?[% urlquerypart FILTER html %]&amp;query_based_on=
-                         [%~ defaultsavename OR searchname FILTER uri %]'">
+                         [%~ defaultsavename OR searchname FILTER uri FILTER js %]'">
             Change Columns</button>
 
 
@@ -250,7 +250,7 @@
 
         [% IF bugowners && user.id %]
           <button type="button" id="email_assignees"
-                  onclick="document.location='mailto:[% bugowners FILTER html %]'">
+                  onclick="document.location='mailto:[% bugowners FILTER html FILTER js %]'">
             Send Mail to [% terms.Bug %] Assignees</button>
         [% END %]
 
@@ -261,7 +261,7 @@
     [% END %]
     <div class="bz_query_edit">
       <button type="button" id="edit_search"
-              onclick="document.location='[% PROCESS edit_search_url %]'">
+              onclick="document.location='[% PROCESS edit_search_url FILTER js %]'">
           Edit Search</button>
     </div>
 
@@ -269,7 +269,7 @@
       <div class="bz_query_forget">
         <button type="button" id="forget_search"
                 onclick="document.location='buglist.cgi?cmdtype=dorem&amp;remaction=forget&amp;namedcmd=
-                         [%- searchname FILTER uri %]&amp;token=
+                         [%- searchname FILTER uri FILTER js %]&amp;token=
                          [%- issue_hash_token([search_id, searchname]) FILTER uri %]'">
           Forget Search '[% searchname FILTER html %]'</button>
       </div>
diff --git a/template/en/default/pages/release-notes.html.tmpl b/template/en/default/pages/release-notes.html.tmpl
index d4bf1c596..0304c4ad3 100644
--- a/template/en/default/pages/release-notes.html.tmpl
+++ b/template/en/default/pages/release-notes.html.tmpl
@@ -29,8 +29,8 @@
 
 <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
+<p>Welcome to [% terms.Bugzilla %] 5.0! It has been slightly over two years since we released
+  [%+ terms.Bugzilla %] 4.4 in May of 2013. This new major release comes with many new features
   and improvements to WebServices and performance.</p>
 
 <p>If you're upgrading, make sure to read
@@ -41,6 +41,189 @@
   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>
+
+<h3>5.0.4.1</h3>
+
+<p>This release fixes three security issues. See the
+  <a href="https://www.bugzilla.org/security/4.4.13/">Security Advisory</a>
+  for details.</p>
+
+<p>This release is intended to follow the stable 5.0 branch of
+[%+ terms.Bugzilla %]. Version 5.0.5 contained invasive fixes which should not
+have been on a stable branch, so we branched again from 5.0.4 to continue to
+provide security updates without the invasive changes. <b>This release track of
+[%+ terms.Bugzilla %] now lives on the 5.0.4 branch in Git.</b></p>
+
+<p>This release also contains the following [% terms.bug %] fixes:</p>
+
+<ul>
+
+  <li>MySQL 8.0 introduced non-backward-compatible changes which broke
+  Bugzilla.  This version now properly detects and prevents you from running it
+  on MySQL 8 or newer. Future versions of [% terms.Bugzilla %] will support
+  MySQL 8, but the changes are too invasive for a branch that only gets
+  security support. If you can't stay on MySQL 5.7.x or older, we recommend
+  using MariaDB or version 5.2 or newer of [% terms.Bugzilla %].
+  (<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1851354">[% terms.Bug %] 1851354</a>)</li>
+
+  <li>The "Email Assignees" link on the [% terms.Bug %] List page was fixed to
+  properly handle email addresses that contain apostrophes.
+  (<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1226123">[% terms.Bug %] 1226123</a>)</li>
+
+  <li>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>)</li>
+
+  <li>The Email::MIME module changed the way it set content types on emails in
+  version 1.949, causing [% terms.Bugzilla %] to throw an error and preventing
+  emails from being correctly delivered to recipients. We now set the content
+  type correctly on emails.
+  (<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1657496">[% terms.Bug %] 1657496</a>)</li>
+
+  <li>Template Toolkit versions 2.28 through 3.007 are blacklisted due to a
+  compatibility issue.  Versions 2.22 through 2.27 and 3.008 and later are
+  still supported.
+  (<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1560873">[% terms.Bug %] 1560873</a>)</li>
+
+  <li>[% terms.Bugzilla %] has a dependency on the Email::Address Perl module
+  which was unstated in the dependency list because it was also a dependency of
+  Email::Sender which [% terms.Bugzilla %] also uses. Newer versions of
+  Email::Sender stopped depending on Email::Address, so [% terms.Bugzilla %]
+  now needs to depend on it explicitly.
+  (<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1851352">[% terms.Bug %] 1851352</a>)</li>
+
+  <li>The contrib script <code>jb2bz.py</code> for importing from Jitterbugz
+  was updated to properly handle missing version and operating system values on
+  imported [% terms.bugs %].
+  (<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1429243">[% terms.Bug %] 1429243</a>)</li>
+
+</ul>
+
+<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>
+
+<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>
+
 <h2 id="req">Minimum Requirements</h2>
 
 <p>Any requirements that are new since 4.4 will look like
@@ -72,7 +255,7 @@
 <h3 id="req_modules">Required Perl Modules</h3>
 
 [% INCLUDE req_table reqs = REQUIRED_MODULES
-                     new = ['File-Slurp','JSON-XS', 'Email-Sender']
+                     new = ['JSON-XS', 'Email-Sender']
                      updated = ['DateTime', 'DateTime-TimeZone',
                                 'Template-Toolkit', 'URI'] %]
 
@@ -103,6 +286,7 @@
   you.</p>
 
 
+<a name="v50_feat"></a>
 <h2 id="feat">New Features and Improvements</h2>
 
 <ul>
@@ -304,9 +488,10 @@
     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> and <kbd>B[%%]ug.get_history</kbd>
-    methods are no longer supported. They have been renamed to <kbd>B[%%]ug.get</kbd>
-    and <kbd>B[%%]ug.history</kbd> respectively.</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>
 </ul>
 
 <h2 id="code_changes">Code Changes Which May Affect Customizations and Extensions</h2>
@@ -333,7 +518,7 @@
   <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.</kbd>
+    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>
 </ul>
diff --git a/template/en/default/reports/chart.html.tmpl b/template/en/default/reports/chart.html.tmpl
index dfab725e6..7004086de 100644
--- a/template/en/default/reports/chart.html.tmpl
+++ b/template/en/default/reports/chart.html.tmpl
@@ -20,6 +20,13 @@
   header_addl_info = time
 %]
 
+[% IF debug %]
+  <p>Bugzilla::Chart object:</p>
+  <pre>
+  [% debug_dump FILTER html %]
+  </pre>
+[% END %]
+
 <div class="center">
 
   [% imageurl = BLOCK %]chart.cgi?
diff --git a/template/en/default/reports/create-chart.html.tmpl b/template/en/default/reports/create-chart.html.tmpl
index 6b5fa5fe3..9ae25c608 100644
--- a/template/en/default/reports/create-chart.html.tmpl
+++ b/template/en/default/reports/create-chart.html.tmpl
@@ -18,6 +18,13 @@
   style_urls = ['skins/standard/buglist.css']
 %]
 
+[% IF debug %]
+  <p>Bugzilla::Chart object:</p>
+  <pre>
+  [% debug_dump FILTER html %]
+  </pre>
+[% END %]
+
 [% PROCESS "reports/series-common.html.tmpl" 
   donames = 1 
 %]
diff --git a/template/en/default/reports/report.html.tmpl b/template/en/default/reports/report.html.tmpl
index a9cd96551..b669070c2 100644
--- a/template/en/default/reports/report.html.tmpl
+++ b/template/en/default/reports/report.html.tmpl
@@ -61,6 +61,11 @@
 %]
 
 [% IF debug %]
+  <p>Data hash:</p>
+  <pre>[% debug_hash FILTER html %]</pre>
+  <p>Data array:</p>
+  <pre>[% debug_array FILTER html %]</pre>
+  <p>Queries:</p>
   [% FOREACH query = queries %]
     <p>[% query.sql FILTER html %]</p>
   [% END %]
diff --git a/template/en/default/request/queue.html.tmpl b/template/en/default/request/queue.html.tmpl
index 7e8c44c8b..101fdd046 100644
--- a/template/en/default/request/queue.html.tmpl
+++ b/template/en/default/request/queue.html.tmpl
@@ -9,40 +9,17 @@
 [% USE Bugzilla %]
 [% cgi = Bugzilla.cgi %]
 
+[% PROCESS "global/js-products.html.tmpl" %]
+
 [% PROCESS global/header.html.tmpl
   title="Request Queue"
   generate_api_token = 1
-  onload="var f = document.request_form; selectProduct(f.product, f.component, null, null, 'Any');"
+  onload="var f = document.request_form; selectProduct(f.product, f.component, 'Any');"
   javascript_urls=["js/productform.js", "js/field.js"]
   style_urls = ['skins/standard/buglist.css']
   yui = ['autocomplete']
 %]
 
-<script type="text/javascript">
-  var useclassification = false; // No classification level in use
-  var first_load = true; // Is this the first time we load the page?
-  var last_sel = []; // Caches last selection
-  var cpts = new Array();
-  [% n = 1 %]
-  [% IF Param('useclassification') %]
-    [% FOREACH clas = user.get_selectable_classifications %]
-      [% FOREACH prod = user.get_selectable_products(clas.id) %]
-        [%+ PROCESS js_comp %]
-      [% END %]
-    [% END %]
-  [% ELSE %]
-    [% FOREACH prod = user.get_selectable_products %]
-      [%+ PROCESS js_comp %]
-    [% END %]
-  [% END %]
-</script>
-
-[% BLOCK js_comp %]
-  cpts['[% n %]'] = [
-    [%- FOREACH comp = prod.components %]'[% comp.name FILTER js %]'[% ", " UNLESS loop.last %] [%- END -%]];
-  [% n = n+1 %]
-[% END %]
-
 <p>
 When you are logged in, only requests made by you or addressed to you
 are shown by default.  You can change the criteria using the form below.
@@ -72,7 +49,7 @@ to some group are shown by default.
            id       => "product"
            name     => "product"
            add      => "Any"
-           onchange => "selectProduct(this, this.form.component, null, null, 'Any');"
+           onchange => "selectProduct(this, this.form.component, 'Any');"
         %]
       </td>
       <th>Flag:</th>
diff --git a/template/en/default/search/form.html.tmpl b/template/en/default/search/form.html.tmpl
index 0420811ee..ac8aeaf61 100644
--- a/template/en/default/search/form.html.tmpl
+++ b/template/en/default/search/form.html.tmpl
@@ -247,7 +247,7 @@ TUI_hide_default('information_query');
       [% FOREACH qv = [
         { name => "substring", description => "contains" },
         { name => "notsubstring", description => "doesn't contain" },
-        { name => "exact", description => "is" },
+        { name => "equals", description => "is" },
         { name => "notequals", description => "is not" },
         { name => "regexp", description => "matches regexp" },
         { name => "notregexp", description => "doesn't match regexp" } ] %]
diff --git a/template/en/default/setup/strings.txt.pl b/template/en/default/setup/strings.txt.pl
index 78c4d861b..202b3c78d 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
diff --git a/template/en/default/whine/multipart-mime.txt.tmpl b/template/en/default/whine/header.txt.tmpl
similarity index 52%
rename from template/en/default/whine/multipart-mime.txt.tmpl
rename to template/en/default/whine/header.txt.tmpl
index d28f4cea6..4067964f2 100644
--- a/template/en/default/whine/multipart-mime.txt.tmpl
+++ b/template/en/default/whine/header.txt.tmpl
@@ -8,10 +8,6 @@
 
 [%# INTERFACE:
   # subject: subject line of message
-  # alternatives: array of hashes containing:
-  #     type: MIME type
-  #     content: verbatim content
-  # boundary: a string that has been generated to be a unique boundary
   # recipient: user object for the intended recipient of the message
   # from: Bugzilla system email address
   #%]
@@ -19,21 +15,5 @@
 From: [% from %]
 To: [% recipient.email %]
 Subject: [[% terms.Bugzilla %]] [% subject %]
-MIME-Version: 1.0
-Content-Type: multipart/alternative; boundary="[% boundary %]"
 X-Bugzilla-Type: whine
 
-
-This is a MIME multipart message.  It is possible that your mail program
-doesn't quite handle these properly.  Some or all of the information in this
-message may be unreadable.
-
-
-[% FOREACH part=alternatives %]
-
---[% boundary %]
-Content-type: [% part.type +%]
-
-[%+ part.content %]
-[%+ END %]
---[% boundary %]--
diff --git a/userprefs.cgi b/userprefs.cgi
index 71b274c01..089708d03 100755
--- a/userprefs.cgi
+++ b/userprefs.cgi
@@ -117,8 +117,7 @@ sub SaveAccount {
             is_available_username($new_login_name)
               || ThrowUserError("account_exists", {email => $new_login_name});
 
-            Bugzilla::Token::IssueEmailChangeToken($user, $new_login_name);
-
+            $vars->{'email_token'} = Bugzilla::Token::IssueEmailChangeToken($new_login_name);
             $vars->{'email_changes_saved'} = 1;
         }
     }
diff --git a/whine.pl b/whine.pl
index a7e3ee1cf..39c9aeed2 100755
--- a/whine.pl
+++ b/whine.pl
@@ -346,53 +346,20 @@ while (my $event = get_next_event) {
 #  - subject        Subject line for the message
 #  - recipient      user object for the recipient
 #  - author         user object of the person who created the whine event
-#
-# In addition, mail adds two more fields to $args:
-#  - alternatives   array of hashes defining mime multipart types and contents
-#  - boundary       a MIME boundary generated using the process id and time
-#
 sub mail {
     my $args = shift;
-    my $addressee = $args->{recipient};
     # Don't send mail to someone whose bugmail notification is disabled.
-    return if $addressee->email_disabled;
-
-    my $template = Bugzilla->template_inner($addressee->setting('lang'));
-    my $msg = ''; # it's a temporary variable to hold the template output
-    $args->{'alternatives'} ||= [];
-
-    # put together the different multipart mime segments
+    return if $args->{recipient}->email_disabled;
 
-    $template->process("whine/mail.txt.tmpl", $args, \$msg)
-        or die($template->error());
-    push @{$args->{'alternatives'}},
+    $args->{to_user} = $args->{recipient};
+    MessageToMTA(generate_email(
+        $args,
         {
-            'content' => $msg,
-            'type'    => 'text/plain',
-        };
-    $msg = '';
-
-    $template->process("whine/mail.html.tmpl", $args, \$msg)
-        or die($template->error());
-    push @{$args->{'alternatives'}},
-        {
-            'content' => $msg,
-            'type'    => 'text/html',
-        };
-    $msg = '';
-
-    # now produce a ready-to-mail mime-encoded message
-
-    $args->{'boundary'} = "----------" . $$ . "--" . time() . "-----";
-
-    $template->process("whine/multipart-mime.txt.tmpl", $args, \$msg)
-        or die($template->error());
-
-    MessageToMTA($msg);
-
-    delete $args->{'boundary'};
-    delete $args->{'alternatives'};
-
+            header => 'whine/header.txt.tmpl',
+            text   => 'whine/mail.txt.tmpl',
+            html   => 'whine/mail.html.tmpl',
+        }
+    ));
 }
 
 # run_queries runs all of the queries associated with a schedule ID, adding
