diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 000000000..1f1a91bb4
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,10 @@
+<!--
+
+NOTE: the 4.4 branch is currently on security-support only. Nothing will be accepted on this branch that isn't either repository maintenance or fixing a security bug, and the latter shouldn't be a public pull request.
+
+-->
+
+#### Additional info
+<!-- Paste the bug number after the # and after the = in the following line. -->
+* [bug#](https://bugzilla.mozilla.org/show_bug.cgi?id=)
+
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 000000000..30ca4583b
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,82 @@
+# 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: [ 4.4 ]
+  pull_request:
+    branches: [ 4.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 \
+          liblocal-lib-perl \
+          cpanminus \
+          graphviz
+      # apparently we can't get this from apt on Ubuntu
+      - name: Install Email::Send from CPAN
+        run: 'cpanm --sudo install Return::Value Email::Send'
+      - name: Get Perl Version and debug info
+        run: '/usr/bin/perl -V'
+      - name: Run tests
+        run: |
+          export PATH="${GITHUB_WORKSPACE}/perl5/bin${PATH:+:${PATH}}"
+          export PERL5LIB="${GITHUB_WORKSPACE}/perl5${PERL5LIB:+:${PERL5LIB}}"
+          /usr/bin/perl runtests.pl
diff --git a/.htaccess b/.htaccess
index 3b464a475..22e6658bd 100644
--- a/.htaccess
+++ b/.htaccess
@@ -1,6 +1,16 @@
 # Don't allow people to retrieve non-cgi executable files or our private data
 <FilesMatch (\.pm|\.pl|\.tmpl|localconfig.*)$>
-  deny from all
+  <IfModule mod_version.c>
+    <IfVersion < 2.4>
+     Deny from all
+    </IfVersion>
+    <IfVersion >= 2.4>
+      Require all denied
+    </IfVersion>
+  </IfModule>
+  <IfModule !mod_version.c>
+    Deny from all
+  </IfModule>
 </FilesMatch>
 
 Options -Indexes
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 000000000..1bbf29483
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,56 @@
+language: perl
+
+addons:
+  postgresql: "9.1"
+
+perl:
+  - 5.10
+  - 5.12
+
+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.12
+      env: TEST_SUITE=docs
+    - perl: 5.10
+      env: TEST_SUITE=webservices DB=mysql
+    - perl: 5.12
+      env: TEST_SUITE=selenium DB=mysql
+    - perl: 5.10
+      env: TEST_SUITE=webservices DB=pg
+    - perl: 5.12
+      env: TEST_SUITE=selenium DB=pg
+
+before_install:
+  - git clone https://github.com/bugzilla/qa.git -b 4.4 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 bb99764f2..6e31ba71a 100644
--- a/Bugzilla.pm
+++ b/Bugzilla.pm
@@ -67,7 +67,7 @@ use constant SHUTDOWNHTML_RETRY_AFTER => 3600;
 # Global Code
 #####################################################################
 
-# $::SIG{__DIE__} = i_am_cgi() ? \&CGI::Carp::confess : \&Carp::confess;
+#$::SIG{__DIE__} = i_am_cgi() ? \&CGI::Carp::confess : \&Carp::confess;
 
 # Note that this is a raw subroutine, not a method, so $class isn't available.
 sub init_page {
diff --git a/Bugzilla/Attachment.pm b/Bugzilla/Attachment.pm
index 380ef3d4c..cd8316a91 100644
--- a/Bugzilla/Attachment.pm
+++ b/Bugzilla/Attachment.pm
@@ -342,7 +342,7 @@ sub data {
     # If there's no attachment data in the database, the attachment is stored
     # in a local file, so retrieve it from there.
     if (length($self->{data}) == 0) {
-        if (open(AH, $self->_get_local_filename())) {
+        if (open(AH, '<', $self->_get_local_filename())) {
             local $/;
             binmode AH;
             $self->{data} = <AH>;
@@ -388,7 +388,7 @@ sub datasize {
     # is stored in a local file, and so retrieve its size from the file,
     # or the attachment has been deleted.
     unless ($self->{datasize}) {
-        if (open(AH, $self->_get_local_filename())) {
+        if (open(AH, '<', $self->_get_local_filename())) {
             binmode AH;
             $self->{datasize} = (stat(AH))[7];
             close(AH);
@@ -895,16 +895,12 @@ sub update {
     }
 
     # Record changes in the activity table.
-    my $sth = $dbh->prepare('INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when,
-                                                        fieldid, removed, added)
-                             VALUES (?, ?, ?, ?, ?, ?, ?)');
-
+    require Bugzilla::Bug;
     foreach my $field (keys %$changes) {
         my $change = $changes->{$field};
         $field = "attachments.$field" unless $field eq "flagtypes.name";
-        my $fieldid = get_field_id($field);
-        $sth->execute($self->bug_id, $self->id, $user->id, $timestamp,
-                      $fieldid, $change->[0], $change->[1]);
+        Bugzilla::Bug::LogActivityEntry($self->bug_id, $field, $change->[0],
+            $change->[1], $user->id, $timestamp, undef, $self->id);
     }
 
     if (scalar(keys %$changes)) {
diff --git a/Bugzilla/Attachment/PatchReader.pm b/Bugzilla/Attachment/PatchReader.pm
index 4026ba739..e75a660f2 100644
--- a/Bugzilla/Attachment/PatchReader.pm
+++ b/Bugzilla/Attachment/PatchReader.pm
@@ -99,7 +99,7 @@ sub process_interdiff {
     # Send through interdiff, send output directly to template.
     # Must hack path so that interdiff will work.
     $ENV{'PATH'} = $lc->{diffpath};
-    open my $interdiff_fh, "$lc->{interdiffbin} $old_filename $new_filename|";
+    open my $interdiff_fh, '-|', "$lc->{interdiffbin} $old_filename $new_filename";
     binmode $interdiff_fh;
     my ($reader, $last_reader) = setup_patch_readers("", $context);
 
diff --git a/Bugzilla/Auth/Login/CGI.pm b/Bugzilla/Auth/Login/CGI.pm
index 090680ebf..f29e8c9c1 100644
--- a/Bugzilla/Auth/Login/CGI.pm
+++ b/Bugzilla/Auth/Login/CGI.pm
@@ -55,7 +55,7 @@ sub get_login_info {
         ThrowUserError('auth_untrusted_request', { login => $login });
     }
 
-    if (!$login || !$password || !$valid) {
+    if (!defined($login) || !defined($password) || !$valid) {
         return { failure => AUTH_NODATA };
     }
 
diff --git a/Bugzilla/Auth/Verify/DB.pm b/Bugzilla/Auth/Verify/DB.pm
index 6ca04f259..99dc48ddc 100644
--- a/Bugzilla/Auth/Verify/DB.pm
+++ b/Bugzilla/Auth/Verify/DB.pm
@@ -68,7 +68,9 @@ sub check_credentials {
     # whatever hashing system we're using now.
     my $current_algorithm = PASSWORD_DIGEST_ALGORITHM;
     if ($real_password_crypted !~ /{\Q$current_algorithm\E}$/) {
-        $user->set_password($password);
+        # We can't call $user->set_password because we don't want the password
+        # complexity rules to apply here.
+        $user->{cryptpassword} = bz_crypt($password);
         $user->update();
     }
 
diff --git a/Bugzilla/Bug.pm b/Bugzilla/Bug.pm
index 97e81dfdd..b390c12d4 100644
--- a/Bugzilla/Bug.pm
+++ b/Bugzilla/Bug.pm
@@ -246,7 +246,6 @@ use constant MAX_LINE_LENGTH => 254;
 # use.)
 use constant FIELD_MAP => {
     blocks           => 'blocked',
-    cc_accessible    => 'cclist_accessible',
     commentprivacy   => 'comment_is_private',
     creation_time    => 'creation_ts',
     creator          => 'reporter',
@@ -355,14 +354,16 @@ sub new {
 
 sub check {
     my $class = shift;
-    my ($id, $field) = @_;
-
-    ThrowUserError('improper_bug_id_field_value', { field => $field }) unless defined $id;
+    my ($param, $field) = @_;
 
     # Bugzilla::Bug throws lots of special errors, so we don't call
     # SUPER::check, we just call our new and do our own checks.
-    $id = trim($id);
-    my $self = $class->new($id);
+    my $id = ref($param)
+        ? ($param->{id} = trim($param->{id}))
+        : ($param = trim($param));
+    ThrowUserError('improper_bug_id_field_value', { field => $field }) unless defined $id;
+
+    my $self = $class->new($param);
 
     if ($self->{error}) {
         # For error messages, use the id that was returned by new(), because
@@ -912,12 +913,6 @@ sub update {
                                    join(', ', @added_names)];
     }
 
-    # Flags
-    my ($removed, $added) = Bugzilla::Flag->update_flags($self, $old_bug, $delta_ts);
-    if ($removed || $added) {
-        $changes->{'flagtypes.name'} = [$removed, $added];
-    }
-
     # Comments
     foreach my $comment (@{$self->{added_comments} || []}) {
         # Override the Comment's timestamp to be identical to the update
@@ -940,6 +935,9 @@ sub update {
                          Bugzilla->user->id, $delta_ts, $comment->id);
     }
 
+    # Clear the cache of comments
+    delete $self->{comments};
+
     # Insert the values into the multiselect value tables
     my @multi_selects = grep {$_->type == FIELD_TYPE_MULTI_SELECT}
                              Bugzilla->active_custom_fields;
@@ -972,6 +970,12 @@ sub update {
                                 join(', ', map { $_->name } @$added_see)];
     }
 
+    # Flags
+    my ($removed, $added) = Bugzilla::Flag->update_flags($self, $old_bug, $delta_ts);
+    if ($removed || $added) {
+        $changes->{'flagtypes.name'} = [$removed, $added];
+    }
+
     $_->update foreach @{ $self->{_update_ref_bugs} || [] };
     delete $self->{_update_ref_bugs};
 
@@ -3939,7 +3943,8 @@ sub get_activity {
 
 # Update the bugs_activity table to reflect changes made in bugs.
 sub LogActivityEntry {
-    my ($i, $col, $removed, $added, $whoid, $timestamp, $comment_id) = @_;
+    my ($i, $col, $removed, $added, $whoid, $timestamp, $comment_id,
+        $attach_id) = @_;
     my $dbh = Bugzilla->dbh;
     # in the case of CCs, deps, and keywords, there's a possibility that someone
     # might try to add or remove a lot of them at once, which might take more
@@ -3964,10 +3969,13 @@ sub LogActivityEntry {
         trick_taint($addstr);
         trick_taint($removestr);
         my $fieldid = get_field_id($col);
-        $dbh->do("INSERT INTO bugs_activity
-                  (bug_id, who, bug_when, fieldid, removed, added, comment_id)
-                  VALUES (?, ?, ?, ?, ?, ?, ?)",
-                  undef, ($i, $whoid, $timestamp, $fieldid, $removestr, $addstr, $comment_id));
+        $dbh->do(
+            "INSERT INTO bugs_activity
+            (bug_id, who, bug_when, fieldid, removed, added, comment_id, attach_id)
+            VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
+            undef,
+            ($i, $whoid, $timestamp, $fieldid, $removestr, $addstr, $comment_id,
+                $attach_id));
     }
 }
 
diff --git a/Bugzilla/BugMail.pm b/Bugzilla/BugMail.pm
index 5a2c9b788..644ed1f1c 100644
--- a/Bugzilla/BugMail.pm
+++ b/Bugzilla/BugMail.pm
@@ -387,9 +387,10 @@ sub _generate_bugmail {
 
     # 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 {
+
+    # 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'};
diff --git a/Bugzilla/CGI.pm b/Bugzilla/CGI.pm
index 7bb9d96c4..19332b17a 100644
--- a/Bugzilla/CGI.pm
+++ b/Bugzilla/CGI.pm
@@ -283,6 +283,69 @@ sub close_standby_message {
     }
 }
 
+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;
@@ -293,6 +356,7 @@ sub header {
         # Since we're adding parameters below, we have to name it.
         unshift(@_, '-type' => shift(@_));
     }
+    $self->_prevent_unsafe_response({@_});
 
     if (!$user->id && $user->authorizer->can_login
         && !$self->cookie('Bugzilla_login_request_cookie'))
@@ -344,6 +408,7 @@ sub header {
 
 sub param {
     my $self = shift;
+    local $CGI::LIST_CONTEXT_WARN = 0;
 
     # When we are just requesting the value of a parameter...
     if (scalar(@_) == 1) {
diff --git a/Bugzilla/Chart.pm b/Bugzilla/Chart.pm
index 0a655769f..968d9a09b 100644
--- a/Bugzilla/Chart.pm
+++ b/Bugzilla/Chart.pm
@@ -94,10 +94,9 @@ sub init {
     if ($self->{'datefrom'} && $self->{'dateto'} && 
         $self->{'datefrom'} > $self->{'dateto'}) 
     {
-          ThrowUserError("misarranged_dates", 
-                                         {'datefrom' => $cgi->param('datefrom'),
-                                          'dateto' => $cgi->param('dateto')});
-    }    
+          ThrowUserError('misarranged_dates', { 'datefrom' => scalar $cgi->param('datefrom'),
+                                                'dateto' => scalar $cgi->param('dateto') });
+    }
 }
 
 # Alter Chart so that the selected series are added to it.
@@ -419,11 +418,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/Config/Common.pm b/Bugzilla/Config/Common.pm
index 0e3551d13..e1c2c8c40 100644
--- a/Bugzilla/Config/Common.pm
+++ b/Bugzilla/Config/Common.pm
@@ -23,7 +23,7 @@ use base qw(Exporter);
     qw(check_multi check_numeric check_regexp check_url check_group
        check_sslbase check_priority check_severity check_platform
        check_opsys check_shadowdb check_urlbase check_webdotbase
-       check_user_verify_class check_ip
+       check_user_verify_class check_ip check_smtp_server
        check_mail_delivery_method check_notification check_utf8
        check_bug_status check_smtp_auth check_theschwartz_available
        check_maxattachmentsize check_email check_smtp_ssl
@@ -231,7 +231,7 @@ sub check_webdotbase {
         # Check .htaccess allows access to generated images
         my $webdotdir = bz_locations()->{'webdotdir'};
         if(-e "$webdotdir/.htaccess") {
-            open HTACCESS, "$webdotdir/.htaccess";
+            open HTACCESS, "<", "$webdotdir/.htaccess";
             if(! grep(/ \\\.png\$/,<HTACCESS>)) {
                 return "Dependency graph images are not accessible.\nAssuming that you have not modified the file, delete $webdotdir/.htaccess and re-run checksetup.pl to rectify.\n";
             }
@@ -325,6 +325,19 @@ sub check_notification {
     return "";
 }
 
+sub check_smtp_server {
+    my $host = shift;
+    my $port;
+
+    if ($host =~ /:/) {
+        ($host, $port) = split(/:/, $host, 2);
+        unless ($port && detaint_natural($port)) {
+            return "Invalid port. It must be an integer (typically 25, 465 or 587)";
+        }
+    }
+    return "";
+}
+
 sub check_smtp_auth {
     my $username = shift;
     if ($username and !Bugzilla->feature('smtp_auth')) {
diff --git a/Bugzilla/Config/MTA.pm b/Bugzilla/Config/MTA.pm
index 8184b9f5e..e6e9505a3 100644
--- a/Bugzilla/Config/MTA.pm
+++ b/Bugzilla/Config/MTA.pm
@@ -49,7 +49,8 @@ sub get_param_list {
   {
    name => 'smtpserver',
    type => 't',
-   default => 'localhost'
+   default => 'localhost',
+   checker => \&check_smtp_server
   },
   {
    name => 'smtp_username',
diff --git a/Bugzilla/Constants.pm b/Bugzilla/Constants.pm
index 33d4a56e2..ae9e8da55 100644
--- a/Bugzilla/Constants.pm
+++ b/Bugzilla/Constants.pm
@@ -182,7 +182,7 @@ use Memoize;
 # CONSTANTS
 #
 # Bugzilla version
-use constant BUGZILLA_VERSION => "4.4.4";
+use constant BUGZILLA_VERSION => "4.4.14";
 
 # Location of the remote and local XML files to track new releases.
 use constant REMOTE_FILE => 'http://updates.bugzilla.org/bugzilla-update.xml';
diff --git a/Bugzilla/DB.pm b/Bugzilla/DB.pm
index cf828d772..248312e12 100644
--- a/Bugzilla/DB.pm
+++ b/Bugzilla/DB.pm
@@ -577,8 +577,11 @@ sub bz_add_column {
     my $current_def = $self->bz_column_info($table, $name);
 
     if (!$current_def) {
+        # REFERENCES need to happen later and not be created right away
+        my $trimmed_def = dclone($new_def);
+        delete $trimmed_def->{REFERENCES};
         my @statements = $self->_bz_real_schema->get_add_column_ddl(
-            $table, $name, $new_def, 
+            $table, $name, $trimmed_def,
             defined $init_value ? $self->quote($init_value) : undef);
         print get_text('install_column_add',
                        { column => $name, table => $table }) . "\n"
@@ -592,14 +595,14 @@ sub bz_add_column {
         # column exists there and has a REFERENCES item.
         # bz_setup_foreign_keys will then add this FK at the end of
         # Install::DB.
-        my $col_abstract = 
+        my $col_abstract =
             $self->_bz_schema->get_column_abstract($table, $name);
         if (exists $col_abstract->{REFERENCES}) {
             my $new_fk = dclone($col_abstract->{REFERENCES});
             $new_fk->{created} = 0;
             $new_def->{REFERENCES} = $new_fk;
         }
-        
+
         $self->_bz_real_schema->set_column($table, $name, $new_def);
         $self->_bz_store_real_schema;
     }
diff --git a/Bugzilla/DB/Mysql.pm b/Bugzilla/DB/Mysql.pm
index c7ce1927a..dc93b7406 100644
--- a/Bugzilla/DB/Mysql.pm
+++ b/Bugzilla/DB/Mysql.pm
@@ -70,17 +70,18 @@ sub new {
     $self->{private_bz_dsn} = $dsn;
 
     bless ($self, $class);
-    
-    # Bug 321645 - disable MySQL strict mode, if set
+
+    # Check for MySQL modes.
     my ($var, $sql_mode) = $self->selectrow_array(
         "SHOW VARIABLES LIKE 'sql\\_mode'");
 
+    # Disable ANSI and strict modes, else Bugzilla will crash.
     if ($sql_mode) {
         # STRICT_TRANS_TABLE or STRICT_ALL_TABLES enable MySQL strict mode,
         # causing bug 321645. TRADITIONAL sets these modes (among others) as
         # well, so it has to be stipped as well
         my $new_sql_mode =
-            join(",", grep {$_ !~ /^STRICT_(?:TRANS|ALL)_TABLES|TRADITIONAL$/}
+            join(",", grep {$_ !~ /^(?:ANSI|STRICT_(?:TRANS|ALL)_TABLES|TRADITIONAL)$/}
                             split(/,/, $sql_mode));
 
         if ($sql_mode ne $new_sql_mode) {
diff --git a/Bugzilla/DB/Sqlite.pm b/Bugzilla/DB/Sqlite.pm
index 47cb0cd25..3470ffc12 100644
--- a/Bugzilla/DB/Sqlite.pm
+++ b/Bugzilla/DB/Sqlite.pm
@@ -215,6 +215,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/Error.pm b/Bugzilla/Error.pm
index cebc2a4ac..32c7715b4 100644
--- a/Bugzilla/Error.pm
+++ b/Bugzilla/Error.pm
@@ -71,7 +71,7 @@ sub _throw_error {
             $val = "*****" if $val =~ /password|http_pass/i;
             $mesg .= "[$$] " . Data::Dumper->Dump([$val],["env($var)"]);
         }
-        open(ERRORLOGFID, ">>$datadir/errorlog");
+        open(ERRORLOGFID, ">>", "$datadir/errorlog");
         print ERRORLOGFID "$mesg\n";
         close ERRORLOGFID;
     }
diff --git a/Bugzilla/Field.pm b/Bugzilla/Field.pm
index c4d687afb..0c9da9b56 100644
--- a/Bugzilla/Field.pm
+++ b/Bugzilla/Field.pm
@@ -196,6 +196,12 @@ use constant DEFAULT_FIELDS => (
      buglist => 1},
     {name => 'qa_contact',   desc => 'QAContact',  in_new_bugmail => 1,
      buglist => 1},
+    {name => 'assigned_to_realname',  desc => 'AssignedToName',
+     in_new_bugmail => 0, buglist => 1},
+    {name => 'reporter_realname',     desc => 'ReportedByName',
+     in_new_bugmail => 0, buglist => 1},
+    {name => 'qa_contact_realname',   desc => 'QAContactName',
+     in_new_bugmail => 0, buglist => 1},
     {name => 'cc',           desc => 'CC',         in_new_bugmail => 1},
     {name => 'dependson',    desc => 'Depends on', in_new_bugmail => 1,
      is_numeric => 1},
diff --git a/Bugzilla/Flag.pm b/Bugzilla/Flag.pm
index 3cba94c88..affeaee68 100644
--- a/Bugzilla/Flag.pm
+++ b/Bugzilla/Flag.pm
@@ -455,14 +455,15 @@ sub create {
 sub update {
     my $self = shift;
     my $dbh = Bugzilla->dbh;
-    my $timestamp = shift || $dbh->selectrow_array('SELECT NOW()');
+    my $timestamp = shift || $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
 
     my $changes = $self->SUPER::update(@_);
 
     if (scalar(keys %$changes)) {
         $dbh->do('UPDATE flags SET modification_date = ? WHERE id = ?',
                  undef, ($timestamp, $self->id));
-        $self->{'modification_date'} = format_time($timestamp, '%Y.%m.%d %T');
+        $self->{'modification_date'} =
+          format_time($timestamp, '%Y.%m.%d %T', Bugzilla->local_timezone);
     }
     return $changes;
 }
@@ -1006,18 +1007,32 @@ sub notify {
         $default_lang = Bugzilla::User->new()->setting('lang');
     }
 
+    # Get comments on the bug
+    my $all_comments = $bug->comments({ after => $bug->lastdiffed });
+    @$all_comments   = grep { $_->type || $_->body =~ /\S/ } @$all_comments;
+
+    # Get public only comments
+    my $public_comments = [ grep { !$_->is_private } @$all_comments ];
+
     foreach my $to (keys %recipients) {
         # Add threadingmarker to allow flag notification emails to be the
         # threaded similar to normal bug change emails.
         my $thread_user_id = $recipients{$to} ? $recipients{$to}->id : 0;
 
-        my $vars = { 'flag'            => $flag,
-                     'old_flag'        => $old_flag,
-                     'to'              => $to,
-                     'date'            => $timestamp,
-                     'bug'             => $bug,
-                     'attachment'      => $attachment,
-                     'threadingmarker' => build_thread_marker($bug->id, $thread_user_id) };
+        # We only want to show private comments to users in the is_insider group
+        my $comments = $recipients{$to} && $recipients{$to}->is_insider
+            ? $all_comments : $public_comments;
+
+        my $vars = {
+            flag            => $flag,
+            old_flag        => $old_flag,
+            to              => $to,
+            date            => $timestamp,
+            bug             => $bug,
+            attachment      => $attachment,
+            threadingmarker => build_thread_marker($bug->id, $thread_user_id),
+            new_comments    => $comments,
+        };
 
         my $lang = $recipients{$to} ?
           $recipients{$to}->setting('lang') : $default_lang;
diff --git a/Bugzilla/FlagType.pm b/Bugzilla/FlagType.pm
index 9c20293bf..9e7ab09de 100644
--- a/Bugzilla/FlagType.pm
+++ b/Bugzilla/FlagType.pm
@@ -39,6 +39,7 @@ use Bugzilla::Util;
 use Bugzilla::Group;
 
 use Email::Address;
+use List::MoreUtils qw(uniq);
 
 use base qw(Bugzilla::Object);
 
@@ -369,8 +370,6 @@ sub set_clusions {
                 if (!$products{$prod_id}) {
                     $params->{id} = $prod_id;
                     $products{$prod_id} = Bugzilla::Product->check($params);
-                    $user->in_group('editcomponents', $prod_id)
-                      || ThrowUserError('product_access_denied', $params);
                 }
                 $prod_name = $products{$prod_id}->name;
 
@@ -396,6 +395,22 @@ sub set_clusions {
             $clusions{"$prod_name:$comp_name"} = "$prod_id:$comp_id";
             $clusions_as_hash{$prod_id}->{$comp_id} = 1;
         }
+
+        # Check the user has the editcomponent permission on products that are changing
+        if (! $user->in_group('editcomponents')) {
+            my $current_clusions = $self->$category;
+            my ($removed, $added)
+                = diff_arrays([ values %$current_clusions ], [ values %clusions ]);
+            my @changed_product_ids
+                = uniq map { substr($_, 0, index($_, ':')) } @$removed, @$added;
+            foreach my $product_id (@changed_product_ids) {
+                $user->in_group('editcomponents', $product_id)
+                    || ThrowUserError('product_access_denied',
+                                      { name => $products{$product_id}->name });
+            }
+        }
+
+        # Set the changes
         $self->{$category} = \%clusions;
         $self->{"${category}_as_hash"} = \%clusions_as_hash;
         $self->{"_update_$category"} = 1;
diff --git a/Bugzilla/Group.pm b/Bugzilla/Group.pm
index 04c36f694..5404dec7e 100644
--- a/Bugzilla/Group.pm
+++ b/Bugzilla/Group.pm
@@ -53,7 +53,7 @@ use constant UPDATE_COLUMNS => qw(
 
 # Parameters that are lists of groups.
 use constant GROUP_PARAMS => qw(chartgroup insidergroup timetrackinggroup
-                                querysharegroup);
+                                querysharegroup debug_group);
 
 ###############################
 ####      Accessors      ######
diff --git a/Bugzilla/Install/CPAN.pm b/Bugzilla/Install/CPAN.pm
index f96bb4cb9..8a880df80 100644
--- a/Bugzilla/Install/CPAN.pm
+++ b/Bugzilla/Install/CPAN.pm
@@ -203,8 +203,8 @@ sub set_cpan_config {
     # Calling a senseless autoload that does nothing makes us
     # automatically load any existing configuration.
     # We want to avoid the "invalid command" message.
-    open(my $saveout, ">&STDOUT");
-    open(STDOUT, '>/dev/null');
+    open(my $saveout, ">&", "STDOUT");
+    open(STDOUT, '>', '/dev/null');
     eval { CPAN->ignore_this_error_message_from_bugzilla; };
     undef $@;
     close(STDOUT);
diff --git a/Bugzilla/Install/Filesystem.pm b/Bugzilla/Install/Filesystem.pm
index cf61a6ec2..eaca1f8b4 100644
--- a/Bugzilla/Install/Filesystem.pm
+++ b/Bugzilla/Install/Filesystem.pm
@@ -43,7 +43,17 @@ our @EXPORT = qw(
 use constant HT_DEFAULT_DENY => <<EOT;
 # nothing in this directory is retrievable unless overridden by an .htaccess
 # in a subdirectory
-deny from all
+<IfModule mod_version.c>
+  <IfVersion < 2.4>
+    Deny from all
+  </IfVersion>
+  <IfVersion >= 2.4>
+    Require all denied
+  </IfVersion>
+</IfModule>
+<IfModule !mod_version.c>
+    Deny from all
+</IfModule>
 EOT
 
 ###############
@@ -329,11 +339,31 @@ EOT
         "$graphsdir/.htaccess" => { perms => WS_SERVE, contents => <<EOT
 # Allow access to .png and .gif files.
 <FilesMatch (\\.gif|\\.png)\$>
-  Allow from all
+  <IfModule mod_version.c>
+    <IfVersion < 2.4>
+      Allow from all
+    </IfVersion>
+    <IfVersion >= 2.4>
+      Require all granted
+    </IfVersion>
+  </IfModule>
+  <IfModule !mod_version.c>
+    Allow from all
+  </IfModule>
 </FilesMatch>
 
 # And no directory listings, either.
-Deny from all
+<IfModule mod_version.c>
+  <IfVersion < 2.4>
+    Deny from all
+  </IfVersion>
+  <IfVersion >= 2.4>
+    Require all denied
+  </IfVersion>
+</IfModule>
+<IfModule !mod_version.c>
+  Deny from all
+</IfModule>
 EOT
         },
 
@@ -342,17 +372,49 @@ EOT
 # if research.att.com ever changes their IP, or if you use a different
 # webdot server, you'll need to edit this
 <FilesMatch \\.dot\$>
-  Allow from 192.20.225.0/24
-  Deny from all
+  <IfModule mod_version.c>
+    <IfVersion < 2.4>
+      Allow from 192.20.225.0/24
+      Deny from all
+    </IfVersion>
+    <IfVersion >= 2.4>
+      Require ip 192.20.225.0/24
+      Require all denied
+    </IfVersion>
+  </IfModule>
+  <IfModule !mod_version.c>
+    Allow from 192.20.225.0/24
+    Deny from all
+  </IfModule>
 </FilesMatch>
 
 # Allow access to .png files created by a local copy of 'dot'
 <FilesMatch \\.png\$>
-  Allow from all
+  <IfModule mod_version.c>
+    <IfVersion < 2.4>
+      Allow from all
+    </IfVersion>
+    <IfVersion >= 2.4>
+      Require all granted
+    </IfVersion>
+  </IfModule>
+  <IfModule !mod_version.c>
+    Allow from all
+  </IfModule>
 </FilesMatch>
 
 # And no directory listings, either.
-Deny from all
+<IfModule mod_version.c>
+  <IfVersion < 2.4>
+    Deny from all
+  </IfVersion>
+  <IfVersion >= 2.4>
+    Require all denied
+  </IfVersion>
+</IfModule>
+<IfModule !mod_version.c>
+  Deny from all
+</IfModule>
 EOT
         },
     );
@@ -574,7 +636,7 @@ sub _update_old_charts {
                  ($in_file =~ /\.orig$/i));
 
         rename("$in_file", "$in_file.orig") or next;
-        open(IN, "$in_file.orig") or next;
+        open(IN, "<", "$in_file.orig") or next;
         open(OUT, '>', $in_file) or next;
 
         # Fields in the header
diff --git a/Bugzilla/Install/Localconfig.pm b/Bugzilla/Install/Localconfig.pm
index 4f1579c86..881f6c956 100644
--- a/Bugzilla/Install/Localconfig.pm
+++ b/Bugzilla/Install/Localconfig.pm
@@ -205,14 +205,20 @@ sub update_localconfig {
         # a 256-character string for site_wide_secret.
         $value = undef if ($name eq 'site_wide_secret' and defined $value
                            and length($value) == 256);
-        
+
         if (!defined $value) {
-            push(@new_vars, $name);
             $var->{default} = &{$var->{default}} if ref($var->{default}) eq 'CODE';
             if (exists $answer->{$name}) {
                 $localconfig->{$name} = $answer->{$name};
             }
             else {
+                # If the user did not supply an answers file, then they get
+                # notified about every variable that gets added. If there was
+                # an answer file, then we don't notify about site_wide_secret
+                # because we assume the intent was to auto-generate it anyway.
+                if (!scalar(keys %$answer) || $name ne 'site_wide_secret') {
+                    push(@new_vars, $name);
+                }
                 $localconfig->{$name} = $var->{default};
             }
         }
diff --git a/Bugzilla/Install/Requirements.pm b/Bugzilla/Install/Requirements.pm
index e2dc50a8f..16a06d5eb 100644
--- a/Bugzilla/Install/Requirements.pm
+++ b/Bugzilla/Install/Requirements.pm
@@ -129,10 +129,12 @@ sub REQUIRED_MODULES {
     },
     # 2.22 fixes various problems related to UTF8 strings in hash keys,
     # as well as line endings on Windows.
+    # 2.28-3.007 are broken, see https://bugzilla.mozilla.org/show_bug.cgi?id=1560873
     {
         package => 'Template-Toolkit',
         module  => 'Template',
-        version => '2.22'
+        version => '2.22',
+        blacklist => ['^2.2[89]$', '^3.00[0-7]$']
     },
     # 2.04 implement the "Test" method (to write to data/mailer.testfile).
     {
@@ -141,6 +143,11 @@ sub REQUIRED_MODULES {
         version => ON_WINDOWS ? '2.16' : '2.04',
         blacklist => ['^2\.196$']
     },
+    {
+        package => 'Email-Address',
+        module  => 'Email::Address',
+        version => 0,
+    },
     {
         package => 'Email-MIME',
         module  => 'Email::MIME',
@@ -168,7 +175,8 @@ sub REQUIRED_MODULES {
     );
 
     if (ON_WINDOWS) {
-        push(@modules, {
+        push(@modules,
+        {
             package => 'Win32',
             module  => 'Win32',
             # 0.35 fixes a memory leak in GetOSVersion, which we use.
@@ -179,7 +187,14 @@ sub REQUIRED_MODULES {
             module  => 'Win32::API',
             # 0.55 fixes a bug with char* that might affect Bugzilla::RNG.
             version => '0.55',
-        });
+        },
+        {
+            package => 'DateTime-TimeZone-Local-Win32',
+            module  => 'DateTime::TimeZone::Local::Win32',
+            # We require DateTime::TimeZone 0.79, so this version must match.
+            version => '0.79',
+        }
+        );
     }
 
     my $extra_modules = _get_extension_requirements('REQUIRED_MODULES');
@@ -368,6 +383,12 @@ sub OPTIONAL_MODULES {
         version => 0,
         feature => ['jobqueue'],
     },
+    {
+        package => 'File-Slurp',
+        module  => 'File::Slurp',
+        version => '9999.13',
+        feature => ['jobqueue'],
+    },
 
     # mod_perl
     {
diff --git a/Bugzilla/Mailer.pm b/Bugzilla/Mailer.pm
index 64640150b..1149eb0ad 100644
--- a/Bugzilla/Mailer.pm
+++ b/Bugzilla/Mailer.pm
@@ -19,8 +19,6 @@ use Bugzilla::Util;
 
 use Date::Format qw(time2str);
 
-use Encode qw(encode);
-use Encode::MIME::Header;
 use Email::Address;
 use Email::MIME;
 # Return::Value 1.666002 pollutes the error log with warnings about this
@@ -71,22 +69,12 @@ sub MessageToMTA {
     # 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
+    # Encode the headers correctly.
     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;
+        map { utf8::decode($_) if defined($_) && !utf8::is_utf8($_) } @values;
 
-            my $encoded = encode('MIME-Q', $value);
-            $email->header_set($header, $encoded);
-        }
+        $email->header_str_set($header, @values);
     }
 
     my $from = $email->header('From');
diff --git a/Bugzilla/Search.pm b/Bugzilla/Search.pm
index f900b3474..d67df03dd 100644
--- a/Bugzilla/Search.pm
+++ b/Bugzilla/Search.pm
@@ -31,7 +31,7 @@ use Date::Format;
 use Date::Parse;
 use Scalar::Util qw(blessed);
 use List::MoreUtils qw(all firstidx part uniq);
-use POSIX qw(INT_MAX);
+use POSIX qw(INT_MAX floor);
 use Storable qw(dclone);
 use Time::HiRes qw(gettimeofday tv_interval);
 
@@ -222,6 +222,9 @@ use constant OPERATOR_FIELD_OVERRIDE => {
     assigned_to => {
         _non_changed => \&_user_nonchanged,
     },
+    assigned_to_realname => {
+        _non_changed => \&_user_nonchanged,
+    },
     cc => {
         _non_changed => \&_user_nonchanged,
     },
@@ -231,6 +234,9 @@ use constant OPERATOR_FIELD_OVERRIDE => {
     reporter => {
         _non_changed => \&_user_nonchanged,
     },
+    reporter_realname => {
+        _non_changed => \&_user_nonchanged,
+    },
     'requestees.login_name' => {
         _non_changed => \&_user_nonchanged,
     },
@@ -240,7 +246,10 @@ use constant OPERATOR_FIELD_OVERRIDE => {
     qa_contact => {
         _non_changed => \&_user_nonchanged,
     },
-    
+    qa_contact_realname => {
+        _non_changed => \&_user_nonchanged,
+    },
+
     # General Bug Fields
     alias        => { _non_changed => \&_nullable },
     'attach_data.thedata' => MULTI_SELECT_OVERRIDE,
@@ -318,20 +327,29 @@ use constant OPERATOR_FIELD_OVERRIDE => {
 
 # These are fields where special action is taken depending on the
 # *value* passed in to the chart, sometimes.
-use constant SPECIAL_PARSING => {
-    # Pronoun Fields (Ones that can accept %user%, etc.)
-    assigned_to => \&_contact_pronoun,
-    cc          => \&_contact_pronoun,
-    commenter   => \&_contact_pronoun,
-    qa_contact  => \&_contact_pronoun,
-    reporter    => \&_contact_pronoun,
-    'setters.login_name' => \&_contact_pronoun,
-    'requestees.login_name' => \&_contact_pronoun,
-
-    # Date Fields that accept the 1d, 1w, 1m, 1y, etc. format.
-    creation_ts => \&_timestamp_translate,
-    deadline    => \&_timestamp_translate,
-    delta_ts    => \&_timestamp_translate,
+# This is a sub because custom fields are dynamic
+sub SPECIAL_PARSING {
+    my $map = {
+        # Pronoun Fields (Ones that can accept %user%, etc.)
+        assigned_to => \&_contact_pronoun,
+        cc          => \&_contact_pronoun,
+        commenter   => \&_contact_pronoun,
+        qa_contact  => \&_contact_pronoun,
+        reporter    => \&_contact_pronoun,
+        'setters.login_name' => \&_contact_pronoun,
+        'requestees.login_name' => \&_contact_pronoun,
+
+        # Date Fields that accept the 1d, 1w, 1m, 1y, etc. format.
+        creation_ts => \&_timestamp_translate,
+        deadline    => \&_timestamp_translate,
+        delta_ts    => \&_timestamp_translate,
+    };
+    foreach my $field (Bugzilla->active_custom_fields) {
+        if ($field->type == FIELD_TYPE_DATETIME) {
+            $map->{$field->name} = \&_timestamp_translate;
+        }
+    }
+    return $map;
 };
 
 # Information about fields that represent "users", used by _user_nonchanged.
@@ -520,9 +538,6 @@ sub COLUMNS {
     # of short_short_desc.)
     my %columns = (
         relevance            => { title => 'Relevance'  },
-        assigned_to_realname => { title => 'Assignee'   },
-        reporter_realname    => { title => 'Reporter'   },
-        qa_contact_realname  => { title => 'QA Contact' },
     );
 
     # Next we define columns that have special SQL instead of just something
@@ -575,7 +590,7 @@ sub COLUMNS {
              $sql = $dbh->sql_string_until($sql, $dbh->quote('@'));
         }
         $special_sql{$col} = $sql;
-        $columns{"${col}_realname"}->{name} = "map_${col}.realname";
+        $special_sql{"${col}_realname"} = "map_${col}.realname";
     }
 
     foreach my $col (@id_fields) {
@@ -1968,6 +1983,13 @@ sub _quote_unless_numeric {
 
 sub build_subselect {
     my ($outer, $inner, $table, $cond, $negate) = @_;
+    if ($table =~ /\battach_data\b/) {
+        # It takes a long time to scan the whole attach_data table
+        # unconditionally, so we return the subselect and let the DB optimizer
+        # restrict the search based on other search criteria.
+        my $not = $negate ? "NOT" : "";
+        return "$outer $not IN (SELECT DISTINCT $inner FROM $table WHERE $cond)";
+    }
     # Execute subselects immediately to avoid dependent subqueries, which are
     # large performance hits on MySql
     my $q = "SELECT DISTINCT $inner FROM $table WHERE $cond";
@@ -2107,7 +2129,8 @@ sub SqlifyDate {
         }
         elsif ($unit eq 'm') {
             $month -= $amount;
-            while ($month<0) { $year--; $month += 12; }
+            $year += floor($month/12);
+            $month %= 12;
             if ($startof) {
                 return sprintf("%4d-%02d-01 00:00:00", $year+1900, $month+1);
             }
@@ -2283,6 +2306,20 @@ sub _user_nonchanged {
     if ($args->{value_is_id}) {
         $null_alternate = 0;
     }
+    elsif (substr($field, -9) eq '_realname') {
+        my $as = "name_${field}_$chart_id";
+        # For fields with periods in their name.
+        $as =~ s/\./_/;
+        my $join = {
+            table => 'profiles',
+            as    => $as,
+            from  => substr($args->{full_field}, 0, -9),
+            to    => 'userid',
+            join  => (!$is_in_other_table and !$is_nullable) ? 'INNER' : undef,
+        };
+        push(@$joins, $join);
+        $args->{full_field} = "$as.realname";
+    }
     else {
         my $as = "name_${field}_$chart_id";
         # For fields with periods in their name.
@@ -2297,7 +2334,7 @@ sub _user_nonchanged {
         push(@$joins, $join);
         $args->{full_field} = "$as.login_name";
     }
-    
+
     # We COALESCE fields that can be NULL, to make "not"-style operators
     # continue to work properly. For example, "qa_contact is not equal to bob"
     # should also show bugs where the qa_contact is NULL. With COALESCE,
@@ -2364,11 +2401,17 @@ sub _user_nonchanged {
 sub _long_desc_changedby {
     my ($self, $args) = @_;
     my ($chart_id, $joins, $value) = @$args{qw(chart_id joins value)};
-    
+
     my $table = "longdescs_$chart_id";
     push(@$joins, { table => 'longdescs', as => $table });
     my $user_id = $self->_get_user_id($value);
     $args->{term} = "$table.who = $user_id";
+
+    # If the user is not part of the insiders group, they cannot see
+    # private comments
+    if (!$self->_user->is_insider) {
+        $args->{term} .= " AND $table.isprivate = 0";
+    }
 }
 
 sub _long_desc_changedbefore_after {
@@ -2376,7 +2419,7 @@ sub _long_desc_changedbefore_after {
     my ($chart_id, $operator, $value, $joins) =
         @$args{qw(chart_id operator value joins)};
     my $dbh = Bugzilla->dbh;
-    
+
     my $sql_operator = ($operator =~ /before/) ? '<=' : '>=';
     my $table = "longdescs_$chart_id";
     my $sql_date = $dbh->quote(SqlifyDate($value));
diff --git a/Bugzilla/Send/Sendmail.pm b/Bugzilla/Send/Sendmail.pm
index 9513134f4..012cd6f28 100644
--- a/Bugzilla/Send/Sendmail.pm
+++ b/Bugzilla/Send/Sendmail.pm
@@ -29,7 +29,7 @@ sub send {
 
     my $pipe = gensym;
 
-    open($pipe, "| $mailer -t -oi @args")
+    open($pipe, "|-", "$mailer -t -oi @args")
         || return failure "Error executing $mailer: $!";
     print($pipe $message->as_string)
         || return failure "Error printing via pipe to $mailer: $!";
diff --git a/Bugzilla/Template.pm b/Bugzilla/Template.pm
index c0072831a..b9cbfcce0 100644
--- a/Bugzilla/Template.pm
+++ b/Bugzilla/Template.pm
@@ -631,6 +631,8 @@ sub create {
                 $var =~ s/([\\\'\"\/])/\\$1/g;
                 $var =~ s/\n/\\n/g;
                 $var =~ s/\r/\\r/g;
+                $var =~ s/\x{2028}/\\u2028/g; # unicode line separator
+                $var =~ s/\x{2029}/\\u2029/g; # unicode paragraph separator
                 $var =~ s/\@/\\x40/g; # anti-spam for email addresses
                 $var =~ s/</\\x3c/g;
                 $var =~ s/>/\\x3e/g;
@@ -712,9 +714,15 @@ 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 either "=", "+", "-" or "@", it is preceded
+            # by a space to prevent stupid formula execution from Excel & co.
             csv => sub
             {
                 my ($var) = @_;
+                $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\"";
diff --git a/Bugzilla/Update.pm b/Bugzilla/Update.pm
index 29133ecce..71c0dd9cd 100644
--- a/Bugzilla/Update.pm
+++ b/Bugzilla/Update.pm
@@ -47,7 +47,8 @@ sub get_notifications {
             'latest_ver' => $branch->{'att'}->{'vid'},
             'status'     => $branch->{'att'}->{'status'},
             'url'        => $branch->{'att'}->{'url'},
-            'date'       => $branch->{'att'}->{'date'}
+            'date'       => $branch->{'att'}->{'date'},
+            'eos_date'   => exists($branch->{'att'}->{'eos-date'}) ? $branch->{'att'}->{'eos-date'} : undef,
         };
         push(@releases, $release);
     }
@@ -66,6 +67,35 @@ sub get_notifications {
         }
     }
     elsif (Bugzilla->params->{'upgrade_notification'} eq 'latest_stable_release') {
+        # We want the latest stable version for the current branch.
+        # If we are running a development snapshot, we won't match anything.
+        my $branch_version = $current_version[0] . '.' . $current_version[1];
+
+        # We do a string comparison instead of a numerical one, because
+        # e.g. 2.2 == 2.20, but 2.2 ne 2.20 (and 2.2 is indeed much older).
+        @release = grep {$_->{'branch_ver'} eq $branch_version} @releases;
+
+        # If the branch has an end-of-support date listed, we should
+        # strongly suggest to upgrade to the latest stable release
+        # available.
+        if (scalar(@release) && $release[0]->{'status'} ne 'closed'
+            && defined($release[0]->{'eos_date'})) {
+            my $eos_date = $release[0]->{'eos_date'};
+            @release = grep {$_->{'status'} eq 'stable'} @releases;
+            return {'data' => $release[0],
+                    'branch_version' => $branch_version,
+                    'eos_date' => $eos_date};
+        };
+
+        # If the branch is now closed, we should strongly suggest
+        # to upgrade to the latest stable release available.
+        if (scalar(@release) && $release[0]->{'status'} eq 'closed') {
+            @release = grep {$_->{'status'} eq 'stable'} @releases;
+            return {'data' => $release[0], 'deprecated' => $branch_version};
+        }
+
+        # If we get here, then we want to recommend the lastest stable
+        # release without any other messages.
         @release = grep {$_->{'status'} eq 'stable'} @releases;
     }
     elsif (Bugzilla->params->{'upgrade_notification'} eq 'stable_branch_release') {
@@ -77,6 +107,18 @@ sub get_notifications {
         # e.g. 2.2 == 2.20, but 2.2 ne 2.20 (and 2.2 is indeed much older).
         @release = grep {$_->{'branch_ver'} eq $branch_version} @releases;
 
+        # If the branch has an end-of-support date listed, we should
+        # strongly suggest to upgrade to the latest stable release
+        # available.
+       if (scalar(@release) && $release[0]->{'status'} ne 'closed'
+            && defined($release[0]->{'eos_date'})) {
+            my $eos_date = $release[0]->{'eos_date'};
+            @release = grep {$_->{'status'} eq 'stable'} @releases;
+            return {'data' => $release[0],
+                    'branch_version' => $branch_version,
+                    'eos_date' => $eos_date}
+        };
+
         # If the branch is now closed, we should strongly suggest
         # to upgrade to the latest stable release available.
         if (scalar(@release) && $release[0]->{'status'} eq 'closed') {
diff --git a/Bugzilla/User.pm b/Bugzilla/User.pm
index aa94e44a1..a6a47fc29 100644
--- a/Bugzilla/User.pm
+++ b/Bugzilla/User.pm
@@ -28,7 +28,6 @@ use Bugzilla::Group;
 use DateTime::TimeZone;
 use List::Util qw(max);
 use Scalar::Util qw(blessed);
-use Storable qw(dclone);
 use URI;
 use URI::QueryParam;
 
@@ -123,7 +122,7 @@ sub new {
     my $class = ref($invocant) || $invocant;
     my ($param) = @_;
 
-    my $user = DEFAULT_USER;
+    my $user = { %{ DEFAULT_USER() } };
     bless ($user, $class);
     return $user unless $param;
 
@@ -133,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 {
@@ -141,7 +152,7 @@ sub super_user {
     my $class = ref($invocant) || $invocant;
     my ($param) = @_;
 
-    my $user = dclone(DEFAULT_USER);
+    my $user = { %{ DEFAULT_USER() } };
     $user->{groups} = [Bugzilla::Group->get_all];
     $user->{bless_groups} = [Bugzilla::Group->get_all];
     bless $user, $class;
@@ -247,8 +258,9 @@ sub _check_is_enabled {
 # Mutators
 ################################################################################
 
-sub set_disable_mail { $_[0]->set('disable_mail', $_[1]); }
-sub set_extern_id    { $_[0]->set('extern_id', $_[1]); }
+sub set_disable_mail  { $_[0]->set('disable_mail', $_[1]); }
+sub set_email_enabled { $_[0]->set('disable_mail', !$_[1]); }
+sub set_extern_id     { $_[0]->set('extern_id', $_[1]); }
 
 sub set_login {
     my ($self, $login) = @_;
@@ -2601,6 +2613,10 @@ i.e. if the 'insidergroup' parameter is set and the user belongs to this group.
 Returns true if the user is a global watcher,
 i.e. if the 'globalwatchers' parameter contains the user.
 
+=item C<set_email_enabled>
+
+C<bool> - Sets C<disable_mail> to the inverse of the boolean provided.
+
 =back
 
 =head1 CLASS FUNCTIONS
diff --git a/Bugzilla/UserAgent.pm b/Bugzilla/UserAgent.pm
index 9f98d597c..5615f86ee 100644
--- a/Bugzilla/UserAgent.pm
+++ b/Bugzilla/UserAgent.pm
@@ -103,6 +103,7 @@ use constant OS_MAP => (
     qr/\(.*Android.*\)/ => ["Android"],
     # Windows
     qr/\(.*Windows XP.*\)/ => ["Windows XP"],
+    qr/\(.*Windows NT 6\.4.*\)/ => ["Windows 10"],
     qr/\(.*Windows NT 6\.3.*\)/ => ["Windows 8.1"],
     qr/\(.*Windows NT 6\.2.*\)/ => ["Windows 8"],
     qr/\(.*Windows NT 6\.1.*\)/ => ["Windows 7"],
diff --git a/Bugzilla/Util.pm b/Bugzilla/Util.pm
index 625fee963..527bae85a 100644
--- a/Bugzilla/Util.pm
+++ b/Bugzilla/Util.pm
@@ -569,10 +569,14 @@ sub datetime_from {
 
     return undef if !@time;
 
-    # strptime() counts years from 1900, and months from 0 (January).
-    # We have to fix both values.
+    # strptime() counts years from 1900, except if they are older than 1901
+    # in which case it returns the full year (so 1890 -> 1890, but 1984 -> 84,
+    # and 3790 -> 1890). We make a guess and assume that 1100 <= year < 3000.
+    $time[5] += 1900 if $time[5] < 1100;
+
     my %args = (
-        year   => $time[5] + 1900,
+        year   => $time[5],
+        # Months start from 0 (January).
         month  => $time[4] + 1,
         day    => $time[3],
         hour   => $time[2],
@@ -628,13 +632,13 @@ sub bz_crypt {
         $algorithm = $1;
     }
 
+    # Wide characters cause crypt and Digest to die.
+    if (Bugzilla->params->{'utf8'}) {
+        utf8::encode($password) if utf8::is_utf8($password);
+    }
+
     my $crypted_password;
     if (!$algorithm) {
-        # Wide characters cause crypt to die
-        if (Bugzilla->params->{'utf8'}) {
-            utf8::encode($password) if utf8::is_utf8($password);
-        }
-    
         # Crypt the password.
         $crypted_password = crypt($password, $salt);
 
@@ -677,12 +681,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 {
diff --git a/Bugzilla/WebService.pm b/Bugzilla/WebService.pm
index 2a0e8890f..5646e381d 100644
--- a/Bugzilla/WebService.pm
+++ b/Bugzilla/WebService.pm
@@ -23,6 +23,10 @@ use constant LOGIN_EXEMPT => { };
 # Methods that can modify data MUST not be listed here.
 use constant READ_ONLY => ();
 
+# Whitelist of methods that a client is allowed to access when making
+# an API call.
+use constant PUBLIC_METHODS => ();
+
 sub login_exempt {
     my ($class, $method) = @_;
     return $class->LOGIN_EXEMPT->{$method};
@@ -269,7 +273,7 @@ hashes.
 
 Some RPC calls support specifying sub fields. If an RPC call states that
 it support sub field restrictions, you can restrict what information is
-returned within the first field. For example, if you call Products.get
+returned within the first field. For example, if you call Product.get
 with an include_fields of components.name, then only the component name
 would be returned (and nothing else). You can include the main field,
 and exclude a sub field.
diff --git a/Bugzilla/WebService/Bug.pm b/Bugzilla/WebService/Bug.pm
index 670d234ce..006925994 100644
--- a/Bugzilla/WebService/Bug.pm
+++ b/Bugzilla/WebService/Bug.pm
@@ -49,6 +49,26 @@ use constant READ_ONLY => qw(
     search
 );
 
+use constant PUBLIC_METHODS => qw(
+    add_attachment
+    add_comment
+    attachments
+    comments
+    create
+    fields
+    get
+    get_bugs
+    get_history
+    history
+    legal_values
+    possible_duplicates
+    render_comment
+    search
+    update
+    update_see_also
+    update_tags
+);
+
 ######################################################
 # Add aliases here for old method name compatibility #
 ######################################################
@@ -707,19 +727,10 @@ sub add_comment {
     # Append comment
     $bug->add_comment($comment, { isprivate => $params->{is_private},
                                   work_time => $params->{work_time} });
-    
-    # Capture the call to bug->update (which creates the new comment) in 
-    # a transaction so we're sure to get the correct comment_id.
-    
-    my $dbh = Bugzilla->dbh;
-    $dbh->bz_start_transaction();
-    
     $bug->update();
-    
-    my $new_comment_id = $dbh->bz_last_key('longdescs', 'comment_id');
-    
-    $dbh->bz_commit_transaction();
-    
+
+    my $new_comment_id = $bug->{added_comments}[0]->id;
+
     # Send mail.
     Bugzilla::BugMail::Send($bug->bug_id, { changer => $user });
 
diff --git a/Bugzilla/WebService/Bugzilla.pm b/Bugzilla/WebService/Bugzilla.pm
index 9513e4183..f6d5fc5f9 100644
--- a/Bugzilla/WebService/Bugzilla.pm
+++ b/Bugzilla/WebService/Bugzilla.pm
@@ -31,6 +31,15 @@ use constant READ_ONLY => qw(
     version
 );
 
+use constant PUBLIC_METHODS => qw(
+    extensions
+    last_audit_time
+    parameters
+    time
+    timezone
+    version
+);
+
 # Logged-out users do not need to know more than that.
 use constant PARAMETERS_LOGGED_OUT => qw(
     maintainer
@@ -145,7 +154,7 @@ sub last_audit_time {
 
 sub parameters {
     my ($self, $args) = @_;
-    my $user = Bugzilla->login();
+    my $user = Bugzilla->login(LOGIN_OPTIONAL);
     my $params = Bugzilla->params;
     $args ||= {};
 
diff --git a/Bugzilla/WebService/Classification.pm b/Bugzilla/WebService/Classification.pm
index 753b52638..f2c3ec51e 100644
--- a/Bugzilla/WebService/Classification.pm
+++ b/Bugzilla/WebService/Classification.pm
@@ -19,6 +19,10 @@ use constant READ_ONLY => qw(
     get
 );
 
+use constant PUBLIC_METHODS => qw(
+    get
+);
+
 sub get {
     my ($self, $params) = validate(@_, 'names', 'ids');
 
diff --git a/Bugzilla/WebService/Group.pm b/Bugzilla/WebService/Group.pm
index d7506aa3d..72c948aa4 100644
--- a/Bugzilla/WebService/Group.pm
+++ b/Bugzilla/WebService/Group.pm
@@ -13,6 +13,11 @@ use Bugzilla::Constants;
 use Bugzilla::Error;
 use Bugzilla::WebService::Util qw(validate translate params_to_objects);
 
+use constant PUBLIC_METHODS => qw(
+    create
+    update
+);
+
 use constant MAPPED_RETURNS => {
     userregexp => 'user_regexp',
     isactive => 'is_active'
diff --git a/Bugzilla/WebService/Product.pm b/Bugzilla/WebService/Product.pm
index bb61ac434..e383cb515 100644
--- a/Bugzilla/WebService/Product.pm
+++ b/Bugzilla/WebService/Product.pm
@@ -23,6 +23,16 @@ use constant READ_ONLY => qw(
     get_selectable_products
 );
 
+use constant PUBLIC_METHODS => qw(
+    create
+    get
+    get_accessible_products
+    get_enterable_products
+    get_products
+    get_selectable_products
+    update
+);
+
 use constant MAPPED_FIELDS => {
     has_unconfirmed => 'allows_unconfirmed',
     is_open => 'is_active',
diff --git a/Bugzilla/WebService/Server/JSONRPC.pm b/Bugzilla/WebService/Server/JSONRPC.pm
index 9f5710439..0a0afd400 100644
--- a/Bugzilla/WebService/Server/JSONRPC.pm
+++ b/Bugzilla/WebService/Server/JSONRPC.pm
@@ -28,6 +28,7 @@ use Bugzilla::Util qw(correct_urlbase trim disable_utf8);
 
 use HTTP::Message;
 use MIME::Base64 qw(decode_base64 encode_base64);
+use List::MoreUtils qw(none);
 
 #####################################
 # Public JSON::RPC Method Overrides #
@@ -77,8 +78,9 @@ sub response {
     # Implement JSONP.
     if (my $callback = $self->_bz_callback) {
         my $content = $response->content;
-        $response->content("$callback($content)");
-
+        # Prepend the JSONP response with /**/ in order to protect
+        # against possible encoding attacks (e.g., affecting Flash).
+        $response->content("/**/$callback($content)");
     }
 
     # Use $cgi->header properly instead of just printing text directly.
@@ -377,6 +379,11 @@ sub _argument_type_check {
         }
     }
 
+    # Only allowed methods to be used from our whitelist
+    if (none { $_ eq $method} $pkg->PUBLIC_METHODS) {
+        ThrowCodeError('unknown_method', { method => $self->_bz_method_name });
+    }
+
     # This is the best time to do login checks.
     $self->handle_login();
 
diff --git a/Bugzilla/WebService/Server/XMLRPC.pm b/Bugzilla/WebService/Server/XMLRPC.pm
index 03590bd1c..266376aa0 100644
--- a/Bugzilla/WebService/Server/XMLRPC.pm
+++ b/Bugzilla/WebService/Server/XMLRPC.pm
@@ -17,6 +17,9 @@ if ($ENV{MOD_PERL}) {
 }
 
 use Bugzilla::WebService::Constants;
+use Bugzilla::Error;
+
+use List::MoreUtils qw(none);
 
 # Allow WebService methods to call XMLRPC::Lite's type method directly
 BEGIN {
@@ -65,6 +68,14 @@ sub handle_login {
     my ($self, $classes, $action, $uri, $method) = @_;
     my $class = $classes->{$uri};
     my $full_method = $uri . "." . $method;
+    # Only allowed methods to be used from the module's whitelist
+    my $file = $class;
+    $file =~ s{::}{/}g;
+    $file .= ".pm";
+    require $file;
+    if (none { $_ eq $method } $class->PUBLIC_METHODS) {
+        ThrowCodeError('unknown_method', { method => $full_method });
+    }
     $self->SUPER::handle_login($class, $method, $full_method);
     return;
 }
@@ -85,6 +96,15 @@ use Bugzilla::WebService::Constants qw(XMLRPC_CONTENT_TYPE_WHITELIST);
 use Bugzilla::WebService::Util qw(fix_credentials);
 use Scalar::Util qw(tainted);
 
+sub new {
+    my $self = shift->SUPER::new(@_);
+    # Initialise XML::Parser to not expand references to entities, to prevent DoS
+    require XML::Parser;
+    my $parser = XML::Parser->new( NoExpand => 1, Handlers => { Default => sub {} } );
+    $self->{_parser}->parser($parser, $parser);
+    return $self;
+}
+
 sub deserialize {
     my $self = shift;
 
diff --git a/Bugzilla/WebService/User.pm b/Bugzilla/WebService/User.pm
index b865e114c..469e5c5cd 100644
--- a/Bugzilla/WebService/User.pm
+++ b/Bugzilla/WebService/User.pm
@@ -32,18 +32,25 @@ use constant READ_ONLY => qw(
     get
 );
 
+use constant PUBLIC_METHODS => qw(
+    create
+    get
+    login
+    logout
+    offer_account_by_email
+    update
+);
+
 use constant MAPPED_FIELDS => {
     email => 'login',
     full_name => 'name',
     login_denied_text => 'disabledtext',
-    email_enabled => 'disable_mail'
 };
 
 use constant MAPPED_RETURNS => {
     login_name => 'email',
     realname => 'full_name',
     disabledtext => 'login_denied_text',
-    disable_mail => 'email_enabled'
 };
 
 ##############
@@ -53,27 +60,20 @@ use constant MAPPED_RETURNS => {
 sub login {
     my ($self, $params) = @_;
 
+    # Check to see if we are already logged in
+    my $user = Bugzilla->user;
+    if ($user->id) {
+        return $self->_login_to_hash($user);
+    }
+
     # Username and password params are required 
     foreach my $param ("login", "password") {
-        defined $params->{$param} 
+        (defined $params->{$param} || defined $params->{'Bugzilla_' . $param})
             || ThrowCodeError('param_required', { param => $param });
     }
 
-    # Make sure the CGI user info class works if necessary.
-    my $input_params = Bugzilla->input_params;
-    $input_params->{'Bugzilla_login'} =  $params->{login};
-    $input_params->{'Bugzilla_password'} = $params->{password};
-    $input_params->{'Bugzilla_restrictlogin'} = $params->{restrict_login};
-
-    my $user = Bugzilla->login();
-
-    my $result = { id => $self->type('int', $user->id) };
-
-    if ($user->{_login_token}) {
-        $result->{'token'} = $user->id . "-" . $user->{_login_token};
-    }
-
-    return $result;
+    $user = Bugzilla->login();
+    return $self->_login_to_hash($user);
 }
 
 sub logout {
@@ -384,6 +384,15 @@ sub _report_to_hash {
     return $item;
 }
 
+sub _login_to_hash {
+    my ($self, $user) = @_;
+    my $item = { id => $self->type('int', $user->id) };
+    if ($user->{_login_token}) {
+        $item->{'token'} = $user->id . "-" . $user->{_login_token};
+    }
+    return $item;
+}
+
 1;
 
 __END__
diff --git a/Bugzilla/WebService/Util.pm b/Bugzilla/WebService/Util.pm
index 195de79e4..c7d63b336 100644
--- a/Bugzilla/WebService/Util.pm
+++ b/Bugzilla/WebService/Util.pm
@@ -150,13 +150,13 @@ sub fix_credentials {
     # even if not calling User.login. We also do not delete them as
     # User.login requires "login" and "password".
     if (exists $params->{'login'} && exists $params->{'password'}) {
-        $params->{'Bugzilla_login'}    = $params->{'login'};
-        $params->{'Bugzilla_password'} = $params->{'password'};
+        $params->{'Bugzilla_login'}    = delete $params->{'login'};
+        $params->{'Bugzilla_password'} = delete $params->{'password'};
     }
     # Allow user to pass token=12345678 as a convenience which becomes
     # "Bugzilla_token" which is what the auth code looks for.
     if (exists $params->{'token'}) {
-        $params->{'Bugzilla_token'} = $params->{'token'};
+        $params->{'Bugzilla_token'} = delete $params->{'token'};
     }
 }
 
diff --git a/Build.PL b/Build.PL
new file mode 100644
index 000000000..024a56024
--- /dev/null
+++ b/Build.PL
@@ -0,0 +1,61 @@
+#!/usr/bin/perl
+# 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.
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use FindBin qw($RealBin);
+use lib ($RealBin, "$RealBin/lib");
+
+use Module::Build 0.36_14;
+
+use Bugzilla::Install::Requirements qw(REQUIRED_MODULES OPTIONAL_MODULES);
+use Bugzilla::Constants qw(BUGZILLA_VERSION);
+
+sub requires {
+    my $requirements = REQUIRED_MODULES();
+    my $hrequires = {};
+    foreach my $module (@$requirements) {
+        $hrequires->{$module->{module}} = $module->{version};
+    }
+    return $hrequires;
+};
+
+sub build_requires {
+    return requires();
+}
+
+sub recommends {
+    my $recommends = OPTIONAL_MODULES();
+    my @blacklist = ('Apache-SizeLimit', 'mod_perl'); # Does not compile properly on Travis
+    my $hrecommends = {};
+    foreach my $module (@$recommends) {
+        next if grep($_ eq $module->{package}, @blacklist);
+        $hrecommends->{$module->{module}} = $module->{version};
+    }
+    return $hrecommends;
+}
+
+my $build = Module::Build->new(
+    module_name        => 'Bugzilla',
+    dist_abstract      => <<END,
+Bugzilla is a free bug-tracking system that is developed by an active
+community of volunteers. You can install and use it without having to
+pay any license fee.
+END
+    dist_version_from  => 'Bugzilla/Constants.pm',
+    dist_version       => BUGZILLA_VERSION,
+    requires           => requires(),
+    recommends         => recommends(),
+    license            => 'Mozilla_2_0',
+    create_readme      => 0,
+    create_makefile_pl => 0
+);
+
+$build->create_build_script;
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 000000000..14e2f777f
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,373 @@
+Mozilla Public License Version 2.0
+==================================
+
+1. Definitions
+--------------
+
+1.1. "Contributor"
+    means each individual or legal entity that creates, contributes to
+    the creation of, or owns Covered Software.
+
+1.2. "Contributor Version"
+    means the combination of the Contributions of others (if any) used
+    by a Contributor and that particular Contributor's Contribution.
+
+1.3. "Contribution"
+    means Covered Software of a particular Contributor.
+
+1.4. "Covered Software"
+    means Source Code Form to which the initial Contributor has attached
+    the notice in Exhibit A, the Executable Form of such Source Code
+    Form, and Modifications of such Source Code Form, in each case
+    including portions thereof.
+
+1.5. "Incompatible With Secondary Licenses"
+    means
+
+    (a) that the initial Contributor has attached the notice described
+        in Exhibit B to the Covered Software; or
+
+    (b) that the Covered Software was made available under the terms of
+        version 1.1 or earlier of the License, but not also under the
+        terms of a Secondary License.
+
+1.6. "Executable Form"
+    means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+    means a work that combines Covered Software with other material, in 
+    a separate file or files, that is not Covered Software.
+
+1.8. "License"
+    means this document.
+
+1.9. "Licensable"
+    means having the right to grant, to the maximum extent possible,
+    whether at the time of the initial grant or subsequently, any and
+    all of the rights conveyed by this License.
+
+1.10. "Modifications"
+    means any of the following:
+
+    (a) any file in Source Code Form that results from an addition to,
+        deletion from, or modification of the contents of Covered
+        Software; or
+
+    (b) any new file in Source Code Form that contains any Covered
+        Software.
+
+1.11. "Patent Claims" of a Contributor
+    means any patent claim(s), including without limitation, method,
+    process, and apparatus claims, in any patent Licensable by such
+    Contributor that would be infringed, but for the grant of the
+    License, by the making, using, selling, offering for sale, having
+    made, import, or transfer of either its Contributions or its
+    Contributor Version.
+
+1.12. "Secondary License"
+    means either the GNU General Public License, Version 2.0, the GNU
+    Lesser General Public License, Version 2.1, the GNU Affero General
+    Public License, Version 3.0, or any later versions of those
+    licenses.
+
+1.13. "Source Code Form"
+    means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+    means an individual or a legal entity exercising rights under this
+    License. For legal entities, "You" includes any entity that
+    controls, is controlled by, or is under common control with You. For
+    purposes of this definition, "control" means (a) the power, direct
+    or indirect, to cause the direction or management of such entity,
+    whether by contract or otherwise, or (b) ownership of more than
+    fifty percent (50%) of the outstanding shares or beneficial
+    ownership of such entity.
+
+2. License Grants and Conditions
+--------------------------------
+
+2.1. Grants
+
+Each Contributor hereby grants You a world-wide, royalty-free,
+non-exclusive license:
+
+(a) under intellectual property rights (other than patent or trademark)
+    Licensable by such Contributor to use, reproduce, make available,
+    modify, display, perform, distribute, and otherwise exploit its
+    Contributions, either on an unmodified basis, with Modifications, or
+    as part of a Larger Work; and
+
+(b) under Patent Claims of such Contributor to make, use, sell, offer
+    for sale, have made, import, and otherwise transfer either its
+    Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+The licenses granted in Section 2.1 with respect to any Contribution
+become effective for each Contribution on the date the Contributor first
+distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+The licenses granted in this Section 2 are the only rights granted under
+this License. No additional rights or licenses will be implied from the
+distribution or licensing of Covered Software under this License.
+Notwithstanding Section 2.1(b) above, no patent license is granted by a
+Contributor:
+
+(a) for any code that a Contributor has removed from Covered Software;
+    or
+
+(b) for infringements caused by: (i) Your and any other third party's
+    modifications of Covered Software, or (ii) the combination of its
+    Contributions with other software (except as part of its Contributor
+    Version); or
+
+(c) under Patent Claims infringed by Covered Software in the absence of
+    its Contributions.
+
+This License does not grant any rights in the trademarks, service marks,
+or logos of any Contributor (except as may be necessary to comply with
+the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+No Contributor makes additional grants as a result of Your choice to
+distribute the Covered Software under a subsequent version of this
+License (see Section 10.2) or under the terms of a Secondary License (if
+permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+Each Contributor represents that the Contributor believes its
+Contributions are its original creation(s) or it has sufficient rights
+to grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+This License is not intended to limit any rights You have under
+applicable copyright doctrines of fair use, fair dealing, or other
+equivalents.
+
+2.7. Conditions
+
+Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
+in Section 2.1.
+
+3. Responsibilities
+-------------------
+
+3.1. Distribution of Source Form
+
+All distribution of Covered Software in Source Code Form, including any
+Modifications that You create or to which You contribute, must be under
+the terms of this License. You must inform recipients that the Source
+Code Form of the Covered Software is governed by the terms of this
+License, and how they can obtain a copy of this License. You may not
+attempt to alter or restrict the recipients' rights in the Source Code
+Form.
+
+3.2. Distribution of Executable Form
+
+If You distribute Covered Software in Executable Form then:
+
+(a) such Covered Software must also be made available in Source Code
+    Form, as described in Section 3.1, and You must inform recipients of
+    the Executable Form how they can obtain a copy of such Source Code
+    Form by reasonable means in a timely manner, at a charge no more
+    than the cost of distribution to the recipient; and
+
+(b) You may distribute such Executable Form under the terms of this
+    License, or sublicense it under different terms, provided that the
+    license for the Executable Form does not attempt to limit or alter
+    the recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+You may create and distribute a Larger Work under terms of Your choice,
+provided that You also comply with the requirements of this License for
+the Covered Software. If the Larger Work is a combination of Covered
+Software with a work governed by one or more Secondary Licenses, and the
+Covered Software is not Incompatible With Secondary Licenses, this
+License permits You to additionally distribute such Covered Software
+under the terms of such Secondary License(s), so that the recipient of
+the Larger Work may, at their option, further distribute the Covered
+Software under the terms of either this License or such Secondary
+License(s).
+
+3.4. Notices
+
+You may not remove or alter the substance of any license notices
+(including copyright notices, patent notices, disclaimers of warranty,
+or limitations of liability) contained within the Source Code Form of
+the Covered Software, except that You may alter any license notices to
+the extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+You may choose to offer, and to charge a fee for, warranty, support,
+indemnity or liability obligations to one or more recipients of Covered
+Software. However, You may do so only on Your own behalf, and not on
+behalf of any Contributor. You must make it absolutely clear that any
+such warranty, support, indemnity, or liability obligation is offered by
+You alone, and You hereby agree to indemnify every Contributor for any
+liability incurred by such Contributor as a result of warranty, support,
+indemnity or liability terms You offer. You may include additional
+disclaimers of warranty and limitations of liability specific to any
+jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+---------------------------------------------------
+
+If it is impossible for You to comply with any of the terms of this
+License with respect to some or all of the Covered Software due to
+statute, judicial order, or regulation then You must: (a) comply with
+the terms of this License to the maximum extent possible; and (b)
+describe the limitations and the code they affect. Such description must
+be placed in a text file included with all distributions of the Covered
+Software under this License. Except to the extent prohibited by statute
+or regulation, such description must be sufficiently detailed for a
+recipient of ordinary skill to be able to understand it.
+
+5. Termination
+--------------
+
+5.1. The rights granted under this License will terminate automatically
+if You fail to comply with any of its terms. However, if You become
+compliant, then the rights granted under this License from a particular
+Contributor are reinstated (a) provisionally, unless and until such
+Contributor explicitly and finally terminates Your grants, and (b) on an
+ongoing basis, if such Contributor fails to notify You of the
+non-compliance by some reasonable means prior to 60 days after You have
+come back into compliance. Moreover, Your grants from a particular
+Contributor are reinstated on an ongoing basis if such Contributor
+notifies You of the non-compliance by some reasonable means, this is the
+first time You have received notice of non-compliance with this License
+from such Contributor, and You become compliant prior to 30 days after
+Your receipt of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+infringement claim (excluding declaratory judgment actions,
+counter-claims, and cross-claims) alleging that a Contributor Version
+directly or indirectly infringes any patent, then the rights granted to
+You by any and all Contributors for the Covered Software under Section
+2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all
+end user license agreements (excluding distributors and resellers) which
+have been validly granted by You or Your distributors under this License
+prior to termination shall survive termination.
+
+************************************************************************
+*                                                                      *
+*  6. Disclaimer of Warranty                                           *
+*  -------------------------                                           *
+*                                                                      *
+*  Covered Software is provided under this License on an "as is"       *
+*  basis, without warranty of any kind, either expressed, implied, or  *
+*  statutory, including, without limitation, warranties that the       *
+*  Covered Software is free of defects, merchantable, fit for a        *
+*  particular purpose or non-infringing. The entire risk as to the     *
+*  quality and performance of the Covered Software is with You.        *
+*  Should any Covered Software prove defective in any respect, You     *
+*  (not any Contributor) assume the cost of any necessary servicing,   *
+*  repair, or correction. This disclaimer of warranty constitutes an   *
+*  essential part of this License. No use of any Covered Software is   *
+*  authorized under this License except under this disclaimer.         *
+*                                                                      *
+************************************************************************
+
+************************************************************************
+*                                                                      *
+*  7. Limitation of Liability                                          *
+*  --------------------------                                          *
+*                                                                      *
+*  Under no circumstances and under no legal theory, whether tort      *
+*  (including negligence), contract, or otherwise, shall any           *
+*  Contributor, or anyone who distributes Covered Software as          *
+*  permitted above, be liable to You for any direct, indirect,         *
+*  special, incidental, or consequential damages of any character      *
+*  including, without limitation, damages for lost profits, loss of    *
+*  goodwill, work stoppage, computer failure or malfunction, or any    *
+*  and all other commercial damages or losses, even if such party      *
+*  shall have been informed of the possibility of such damages. This   *
+*  limitation of liability shall not apply to liability for death or   *
+*  personal injury resulting from such party's negligence to the       *
+*  extent applicable law prohibits such limitation. Some               *
+*  jurisdictions do not allow the exclusion or limitation of           *
+*  incidental or consequential damages, so this exclusion and          *
+*  limitation may not apply to You.                                    *
+*                                                                      *
+************************************************************************
+
+8. Litigation
+-------------
+
+Any litigation relating to this License may be brought only in the
+courts of a jurisdiction where the defendant maintains its principal
+place of business and such litigation shall be governed by laws of that
+jurisdiction, without reference to its conflict-of-law provisions.
+Nothing in this Section shall prevent a party's ability to bring
+cross-claims or counter-claims.
+
+9. Miscellaneous
+----------------
+
+This License represents the complete agreement concerning the subject
+matter hereof. If any provision of this License is held to be
+unenforceable, such provision shall be reformed only to the extent
+necessary to make it enforceable. Any law or regulation which provides
+that the language of a contract shall be construed against the drafter
+shall not be used to construe this License against a Contributor.
+
+10. Versions of the License
+---------------------------
+
+10.1. New Versions
+
+Mozilla Foundation is the license steward. Except as provided in Section
+10.3, no one other than the license steward has the right to modify or
+publish new versions of this License. Each version will be given a
+distinguishing version number.
+
+10.2. Effect of New Versions
+
+You may distribute the Covered Software under the terms of the version
+of the License under which You originally received the Covered Software,
+or under the terms of any subsequent version published by the license
+steward.
+
+10.3. Modified Versions
+
+If you create software not governed by this License, and you want to
+create a new license for such software, you may create and use a
+modified version of this License if you rename the license and remove
+any references to the name of the license steward (except to note that
+such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+Licenses
+
+If You choose to distribute Source Code Form that is Incompatible With
+Secondary Licenses under the terms of this version of the License, the
+notice described in Exhibit B of this License must be attached.
+
+Exhibit A - Source Code Form License Notice
+-------------------------------------------
+
+  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/.
+
+If it is not possible or desirable to put the notice in a particular
+file, then You may include the notice in a location (such as a LICENSE
+file in a relevant directory) where a recipient would be likely to look
+for such a notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+---------------------------------------------------------
+
+  This Source Code Form is "Incompatible With Secondary Licenses", as
+  defined by the Mozilla Public License, v. 2.0.
diff --git a/MANIFEST.SKIP b/MANIFEST.SKIP
new file mode 100644
index 000000000..69204e63f
--- /dev/null
+++ b/MANIFEST.SKIP
@@ -0,0 +1,53 @@
+# 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.
+
+#!start included /usr/share/perl5/ExtUtils/MANIFEST.SKIP
+# Avoid version control files.
+\B\.git\b
+\B\.bzr\b
+\B\.bzrignore\b
+\B\.gitignore\b
+\B\.gitrev\b
+\B\.patch\b
+
+# Avoid Makemaker generated and utility files.
+\bMANIFEST\.bak
+\bMakefile$
+\bblib/
+\bMakeMaker-\d
+\bpm_to_blib\.ts$
+\bpm_to_blib$
+\bblibdirs\.ts$         # 6.18 through 6.25 generated this
+
+# Avoid Module::Build generated and utility files.
+\bBuild$
+\b_build/
+
+# Avoid temp and backup files.
+~$
+\.old$
+\#$
+\b\.#
+\.bak$
+\.swp$
+
+#!end included /usr/share/perl5/ExtUtils/MANIFEST.SKIP
+
+# Avoid Module::Build generated and utility files.
+\bBuild$
+\bBuild.bat$
+\b_build
+\bBuild.COM$
+\bBUILD.COM$
+\bbuild.com$
+
+# Avoid archives of this distribution
+\bBugzilla-[\d\.\_]+
+
+# Bugzilla specific avoids
+\bdata\/\b
+\blocalconfig$
diff --git a/README b/README
index 041aebc13..e68afd8e4 100644
--- a/README
+++ b/README
@@ -1,92 +1,54 @@
-What is Bugzilla?
-=================
-Bugzilla is a free bug-tracking system that is developed by an active
-community of volunteers in the Mozilla community. You can install and
-use it without having to pay any license fee.
-
-Minimum requirements
-====================
-It can be installed on Windows, Mac OS X, Linux and other Unix flavors.
-Bugzilla is written in Perl, meaning that Perl must be installed on your system.
-You will also need a web server as well as a DB server (see below).
-
-Installation & Upgrading
-========================
-The documentation to install, upgrade, configure and use Bugzilla can be found
-in different formats:
-* docs/en/html/Bugzilla-Guide.html (HTML version)
-* docs/en/txt/Bugzilla-Guide.txt (text version)
-* docs/en/pdf/Bugzilla-Guide.pdf (PDF version)
-
-If the documentation is missing, you can get it online by visiting
-http://www.bugzilla.org/docs/ from where you can select the documentation
-corresponding to the Bugzilla version you are installing.
-
-Bugzilla Quick Start Guide
-==========================
-(or, how to get Bugzilla up and running in 10 steps)
-Christian Reis <kiko@async.com.br>
-
-This express installation guide is for "normal" Bugzilla installations,
-which means a Linux or Unix system on which Apache, Perl, MySQL or PostgreSQL
-and a Sendmail compatible MTA are available. For other configurations, please
-see the "Installing Bugzilla" section of the Bugzilla Guide in the docs/ directory.
-
-1. Decide from which URL and directory under your webserver root you
-   will be serving the Bugzilla webpages.
-
-2. Unpack the distribution into the chosen directory (there is no copying or
-   installation involved).
-
-3. Run ./checksetup.pl, look for unsolved requirements, and install them.
-   You can run checksetup as many times as necessary to check if
-   everything required has been installed.
-
-   These will usually include assorted Perl modules, MySQL or PostgreSQL,
-   and a MTA.
-
-   After a successful dependency check, checksetup should complain that
-   localconfig needs to be edited.
-
-4. Edit the localconfig file, in particular the $webservergroup and
-   $db_* variables. In particular, $db_name and $db_user will define
-   your database setup in step 5.
-
-5. Create a user permission for the name supplied as $db_user with
-   read/write access to the database whose name is given by $db_name.
-
-   If you are not familiar with MySQL permissions, it's a good idea to
-   use the mysql_setpermission script that is installed with the MySQL
-   distribution, and be sure to read Bugzilla Security - MySQL section
-   in the Bugzilla Guide or PostgreSQL documentation.
-
-6. Run checksetup.pl once more; if all goes well, it should set up the
-   Bugzilla database for you. If not, return to step 5.
-
-   checksetup.pl should ask you, this time, for the administrator's
-   email address and password. These will be used for the initial
-   Bugzilla administrator account.
-
-7. Configure Apache (or install and configure, if you don't have it up
-   yet) to point to the Bugzilla directory. You can choose between
-   mod_cgi and mod_perl. The Bugzilla documentation has detailed information
-   for both modes.
-
-8. Visit the URL you chose for Bugzilla. Your browser should display the
-   default Bugzilla home page. You should then log in as the
-   administrator by following the "Log in" link and supplying the
-   account information you provided in step 6.
-
-9. Visit the "Parameters" page, as suggested by the page displayed to you.
-   Set up the relevant parameters for your local setup.
-
-10. That's it. If anything unexpected comes up:
-
-    - read the error message carefully,
-    - backtrack through the steps above,
-    - check the official installation guide.
-
-Support and installation questions should be directed to the
-support-bugzilla@lists.mozilla.org mailing list.
-
-Further support information is at http://www.bugzilla.org/support/
+Bugzilla
+========
+
+Bugzilla is free and open source web-based bug-tracking software that is
+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
+=============
+
+Bugzilla's comprehensive documentation, including installation instructions,
+can be found here:
+http://www.bugzilla.org/docs/
+
+Reporting Bugs
+==============
+
+Report bugs here:
+https://bugzilla.mozilla.org/enter_bug.cgi?product=Bugzilla
+
+(Please do not file test bugs in this installation of Bugzilla.)
+
+Mailing Lists
+=============
+
+Development:
+https://www.mozilla.org/en-US/about/forums/#dev-apps-bugzilla
+
+Support:
+https://www.mozilla.org/en-US/about/forums/#support-bugzilla
+
+IRC
+===
+
+You can often find Bugzilla developers on IRC:
+irc://irc.mozilla.org/bugzilla
+
+License
+=======
+
+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.
+
+However, this is all only relevant to you if you want to modify the code and
+redistribute it. As with all open source software, there are no restrictions
+on running it, or on modifying it for your own purposes.
diff --git a/attachment.cgi b/attachment.cgi
index f728db484..cfcb8f5f9 100755
--- a/attachment.cgi
+++ b/attachment.cgi
@@ -31,6 +31,7 @@ use Bugzilla::Token;
 use Bugzilla::Keyword;
 
 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
@@ -39,6 +40,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;
 
 ################################################################################
 # Main Body Execution
@@ -205,8 +207,9 @@ sub validateContext
 {
   my $context = $cgi->param('context') || "patch";
   if ($context ne "file" && $context ne "patch") {
-    detaint_natural($context)
-      || ThrowUserError("invalid_context", { context => $cgi->param('context') });
+      my $orig_context = $context;
+      detaint_natural($context)
+        || ThrowUserError("invalid_context", { context => $orig_context });
   }
 
   return $context;
@@ -524,13 +527,14 @@ sub insert {
 
     # Get the filehandle of the attachment.
     my $data_fh = $cgi->upload('data');
+    my $attach_text = $cgi->param('attach_text');
 
     my $attachment = Bugzilla::Attachment->create(
         {bug           => $bug,
          creation_ts   => $timestamp,
-         data          => scalar $cgi->param('attach_text') || $data_fh,
+         data          => $attach_text || $data_fh,
          description   => scalar $cgi->param('description'),
-         filename      => $cgi->param('attach_text') ? "file_$bugid.txt" : scalar $cgi->upload('data'),
+         filename      => $attach_text ? "file_$bugid.txt" : $data_fh,
          ispatch       => scalar $cgi->param('ispatch'),
          isprivate     => scalar $cgi->param('isprivate'),
          mimetype      => $content_type,
@@ -547,7 +551,6 @@ sub insert {
     my ($flags, $new_flags) = Bugzilla::Flag->extract_flags_from_cgi(
                                   $bug, $attachment, $vars, SKIP_REQUESTEE_ON_ERROR);
     $attachment->set_flags($flags, $new_flags);
-    $attachment->update($timestamp);
 
     # Insert a comment about the new attachment into the database.
     my $comment = $cgi->param('comment');
@@ -578,6 +581,10 @@ sub insert {
   $bug->add_cc($user) if $cgi->param('addselfcc');
   $bug->update($timestamp);
 
+  # We have to update the attachment after updating the bug, to ensure new
+  # comments are available.
+  $attachment->update($timestamp);
+
   $dbh->bz_commit_transaction;
 
   # Define the variables and functions that will be passed to the UI template.
@@ -700,6 +707,11 @@ sub update {
     # Figure out when the changes were made.
     my $timestamp = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
 
+    # Commit the comment, if any.
+    # This has to happen before updating the attachment, to ensure new comments
+    # are available to $attachment->update.
+    $bug->update($timestamp);
+
     if ($can_edit) {
         my $changes = $attachment->update($timestamp);
         # If there are changes, we updated delta_ts in the DB. We have to
@@ -707,9 +719,6 @@ sub update {
         $bug->{delta_ts} = $timestamp if scalar(keys %$changes);
     }
 
-    # Commit the comment, if any.
-    $bug->update($timestamp);
-
     # Commit the transaction now that we are finished updating the database.
     $dbh->bz_commit_transaction();
 
diff --git a/buglist.cgi b/buglist.cgi
index 81350dc81..e3d8fe711 100755
--- a/buglist.cgi
+++ b/buglist.cgi
@@ -916,7 +916,7 @@ if (scalar(@products) == 1) {
 # This is used in the "Zarroo Boogs" case.
 elsif (my @product_input = $cgi->param('product')) {
     if (scalar(@product_input) == 1 and $product_input[0] ne '') {
-        $one_product = new Bugzilla::Product({ name => $cgi->param('product') });
+        $one_product = new Bugzilla::Product({ name => $product_input[0] });
     }
 }
 # We only want the template to use it if the user can actually 
diff --git a/chart.cgi b/chart.cgi
index 7f21fd098..6e995a4e0 100755
--- a/chart.cgi
+++ b/chart.cgi
@@ -94,6 +94,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
@@ -304,9 +311,12 @@ sub plot {
     my $format = $template->get_format("reports/chart", "", scalar($cgi->param('ctype')));
 
     # 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'});
@@ -348,7 +358,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 bcc1ad8ea..ab7ea9f7a 100755
--- a/checksetup.pl
+++ b/checksetup.pl
@@ -15,12 +15,13 @@
 use strict;
 use 5.008001;
 use File::Basename;
+BEGIN { chdir dirname($0); }
+use lib qw(. lib);
+
 use Getopt::Long qw(:config bundling);
 use Pod::Usage;
 use Safe;
 
-BEGIN { chdir dirname($0); }
-use lib qw(. lib);
 use Bugzilla::Constants;
 use Bugzilla::Install::Requirements;
 use Bugzilla::Install::Util qw(install_string get_version_and_os 
diff --git a/collectstats.pl b/collectstats.pl
index aa98ddfb4..d2b6b74d3 100755
--- a/collectstats.pl
+++ b/collectstats.pl
@@ -321,7 +321,7 @@ sub regenerate_stats {
         return;
     }
 
-    if (open DATA, ">$file") {
+    if (open DATA, ">", $file) {
         my $fields = join('|', ('DATE', @statuses, @resolutions));
         print DATA <<FIN;
 # Bugzilla Daily Bug Stats
diff --git a/contrib/bz_webservice_demo.pl b/contrib/bz_webservice_demo.pl
index 3b87cf5b5..8850d642a 100755
--- a/contrib/bz_webservice_demo.pl
+++ b/contrib/bz_webservice_demo.pl
@@ -24,7 +24,6 @@ use Getopt::Long;
 use Pod::Usage;
 use File::Basename qw(dirname);
 use File::Spec;
-use HTTP::Cookies;
 use XMLRPC::Lite;
 
 # If you want, say “use Bugzilla::WebService::Constants” here to get access
@@ -36,7 +35,8 @@ my $help;
 my $Bugzilla_uri;
 my $Bugzilla_login;
 my $Bugzilla_password;
-my $Bugzilla_remember;
+my $Bugzilla_restrict;
+my $Bugzilla_token;
 my $bug_id;
 my $product_name;
 my $create_file_name;
@@ -51,7 +51,7 @@ GetOptions('help|h|?'       => \$help,
            'uri=s'          => \$Bugzilla_uri,
            'login:s'        => \$Bugzilla_login,
            'password=s'     => \$Bugzilla_password,
-           'rememberlogin!' => \$Bugzilla_remember,
+           'restrictlogin!' => \$Bugzilla_restrict,
            'bug_id:s'       => \$bug_id,
            'product_name:s' => \$product_name,
            'create:s'       => \$create_file_name,
@@ -86,14 +86,14 @@ Specify this without a value in order to log out.
 
 Bugzilla password. Specify this together with B<--login> in order to log in.
 
-=item --rememberlogin
+=item --restrictlogin
 
-Gives access to Bugzilla's "Bugzilla_remember" option.
-Specify this option while logging in to do the same thing as ticking the
-C<Bugzilla_remember> box on Bugilla's log in form.
+Gives access to Bugzilla's "Bugzilla_restrictlogin" option.
+Specify this option while logging in to restrict the login token to be
+only valid from the IP address which called
 Don't specify this option to do the same thing as unchecking the box.
 
-See Bugzilla's rememberlogin parameter for details.
+See Bugzilla's restrictlogin parameter for details.
 
 =item --bug_id
 
@@ -151,17 +151,6 @@ my $soapresult;
 # We will use this variable for function call results.
 my $result;
 
-# Open our cookie jar. We save it into a file so that we may re-use cookies
-# to avoid the need of logging in every time. You're encouraged, but not
-# required, to do this in your applications, too.
-# Cookies are only saved if Bugzilla's rememberlogin parameter is set to one of
-#    - on
-#    - defaulton (and you didn't pass 0 as third parameter to User.login)
-#    - defaultoff (and you passed 1 as third parameter to User.login)
-my $cookie_jar =
-    new HTTP::Cookies('file' => File::Spec->catdir(dirname($0), 'cookies.txt'),
-                      'autosave' => 1);
-
 =head2 Initialization
 
 Using the XMLRPC::Lite class, you set up a proxy, as shown in this script.
@@ -170,8 +159,7 @@ of C<http://your.bugzilla.installation/path/to/bugzilla/xmlrpc.cgi>.
 
 =cut
 
-my $proxy = XMLRPC::Lite->proxy($Bugzilla_uri,
-                                'cookie_jar' => $cookie_jar);
+my $proxy = XMLRPC::Lite->proxy($Bugzilla_uri);
 
 =head2 Debugging
 
@@ -205,25 +193,6 @@ $soapresult = $proxy->call('Bugzilla.timezone');
 _die_on_fault($soapresult);
 print 'Bugzilla\'s timezone is ' . $soapresult->result()->{timezone} . ".\n";
 
-=head2 Getting Extension Information
-
-Returns all the information any extensions have decided to provide to the webservice.
-
-=cut
-
-if ($fetch_extension_info) {
-    $soapresult = $proxy->call('Bugzilla.extensions');
-    _die_on_fault($soapresult);
-    my $extensions = $soapresult->result()->{extensions};
-    foreach my $extensionname (keys(%$extensions)) {
-        print "Extension '$extensionname' information\n";
-        my $extension = $extensions->{$extensionname};
-        foreach my $data (keys(%$extension)) {
-            print '  ' . $data . ' => ' . $extension->{$data} . "\n";
-        }
-    }
-}
-
 =head2 Logging In and Out
 
 =head3 Using Bugzilla's Environment Authentication
@@ -238,21 +207,20 @@ You don't log out if you're using this kind of authentication.
 Use the C<User.login> and C<User.logout> calls to log in and out, as shown
 in this script.
 
-The C<Bugzilla_remember> parameter is optional.
-If omitted, Bugzilla's defaults apply (as specified by its C<rememberlogin>
+The C<Bugzilla_restrictlogin> parameter is optional.
+If omitted, Bugzilla's defaults apply (as specified by its C<restrictlogin>
 parameter).
 
-Bugzilla hands back cookies you'll need to pass along during your work calls.
-
 =cut
 
 if (defined($Bugzilla_login)) {
     if ($Bugzilla_login ne '') {
         # Log in.
         $soapresult = $proxy->call('User.login',
-                                   { login => $Bugzilla_login, 
+                                   { login => $Bugzilla_login,
                                      password => $Bugzilla_password,
-                                     remember => $Bugzilla_remember } );
+                                     restrict_login => $Bugzilla_restrict } );
+        $Bugzilla_token = $soapresult->result->{token};
         _die_on_fault($soapresult);
         print "Login successful.\n";
     }
@@ -264,17 +232,36 @@ if (defined($Bugzilla_login)) {
     }
 }
 
+=head2 Getting Extension Information
+
+Returns all the information any extensions have decided to provide to the webservice.
+
+=cut
+
+if ($fetch_extension_info) {
+    $soapresult = $proxy->call('Bugzilla.extensions', {token => $Bugzilla_token});
+    _die_on_fault($soapresult);
+    my $extensions = $soapresult->result()->{extensions};
+    foreach my $extensionname (keys(%$extensions)) {
+        print "Extension '$extensionname' information\n";
+        my $extension = $extensions->{$extensionname};
+        foreach my $data (keys(%$extension)) {
+            print '  ' . $data . ' => ' . $extension->{$data} . "\n";
+        }
+    }
+}
+
 =head2 Retrieving Bug Information
 
 Call C<Bug.get> with the ID of the bug you want to know more of.
-The call will return a C<Bugzilla::Bug> object. 
+The call will return a C<Bugzilla::Bug> object.
 
 Note: You can also use "Bug.get_bugs" for compatibility with Bugzilla 3.0 API.
 
 =cut
 
 if ($bug_id) {
-    $soapresult = $proxy->call('Bug.get', { ids => [$bug_id] });
+    $soapresult = $proxy->call('Bug.get', { ids => [$bug_id], token => $Bugzilla_token});
     _die_on_fault($soapresult);
     $result = $soapresult->result;
     my $bug = $result->{bugs}->[0];
@@ -299,7 +286,7 @@ The call will return a C<Bugzilla::Product> object.
 =cut
 
 if ($product_name) {
-    $soapresult = $proxy->call('Product.get', {'names' => [$product_name]});
+    $soapresult = $proxy->call('Product.get', {'names' => [$product_name], token => $Bugzilla_token});
     _die_on_fault($soapresult);
     $result = $soapresult->result()->{'products'}->[0];
 
@@ -325,14 +312,16 @@ if ($product_name) {
 =head2 Creating A Bug
 
 Call C<Bug.create> with the settings read from the file indicated on
-the command line. The file must contain a valid anonymous hash to use 
+the command line. The file must contain a valid anonymous hash to use
 as argument for the call to C<Bug.create>.
 The call will return a hash with a bug id for the newly created bug.
 
 =cut
 
 if ($create_file_name) {
-    $soapresult = $proxy->call('Bug.create', do "$create_file_name" );
+    my $bug_fields = do "$create_file_name";
+    $bug_fields->{Bugzilla_token} = $Bugzilla_token;
+    $soapresult = $proxy->call('Bug.create', \%$bug_fields);
     _die_on_fault($soapresult);
     $result = $soapresult->result;
 
@@ -356,7 +345,7 @@ list of legal values for this field.
 =cut
 
 if ($legal_field_values) {
-    $soapresult = $proxy->call('Bug.legal_values', {field => $legal_field_values} );
+    $soapresult = $proxy->call('Bug.legal_values', {field => $legal_field_values, token => $Bugzilla_token} );
     _die_on_fault($soapresult);
     $result = $soapresult->result;
 
@@ -374,7 +363,7 @@ or not.
 if ($add_comment) {
     if ($bug_id) {
         $soapresult = $proxy->call('Bug.add_comment', {id => $bug_id,
-            comment => $add_comment, private => $private, work_time => $work_time});
+            comment => $add_comment, private => $private, work_time => $work_time, token => $Bugzilla_token});
         _die_on_fault($soapresult);
         print "Comment added.\n";
     }
diff --git a/editflagtypes.cgi b/editflagtypes.cgi
index e9c430d7d..aa789fc74 100755
--- a/editflagtypes.cgi
+++ b/editflagtypes.cgi
@@ -44,23 +44,24 @@ my @products = @{$vars->{products}};
 
 my $action = $cgi->param('action') || 'list';
 my $token  = $cgi->param('token');
-my $product = $cgi->param('product');
-my $component = $cgi->param('component');
+my $prod_name = $cgi->param('product');
+my $comp_name = $cgi->param('component');
 my $flag_id = $cgi->param('id');
 
-if ($product) {
+my ($product, $component);
+
+if ($prod_name) {
     # Make sure the user is allowed to view this product name.
     # Users with global editcomponents privs can see all product names.
-    ($product) = grep { lc($_->name) eq lc($product) } @products;
-    $product || ThrowUserError('product_access_denied', { name => $cgi->param('product') });
+    ($product) = grep { lc($_->name) eq lc($prod_name) } @products;
+    $product || ThrowUserError('product_access_denied', { name => $prod_name });
 }
 
-if ($component) {
-    ($product && $product->id)
-      || ThrowUserError('flag_type_component_without_product');
-    ($component) = grep { lc($_->name) eq lc($component) } @{$product->components};
+if ($comp_name) {
+    $product || ThrowUserError('flag_type_component_without_product');
+    ($component) = grep { lc($_->name) eq lc($comp_name) } @{$product->components};
     $component || ThrowUserError('product_unknown_component', { product => $product->name,
-                                                                comp => $cgi->param('component') });
+                                                                comp => $comp_name });
 }
 
 # If 'categoryAction' is set, it has priority over 'action'.
diff --git a/editgroups.cgi b/editgroups.cgi
index d603ab183..e3b9f60d1 100755
--- a/editgroups.cgi
+++ b/editgroups.cgi
@@ -19,9 +19,6 @@ use Bugzilla::Product;
 use Bugzilla::User;
 use Bugzilla::Token;
 
-use constant SPECIAL_GROUPS => ('chartgroup', 'insidergroup',
-                                'timetrackinggroup', 'querysharegroup');
-
 my $cgi = Bugzilla->cgi;
 my $dbh = Bugzilla->dbh;
 my $template = Bugzilla->template;
@@ -224,7 +221,7 @@ if ($action eq 'new') {
 
 if ($action eq 'del') {
     # Check that an existing group ID is given
-    my $group = Bugzilla::Group->check({ id => $cgi->param('group') });
+    my $group = Bugzilla::Group->check({ id => scalar $cgi->param('group') });
     $group->check_remove({ test_only => 1 });
     $vars->{'shared_queries'} =
         $dbh->selectrow_array('SELECT COUNT(*)
@@ -248,7 +245,7 @@ if ($action eq 'del') {
 if ($action eq 'delete') {
     check_token_data($token, 'delete_group');
     # Check that an existing group ID is given
-    my $group = Bugzilla::Group->check({ id => $cgi->param('group') });
+    my $group = Bugzilla::Group->check({ id => scalar $cgi->param('group') });
     $vars->{'name'} = $group->name;
     $group->remove_from_db({
         remove_from_users => scalar $cgi->param('removeusers'),
diff --git a/editusers.cgi b/editusers.cgi
index d022321f0..9778aa808 100755
--- a/editusers.cgi
+++ b/editusers.cgi
@@ -483,10 +483,6 @@ if ($action eq 'search') {
     my $sth_set_bug_timestamp =
         $dbh->prepare('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?');
 
-    my $sth_updateFlag = $dbh->prepare('INSERT INTO bugs_activity
-                  (bug_id, attach_id, who, bug_when, fieldid, removed, added)
-                  VALUES (?, ?, ?, ?, ?, ?, ?)');
-
     # Flags
     my $flag_ids =
       $dbh->selectcol_arrayref('SELECT id FROM flags WHERE requestee_id = ?',
@@ -501,16 +497,15 @@ if ($action eq 'search') {
     # so we have to log these changes manually.
     my %bugs;
     push(@{$bugs{$_->bug_id}->{$_->attach_id || 0}}, $_) foreach @$flags;
-    my $fieldid = get_field_id('flagtypes.name');
     foreach my $bug_id (keys %bugs) {
         foreach my $attach_id (keys %{$bugs{$bug_id}}) {
             my @old_summaries = Bugzilla::Flag->snapshot($bugs{$bug_id}->{$attach_id});
             $_->_set_requestee() foreach @{$bugs{$bug_id}->{$attach_id}};
             my @new_summaries = Bugzilla::Flag->snapshot($bugs{$bug_id}->{$attach_id});
             my ($removed, $added) =
-              Bugzilla::Flag->update_activity(\@old_summaries, \@new_summaries);
-            $sth_updateFlag->execute($bug_id, $attach_id || undef, $userid,
-                                     $timestamp, $fieldid, $removed, $added);
+                Bugzilla::Flag->update_activity(\@old_summaries, \@new_summaries);
+            LogActivityEntry($bug_id, 'flagtypes.name', $removed, $added,
+                $userid, $timestamp, undef, $attach_id);
         }
         $sth_set_bug_timestamp->execute($timestamp, $bug_id);
         $updatedbugs{$bug_id} = 1;
@@ -709,7 +704,9 @@ sub check_user {
 sub mirrorListSelectionValues {
     my $cgi = Bugzilla->cgi;
     if (defined($cgi->param('matchtype'))) {
-        foreach ('matchvalue', 'matchstr', 'matchtype', 'grouprestrict', 'groupid') {
+        foreach ('matchvalue', 'matchstr', 'matchtype',
+                 'grouprestrict', 'groupid', 'enabled_only')
+        {
             $vars->{'listselectionvalues'}{$_} = $cgi->param($_);
         }
     }
diff --git a/extensions/Example/lib/WebService.pm b/extensions/Example/lib/WebService.pm
index 659189d2f..56adc8cb8 100644
--- a/extensions/Example/lib/WebService.pm
+++ b/extensions/Example/lib/WebService.pm
@@ -11,6 +11,11 @@ use warnings;
 use base qw(Bugzilla::WebService);
 use Bugzilla::Error;
 
+use constant PUBLIC_METHODS => qw(
+    hello
+    throw_an_error
+);
+
 # This can be called as Example.hello() from the WebService.
 sub hello { return 'Hello!'; }
 
diff --git a/importxml.pl b/importxml.pl
index e7dc9761b..97c022f6a 100755
--- a/importxml.pl
+++ b/importxml.pl
@@ -1264,6 +1264,9 @@ my $twig = XML::Twig->new(
     },
     start_tag_handlers => { bugzilla => \&init }
 );
+# Prevent DoS using the billion laughs attack.
+$twig->{NoExpand} = 1;
+
 $twig->parse($xml);
 my $root       = $twig->root;
 my $maintainer = $root->{'att'}->{'maintainer'};
diff --git a/js/field.js b/js/field.js
index c0d0aaa6e..356c0cd5a 100644
--- a/js/field.js
+++ b/js/field.js
@@ -46,10 +46,14 @@ function validateEnterBug(theform) {
         _errorFor(attach_desc, 'attach_desc');
         focus_me = attach_desc;
     }
-    var check_description = status_comment_required[bug_status.value];
-    if (check_description && YAHOO.lang.trim(description.value) == '') {
-        _errorFor(description, 'description');
-        focus_me = description;
+    // bug_status can be undefined if the bug_status field is not editable by
+    // the currently logged in user.
+    if (bug_status) {
+        var check_description = status_comment_required[bug_status.value];
+        if (check_description && YAHOO.lang.trim(description.value) == '') {
+            _errorFor(description, 'description');
+            focus_me = description;
+        }
     }
     if (YAHOO.lang.trim(short_desc.value) == '') {
         _errorFor(short_desc);
diff --git a/mod_perl.pl b/mod_perl.pl
index ae15ae5fc..4794e285a 100644
--- a/mod_perl.pl
+++ b/mod_perl.pl
@@ -73,7 +73,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/post_bug.cgi b/post_bug.cgi
index 33f5652a5..0a0f8562c 100755
--- a/post_bug.cgi
+++ b/post_bug.cgi
@@ -150,7 +150,10 @@ if (defined $cgi->param('version')) {
 # after the bug is filed.
 
 # Add an attachment if requested.
-if (defined($cgi->upload('data')) || $cgi->param('attach_text')) {
+my $data_fh = $cgi->upload('data');
+my $attach_text = $cgi->param('attach_text');
+
+if ($data_fh || $attach_text) {
     $cgi->param('isprivate', $cgi->param('comment_is_private'));
 
     # Must be called before create() as it may alter $cgi->param('ispatch').
@@ -165,9 +168,9 @@ if (defined($cgi->upload('data')) || $cgi->param('attach_text')) {
         $attachment = Bugzilla::Attachment->create(
             {bug           => $bug,
              creation_ts   => $timestamp,
-             data          => scalar $cgi->param('attach_text') || $cgi->upload('data'),
+             data          => $attach_text || $data_fh,
              description   => scalar $cgi->param('description'),
-             filename      => $cgi->param('attach_text') ? "file_$id.txt" : scalar $cgi->upload('data'),
+             filename      => $attach_text ? "file_$id.txt" : $data_fh,
              ispatch       => scalar $cgi->param('ispatch'),
              isprivate     => scalar $cgi->param('isprivate'),
              mimetype      => $content_type,
diff --git a/relogin.cgi b/relogin.cgi
index 4338c8ee0..b86463bb8 100755
--- a/relogin.cgi
+++ b/relogin.cgi
@@ -61,6 +61,9 @@ elsif ($action eq 'prepare-sudo') {
                           -httponly => 1,
                           %args);
 
+        # The user ID must not be set when generating the token, because
+        # that information will not be available when validating it.
+        local Bugzilla->user->{userid} = 0;
         $vars->{'login_request_token'} = issue_hash_token(['login_request', $value]);
     }
 
@@ -84,19 +87,21 @@ elsif ($action eq 'begin-sudo') {
     {
         $credentials_provided = 1;
     }
-    
+
     # Next, log in the user
     my $user = Bugzilla->login(LOGIN_REQUIRED);
-    
+
+    my $target_login = $cgi->param('target_login');
+    my $reason = $cgi->param('reason') || '';
+
     # At this point, the user is logged in.  However, if they used a method
     # where they could have provided a username/password (i.e. CGI), but they 
     # did not provide a username/password, then throw an error.
     if ($user->authorizer->can_login && !$credentials_provided) {
         ThrowUserError('sudo_password_required',
-                       { target_login => $cgi->param('target_login'),
-                               reason => $cgi->param('reason')});
+                       { target_login => $target_login, reason => $reason });
     }
-    
+
     # The user must be in the 'bz_sudoers' group
     unless ($user->in_group('bz_sudoers')) {
         ThrowUserError('auth_failure', {  group => 'bz_sudoers',
@@ -120,30 +125,22 @@ elsif ($action eq 'begin-sudo') {
             && ($token_data eq 'sudo_prepared'))
     {
         ThrowUserError('sudo_preparation_required', 
-                       { target_login => scalar $cgi->param('target_login'),
-                               reason => scalar $cgi->param('reason')});
+                       { target_login => $target_login, reason => $reason });
     }
     delete_token($cgi->param('token'));
 
     # Get & verify the target user (the user who we will be impersonating)
-    my $target_user = 
-        new Bugzilla::User({ name => $cgi->param('target_login') });
+    my $target_user = new Bugzilla::User({ name => $target_login });
     unless (defined($target_user)
             && $target_user->id
             && $user->can_see_user($target_user))
     {
-        ThrowUserError('user_match_failed',
-                       { 'name' => $cgi->param('target_login') }
-        );
+        ThrowUserError('user_match_failed', { name => $target_login });
     }
     if ($target_user->in_group('bz_sudo_protect')) {
         ThrowUserError('sudo_protected', { login => $target_user->login });
     }
 
-    # If we have a reason passed in, keep it under 200 characters
-    my $reason = $cgi->param('reason') || '';
-    $reason = substr($reason, 0, 200);
-    
     # Calculate the session expiry time (T + 6 hours)
     my $time_string = time2str('%a, %d-%b-%Y %T %Z', time + MAX_SUDO_TOKEN_AGE, 'GMT');
 
@@ -163,9 +160,12 @@ elsif ($action eq 'begin-sudo') {
 
     # For the present, change the values of Bugzilla::user & Bugzilla::sudoer
     Bugzilla->sudo_request($target_user, $user);
-    
+
     # NOTE: If you want to log the start of an sudo session, do it here.
 
+    # If we have a reason passed in, keep it under 200 characters
+    $reason = substr($reason, 0, 200);
+
     # Go ahead and send out the message now
     my $message;
     my $mail_template = Bugzilla->template_inner($target_user->setting('lang'));
diff --git a/report.cgi b/report.cgi
index f4f015b92..4b1356163 100755
--- a/report.cgi
+++ b/report.cgi
@@ -311,7 +311,12 @@ 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");
+}
 
 my @time = localtime(time());
 my $date = sprintf "%04d-%02d-%02d", 1900+$time[5],$time[4]+1,$time[3];
@@ -321,12 +326,10 @@ print $cgi->header(-type => $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/reports.cgi b/reports.cgi
index a2e1e6a7e..7b7c59478 100755
--- a/reports.cgi
+++ b/reports.cgi
@@ -136,7 +136,7 @@ sub generate_chart {
     $data_file =~ s/\//-/gs;
     $data_file = $dir . '/' . $data_file;
 
-    if (! open FILE, $data_file) {
+    if (!open(FILE, '<', $data_file)) {
         if ($product eq '-All-') {
             $product = '';
         }
diff --git a/search_plugin.cgi b/search_plugin.cgi
index 3809159c7..ca515bfae 100755
--- a/search_plugin.cgi
+++ b/search_plugin.cgi
@@ -24,7 +24,7 @@ print $cgi->header('application/xml');
 
 # Get the contents of favicon.ico
 my $filename = bz_locations()->{'libpath'} . "/images/favicon.ico";
-if (open(IN, $filename)) {
+if (open(IN, '<', $filename)) {
     local $/;
     binmode IN;
     $vars->{'favicon'} = <IN>;
diff --git a/showdependencygraph.cgi b/showdependencygraph.cgi
index 8e3592fbe..4a451c104 100755
--- a/showdependencygraph.cgi
+++ b/showdependencygraph.cgi
@@ -46,19 +46,25 @@ sub CreateImagemap {
     my $map = "<map name=\"imagemap\">\n";
     my $default = "";
 
-    open MAP, "<$mapfilename";
+    open MAP, "<", $mapfilename;
     while(my $line = <MAP>) {
         if($line =~ /^default ([^ ]*)(.*)$/) {
             $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};
@@ -176,13 +182,16 @@ foreach my $k (@bug_ids) {
     # Retrieve bug information from the database
     my ($stat, $resolution, $summary) = $dbh->selectrow_array($sth, undef, $k);
 
+    $vars->{'short_desc'} = $summary if ($k eq $cgi->param('id'));
+
     # Resolution and summary are shown only if user can see the bug
-    if (!$user->can_see_bug($k)) {
+    if ($user->can_see_bug($k)) {
+        $summary = html_quote(clean_text($summary));
+    }
+    else {
         $resolution = $summary = '';
     }
 
-    $vars->{'short_desc'} = $summary if ($k eq $cgi->param('id'));
-
     my @params;
 
     if ($summary ne "" && $cgi->param('showsummary')) {
@@ -247,7 +256,7 @@ if ($webdotbase =~ /^https?:/) {
                                                  error => $! });
 
     binmode $pngfh;
-    open(DOT, "\"$webdotbase\" -Tpng $filename|");
+    open(DOT, '-|', "\"$webdotbase\" -Tpng $filename");
     binmode DOT;
     print $pngfh $_ while <DOT>;
     close DOT;
@@ -276,7 +285,7 @@ if ($webdotbase =~ /^https?:/) {
                                                  error => $! });
 
     binmode $mapfh;
-    open(DOT, "\"$webdotbase\" -Tismap $filename|");
+    open(DOT, '-|', "\"$webdotbase\" -Tismap $filename");
     binmode DOT;
     print $mapfh $_ while <DOT>;
     close DOT;
diff --git a/t/002goodperl.t b/t/002goodperl.t
index e691b39dd..8bbe657b3 100644
--- a/t/002goodperl.t
+++ b/t/002goodperl.t
@@ -12,11 +12,11 @@
 
 use strict;
 
-use lib 't';
+use lib qw(. lib t);
 
 use Support::Files;
 
-use Test::More tests => (scalar(@Support::Files::testitems) * 3);
+use Test::More tests => (scalar(@Support::Files::testitems) * 4);
 
 my @testitems = @Support::Files::testitems; # get the files to test.
 
@@ -110,4 +110,35 @@ foreach my $file (@testitems) {
     
     close(FILE);
 }
+
+# Forbird the { foo => $cgi->param() } syntax, for security reasons.
+foreach my $file (@testitems) {
+    $file =~ s/\s.*$//; # nuke everything after the first space (#comment)
+    next unless $file; # skip null entries
+    if (!open(FILE, $file)) {
+        ok(0, "could not open $file --WARNING");
+        next;
+    }
+    my $lineno = 0;
+    my @unsafe_args;
+
+    while (my $file_line = <FILE>) {
+        $lineno++;
+        $file_line =~ s/^\s*(.+)\s*$/$1/; # Remove leading and trailing whitespaces.
+        if ($file_line =~ /^[^#]+=> \$cgi\->param/) {
+            push(@unsafe_args, "$file_line on line $lineno");
+        }
+    }
+
+    if (@unsafe_args) {
+        ok(0, "$file incorrectly passes a CGI argument to a hash --ERROR\n" .
+              join("\n", @unsafe_args));
+    }
+    else {
+        ok(1, "$file has no vulnerable hash syntax");
+    }
+
+    close(FILE);
+}
+
 exit 0;
diff --git a/t/003safesys.t b/t/003safesys.t
index 0d6a215b1..46432662f 100644
--- a/t/003safesys.t
+++ b/t/003safesys.t
@@ -12,7 +12,7 @@
 
 use strict;
 
-use lib 't';
+use lib qw(. lib t);
 
 use Support::Files;
 
@@ -38,7 +38,16 @@ my $perlapp = "\"$^X\"";
 foreach my $file (@testitems) {
     $file =~ s/\s.*$//; # nuke everything after the first space (#comment)
     next if (!$file); # skip null entries
-    my $command = "$perlapp -c -It -MSupport::Systemexec $file 2>&1";
+
+    open(my $fh2, '<', $file);
+    my $bang = <$fh2>;
+    close $fh2;
+
+    my $T = "";
+    if ($bang =~ m/#!\S*perl\s+-.*T/) {
+        $T = "T";
+    }
+    my $command = "$perlapp -c$T -It -MSupport::Systemexec $file 2>&1";
     my $loginfo=`$command`;
     if ($loginfo =~ /arguments for Support::Systemexec::(system|exec)/im) {
         ok(0,"$file DOES NOT use proper system or exec calls");
diff --git a/t/004template.t b/t/004template.t
index 604559dc0..7e4973c35 100644
--- a/t/004template.t
+++ b/t/004template.t
@@ -11,7 +11,7 @@
 
 use strict;
 
-use lib 't';
+use lib qw(. lib t);
 
 use Support::Templates;
 
diff --git a/t/005whitespace.t b/t/005whitespace.t
index 624df69f6..124fbfe77 100644
--- a/t/005whitespace.t
+++ b/t/005whitespace.t
@@ -11,7 +11,7 @@
 
 use strict;
 
-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 07cd3ea8c..4382e4f9e 100644
--- a/t/006spellcheck.t
+++ b/t/006spellcheck.t
@@ -10,7 +10,7 @@
 #Bugzilla Test 6#
 ####Spelling#####
 
-use lib 't';
+use lib qw(. lib t);
 use Support::Files;
 
 BEGIN { # yes the indenting is off, deal with it
diff --git a/t/007util.t b/t/007util.t
index 495102ffa..f3c25f076 100644
--- a/t/007util.t
+++ b/t/007util.t
@@ -9,7 +9,7 @@
 #Bugzilla Test 7#
 #####Util.pm#####
 
-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 66262655c..a31f5d65d 100644
--- a/t/009bugwords.t
+++ b/t/009bugwords.t
@@ -17,7 +17,7 @@
 
 use strict;
 
-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 d84688a7e..a6402d3df 100644
--- a/t/010dependencies.t
+++ b/t/010dependencies.t
@@ -66,7 +66,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 c638dbcde..c3b20b650 100644
--- a/t/011pod.t
+++ b/t/011pod.t
@@ -12,7 +12,7 @@
 
 use strict;
 
-use lib 't';
+use lib qw(. lib t);
 
 use Support::Files;
 use Pod::Checker;
diff --git a/taskgraph.json b/taskgraph.json
new file mode 100644
index 000000000..23eafb9ce
--- /dev/null
+++ b/taskgraph.json
@@ -0,0 +1,264 @@
+{
+  "metadata": {
+    "name": "Bugzilla Task Graph",
+    "description": "A suite of tests to check the quality of the Bugzilla codebase.",
+    "owner": "dlawrence@mozilla.com",
+    "source": "https://raw.githubusercontent.com/bugzilla/bugzilla/4.4/taskgraph.json"
+  },
+  "tasks": [
+    {
+      "reruns": 3,
+      "maxRunTime": 3000,
+      "task": {
+        "expires": "2018-02-18T17:33:38.806Z",
+        "metadata": {
+          "name": "Basic Sanity Tests"
+        },
+        "provisionerId": "aws-provisioner-v1",
+        "workerType": "b2gtest",
+        "payload": {
+          "image": "bugzilla/bugzilla-ci",
+          "command": ["runtests.sh"],
+          "env": {
+            "TEST_SUITE": "sanity"
+          },
+          "artifacts": {
+            "public/runtests_log": {
+              "type": "file",
+              "path": "/tmp/runtests.log",
+              "expires": "2018-02-17T17:33:38.806Z"
+            }
+          }
+        },
+        "extra": {
+          "treeherder": {
+            "symbol": "San",
+            "machine": {
+                "platform": "linux64"
+            },
+            "build": {
+                "platform": "linux64"
+            }
+          }
+        }
+      }
+    },
+    {
+      "reruns": 3,
+      "maxRunTime": 3000,
+      "task": {
+        "expires": "2018-02-18T17:33:38.806Z",
+        "metadata": {
+          "name": "Documentation Build Test"
+        },
+        "provisionerId": "aws-provisioner-v1",
+        "workerType": "b2gtest",
+        "payload": {
+          "image": "bugzilla/bugzilla-ci",
+          "command": ["runtests.sh"],
+          "env": {
+            "TEST_SUITE": "docs"
+          },
+          "artifacts": {
+            "public/runtests_log": {
+              "type": "file",
+              "path": "/tmp/runtests.log",
+              "expires": "2018-02-17T17:33:38.806Z"
+            }
+          }
+        },
+        "extra": {
+          "treeherder": {
+            "symbol": "Doc",
+            "machine": {
+                "platform": "linux64"
+            },
+            "build": {
+                "platform": "linux64"
+            }
+          }
+        }
+      }
+    },
+    {
+      "reruns": 3,
+      "maxRunTime": 7200,
+      "task": {
+        "expires": "2018-02-18T17:33:38.806Z",
+        "metadata": {
+          "name": "WebService API Tests (MySQL)"
+        },
+        "provisionerId": "aws-provisioner-v1",
+        "workerType": "b2gtest",
+        "payload": {
+          "image": "bugzilla/bugzilla-ci",
+          "command": ["runtests.sh"],
+          "env": {
+            "TEST_SUITE": "webservices"
+          },
+          "artifacts": {
+            "public/runtests_log": {
+              "type": "file",
+              "path": "/tmp/runtests.log",
+              "expires": "2018-02-17T17:33:38.806Z"
+            },
+            "public/httpd_error_log": {
+              "type": "file",
+              "path": "/var/log/httpd/error_log",
+              "expires": "2018-02-17T17:33:38.806Z"
+            }
+          }
+        },
+        "extra": {
+          "treeherder": {
+            "symbol": "API",
+            "machine": {
+                "platform": "linux64"
+            },
+            "build": {
+                "platform": "linux64"
+            }
+          }
+        }
+      }
+    },
+    {
+      "reruns": 3,
+      "maxRunTime": 7200,
+      "task": {
+        "expires": "2018-02-18T17:33:38.806Z",
+        "metadata": {
+          "name": "Selenium Tests (MySQL)"
+        },
+        "provisionerId": "aws-provisioner-v1",
+        "workerType": "b2gtest",
+        "payload": {
+          "image": "bugzilla/bugzilla-ci",
+          "command": ["runtests.sh"],
+          "env": {
+            "TEST_SUITE": "selenium"
+          },
+          "artifacts": {
+            "public/runtests_log": {
+              "type": "file",
+              "path": "/tmp/runtests.log",
+              "expires": "2018-02-17T17:33:38.806Z"
+            },
+            "public/httpd_error_log": {
+              "type": "file",
+              "path": "/var/log/httpd/error_log",
+              "expires": "2018-02-17T17:33:38.806Z"
+            },
+            "public/selenium_log": {
+              "type": "file",
+              "path": "/tmp/selenium.log",
+              "expires": "2018-02-17T17:33:38.806Z"
+            }
+          }
+        },
+        "extra": {
+          "treeherder": {
+            "symbol": "Sel",
+            "machine": {
+                "platform": "linux64"
+            },
+            "build": {
+                "platform": "linux64"
+            }
+          }
+        }
+      }
+    },
+    {
+      "reruns": 3,
+      "maxRunTime": 7200,
+      "task": {
+        "expires": "2018-02-18T17:33:38.806Z",
+        "metadata": {
+          "name": "WebService API Tests (Pg)"
+        },
+        "provisionerId": "aws-provisioner-v1",
+        "workerType": "b2gtest",
+        "payload": {
+          "image": "bugzilla/bugzilla-ci",
+          "command": ["runtests.sh"],
+          "env": {
+            "BUGS_DB_DRIVER": "pg",
+            "TEST_SUITE": "webservices"
+          },
+          "artifacts": {
+            "public/runtests_log": {
+              "type": "file",
+              "path": "/tmp/runtests.log",
+              "expires": "2018-02-17T17:33:38.806Z"
+            },
+            "public/httpd_error_log": {
+              "type": "file",
+              "path": "/var/log/httpd/error_log",
+              "expires": "2018-02-17T17:33:38.806Z"
+            }
+          }
+        },
+        "extra": {
+          "treeherder": {
+            "symbol": "API-Pg",
+            "machine": {
+                "platform": "linux64"
+            },
+            "build": {
+                "platform": "linux64"
+            }
+          }
+        }
+      }
+    },
+    {
+      "reruns": 3,
+      "maxRunTime": 7200,
+      "task": {
+        "expires": "2018-02-18T17:33:38.806Z",
+        "metadata": {
+          "name": "Selenium Tests (Pg)"
+        },
+        "provisionerId": "aws-provisioner-v1",
+        "workerType": "b2gtest",
+        "payload": {
+          "image": "bugzilla/bugzilla-ci",
+          "command": ["runtests.sh"],
+          "env": {
+            "BUGS_DB_DRIVER": "pg",
+            "TEST_SUITE": "selenium"
+          },
+          "artifacts": {
+            "public/runtests_log": {
+              "type": "file",
+              "path": "/tmp/runtests.log",
+              "expires": "2018-02-17T17:33:38.806Z"
+            },
+            "public/httpd_error_log": {
+              "type": "file",
+              "path": "/var/log/httpd/error_log",
+              "expires": "2018-02-17T17:33:38.806Z"
+            },
+            "public/selenium_log": {
+              "type": "file",
+              "path": "/tmp/selenium.log",
+              "expires": "2018-02-17T17:33:38.806Z"
+            }
+          }
+        },
+        "extra": {
+          "treeherder": {
+            "symbol": "Sel-Pg",
+            "machine": {
+                "platform": "linux64"
+            },
+            "build": {
+                "platform": "linux64"
+            }
+          }
+        }
+      }
+    }
+  ]
+}
diff --git a/template/en/default/admin/groups/list.html.tmpl b/template/en/default/admin/groups/list.html.tmpl
index af7da33a6..859f26205 100644
--- a/template/en/default/admin/groups/list.html.tmpl
+++ b/template/en/default/admin/groups/list.html.tmpl
@@ -74,7 +74,8 @@
    } 
 %]
 
-[% FOREACH group IN ["chartgroup", "insidergroup", "timetrackinggroup", "querysharegroup"] %]
+[% FOREACH group IN ["chartgroup", "insidergroup", "timetrackinggroup",
+                     "querysharegroup", "debug_group"] %]
   [% special_group = Param(group) %]
 
   [% IF special_group %]
diff --git a/template/en/default/admin/products/edit.html.tmpl b/template/en/default/admin/products/edit.html.tmpl
index c38530125..a4fcd188f 100644
--- a/template/en/default/admin/products/edit.html.tmpl
+++ b/template/en/default/admin/products/edit.html.tmpl
@@ -42,12 +42,12 @@
       </th>
       <td>
         [% IF product.components.size -%]
-          [% FOREACH component = product.components %]
+          [% FOREACH comp = product.components %]
             <a href="editcomponents.cgi?action=edit&product=
             [%- product.name FILTER uri %]&component=
-            [%- component.name FILTER uri %]">[% component.name FILTER html %]</a>:&nbsp;
-            [% IF component.description %]
-              [% component.description FILTER html_light %]
+            [%- comp.name FILTER uri %]">[% comp.name FILTER html %]</a>:&nbsp;
+            [% IF comp.description %]
+              [% comp.description FILTER html_light %]
             [% ELSE %]
               <font color="red">description missing</font>
             [% END %]
diff --git a/template/en/default/admin/users/responsibilities.html.tmpl b/template/en/default/admin/users/responsibilities.html.tmpl
index 9e6e48c6a..67ea7d294 100644
--- a/template/en/default/admin/users/responsibilities.html.tmpl
+++ b/template/en/default/admin/users/responsibilities.html.tmpl
@@ -23,26 +23,26 @@
       <th>Default QA Contact</th>
       <th>Default CC</th>
     </tr>
-    [% FOREACH component = item.components %]
+    [% FOREACH comp = item.components %]
       <tr>
         <td>
-          [% IF user.in_group("editcomponents", component.product_id) %]
+          [% IF user.in_group("editcomponents", comp.product_id) %]
             <a href="editcomponents.cgi?action=edit&amp;product=
                      [% item.product.name FILTER uri %]&amp;component=
-                     [% component.name FILTER uri %]">
+                     [% comp.name FILTER uri %]">
           [% END %]
-          [% component.name FILTER html %]
-          [% IF user.in_group("editcomponents", component.product_id) %]
+          [% comp.name FILTER html %]
+          [% IF user.in_group("editcomponents", comp.product_id) %]
             </a>
           [% END %]
         </td>
         [% FOREACH responsibility = ['default_assignee', 'default_qa_contact'] %]
           <td class="center">
-            [% component.$responsibility.id == otheruser.id ? "X" : "&nbsp;" %]
+            [% comp.$responsibility.id == otheruser.id ? "X" : "&nbsp;" %]
           </td>
         [% END %]
         <td class="center">
-          [% component.initial_cc.contains(otheruser) ? "X" : "&nbsp;" %]
+          [% comp.initial_cc.contains(otheruser) ? "X" : "&nbsp;" %]
         </td>
       </tr>
     [% END %]
diff --git a/template/en/default/bug/dependency-tree.html.tmpl b/template/en/default/bug/dependency-tree.html.tmpl
index a4bb0e672..f53663685 100644
--- a/template/en/default/bug/dependency-tree.html.tmpl
+++ b/template/en/default/bug/dependency-tree.html.tmpl
@@ -152,9 +152,12 @@
 [% END %]
 
 [% BLOCK buginfo %]
-  [% display_value("bug_status", bug.bug_status) FILTER html -%] [%+ display_value("resolution", bug.resolution) FILTER html %];
+  [% display_value("bug_status", bug.bug_status) FILTER html -%]
+  [%- IF bug.resolution %] [%+ display_value("resolution", bug.resolution) FILTER html %][% END %];
   [%-%] assigned to [% bug.assigned_to.login FILTER email FILTER html %]
-  [%-%][% "; Target: " _ bug.target_milestone IF bug.target_milestone %]
+  [% IF Param("usetargetmilestone") AND bug.target_milestone %]
+    [%-%]; target: [% bug.target_milestone FILTER html %]
+  [% END %]
 [% END %]
 
 [%###########################################################################%]
diff --git a/template/en/default/config.rdf.tmpl b/template/en/default/config.rdf.tmpl
index b14d0d056..0d183cf56 100644
--- a/template/en/default/config.rdf.tmpl
+++ b/template/en/default/config.rdf.tmpl
@@ -139,8 +139,8 @@
             [% END %]
             <bz:components>
               <Seq>
-                [% FOREACH component = product.components %]
-                  <li resource="[% escaped_urlbase %]component.cgi?name=[% component.name FILTER uri
+                [% FOREACH comp = product.components %]
+                  <li resource="[% escaped_urlbase %]component.cgi?name=[% comp.name FILTER uri
                       %]&amp;product=[% product.name FILTER uri %]"/>
                 [% END %]
               </Seq>
@@ -176,16 +176,16 @@
   <bz:components>
     <Seq>
       [% FOREACH product = products %]
-        [% FOREACH component = product.components %]
+        [% FOREACH comp = product.components %]
           <li>
-            <bz:component rdf:about="[% escaped_urlbase %]component.cgi?name=[% component.name FILTER uri
+            <bz:component rdf:about="[% escaped_urlbase %]component.cgi?name=[% comp.name FILTER uri
                           %]&amp;product=[% product.name FILTER uri %]">
-              <bz:name>[% component.name FILTER html %]</bz:name>
-              <bz:is_active>[% component.is_active FILTER html %]</bz:is_active>
+              <bz:name>[% comp.name FILTER html %]</bz:name>
+              <bz:is_active>[% comp.is_active FILTER html %]</bz:is_active>
               [% IF show_flags %]
                 <bz:flag_types>
                   <Seq>
-                    [% flag_types = component.flag_types.bug.merge(component.flag_types.attachment) %]
+                    [% flag_types = comp.flag_types.bug.merge(comp.flag_types.attachment) %]
                     [% FOREACH flag_type = flag_types %]
                       [% NEXT UNLESS flag_type.is_active %]
                       [% all_visible_flag_types.${flag_type.id} = flag_type %]
diff --git a/template/en/default/email/flagmail.txt.tmpl b/template/en/default/email/flagmail.txt.tmpl
index 169dfa892..037673dfc 100644
--- a/template/en/default/email/flagmail.txt.tmpl
+++ b/template/en/default/email/flagmail.txt.tmpl
@@ -64,11 +64,14 @@ Attachment [% attidsummary %]
 
 [%- FILTER bullet = wrap(80) %]
 
-[% USE Bugzilla %]
-[%-# .defined is necessary to avoid a taint issue in Perl < 5.10.1, see bug 509794. %]
-[% IF Bugzilla.cgi.param("comment").defined && Bugzilla.cgi.param("comment").length > 0 %]
-------- Additional Comments from [% user.identity %]
-[%+ Bugzilla.cgi.param("comment") FILTER strip_control_chars %]
+[% FOREACH comment = new_comments %]
+
+[%- IF comment.count %]
+--- Comment #[% comment.count %] from [% comment.author.identity %] ---
+[% ELSE %]
+--- Description ---
+[% END %]
+[%+ comment.body_full({ is_bugmail => 1, wrap => 1 }) FILTER strip_control_chars %]
 [% END %]
 
 [%- END %]
diff --git a/template/en/default/filterexceptions.pl b/template/en/default/filterexceptions.pl
index 189862527..e37fec1a7 100644
--- a/template/en/default/filterexceptions.pl
+++ b/template/en/default/filterexceptions.pl
@@ -170,7 +170,6 @@
 ],
 
 'global/messages.html.tmpl' => [
-  'message_tag', 
   'series.frequency * 2',
 ],
 
diff --git a/template/en/default/global/code-error.html.tmpl b/template/en/default/global/code-error.html.tmpl
index 086f34ab6..e4416326b 100644
--- a/template/en/default/global/code-error.html.tmpl
+++ b/template/en/default/global/code-error.html.tmpl
@@ -219,7 +219,8 @@
     but rather <em>[% target_type FILTER html %]</em>.
   
   [% ELSIF error == "invalid_field_name" %]
-    Can't use [% field FILTER html %] as a field name.
+    [% title = "Invalid Field Name" %]
+    Can't use "[% field.truncate(30, "...") FILTER html %]" as a field name.
 
   [% ELSIF error == "jobqueue_insert_failed" %]
    [% title = "Job Queue Failure" %]
@@ -384,8 +385,9 @@
     Invalid setting for post_bug_submit_action
 
   [% ELSIF error == "search_field_operator_unsupported" %]
+    [% title = "Invalid Search Type" %]
     [% terms.Bugzilla %] does not support the search type
-    "[% operator FILTER html %]".
+    "[% operator.truncate(30, "...") FILTER html %]".
 
   [% ELSE %]
     [%# Try to find hooked error messages %]
diff --git a/template/en/default/global/messages.html.tmpl b/template/en/default/global/messages.html.tmpl
index f55ab92a4..ba961c392 100644
--- a/template/en/default/global/messages.html.tmpl
+++ b/template/en/default/global/messages.html.tmpl
@@ -918,7 +918,7 @@
 [% IF !message %]
   [% message = BLOCK %]
     You are using [% terms.Bugzilla %]'s messaging functions incorrectly. You
-    passed in the string '[% message_tag %]'. The correct use is to pass
+    passed in the string '[% message_tag FILTER html %]'. The correct use is to pass
     in a tag, and define that tag in the file <kbd>messages.html.tmpl</kbd>.<br>
     <br>
     If you are a [% terms.Bugzilla %] end-user seeing this message, please
diff --git a/template/en/default/global/tabs.html.tmpl b/template/en/default/global/tabs.html.tmpl
index 454066889..dc9ca4c0a 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/user-error.html.tmpl b/template/en/default/global/user-error.html.tmpl
index 8dbcd45e3..492c2ad4b 100644
--- a/template/en/default/global/user-error.html.tmpl
+++ b/template/en/default/global/user-error.html.tmpl
@@ -1725,10 +1725,11 @@
     then try again.
 
   [% ELSIF error == "unknown_action" %]
+    [% title = "Unknown Action" %]
     [% IF action %]
-       Unknown action [% action FILTER html %]!
+      Unknown action "[% action.truncate(20, "...") FILTER html %]"!
     [% ELSE %]
-       I could not figure out what you wanted to do.
+      I could not figure out what you wanted to do.
     [% END %]
 
   [% ELSIF error == "unknown_tab" %]
diff --git a/template/en/default/index.html.tmpl b/template/en/default/index.html.tmpl
index b47b912c0..87cfa5921 100644
--- a/template/en/default/index.html.tmpl
+++ b/template/en/default/index.html.tmpl
@@ -58,6 +58,12 @@ YAHOO.util.Event.onDOMReady(onLoadActions);
 [% 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>[% terms.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/pages/release-notes.html.tmpl b/template/en/default/pages/release-notes.html.tmpl
index dfd5c6472..bcec5a13e 100644
--- a/template/en/default/pages/release-notes.html.tmpl
+++ b/template/en/default/pages/release-notes.html.tmpl
@@ -45,6 +45,181 @@
 
 <h2 id="v44_point">Updates in this 4.4.x Release</h2>
 
+<h3>4.4.14</h3>
+
+<p>This release fixes two security issues. See the
+  <a href="https://www.bugzilla.org/security/4.4.13/">Security Advisory</a>
+  for details.</p>
+
+<div style="border: 3px solid red; padding: 0.5em;">
+This will be the last release of the [% terms.Bugzilla %] 4.4 branch unless
+further security issues are found before the EOL date passes.
+[%+ terms.Bugzilla %] 4.4 will reach end of life 4 months after the release of
+Version 4.4.14. Please take the opportunity to upgrade to version 5.2 or newer.
+</div>
+
+<p>This release also contains the following [% terms.bug %] fixes:</p>
+
+<ul>
+  <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>
+
+</ul>
+
+<h3>4.4.13</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 %] fix:</p>
+
+<ul>
+  <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>4.4.12</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>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>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>
+</ul>
+
+<h3>4.4.11</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 %] fix:</p>
+
+<ul>
+  <li>mod_perl now works correctly with mod_access_compat turned off on
+    Apache 2.4. The (incorrect) fix implemented in [% terms.Bugzilla %] 4.4.9
+    has been backed out. 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>
+</ul>
+
+<h3>4.4.10</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>The <kbd>email_enabled</kbd> attribute passed to the <kbd>User.update</kbd>
+    WebService method was incorrectly taken into account. Its logic was reversed.
+    (<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1162334">[% terms.Bug %] 1162334</a>)</li>
+  <li>The <kbd>DateTime::TimeZone::Local::Win32</kbd> Perl module is now required
+    on Windows to correctly determine the local timezone.
+    (<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1124401">[% terms.Bug %] 1124401</a>)</li>
+  <li>[% terms.Bugzilla %] is now protected against the billion laughs attack
+    which could cause a denial of service when using the XML-RPC API.
+    (<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1031035">[% terms.Bug %] 1031035</a>)</li>
+</ul>
+
+<h3>4.4.9</h3>
+
+<p>This release contains the following [% terms.bug %] fixes:</p>
+
+<ul>
+  <li>Users who are not in the insidergroup were able to determine if some
+    specific user made a private comment in [% terms.bugs %].
+    (<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1151290">[% terms.Bug %] 1151290</a>)</li>
+  <li>Due to a regression caused by <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1090275">
+    [% terms.bug %] 1090275</a>, the WebService methods <kbd>B[%%]ug.get_bugs</kbd> and
+    <kbd>B[%%]ug.get_history</kbd> were no longer allowed.
+    (<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1154099">[% terms.Bug %] 1154099</a>)</li>
+  <li>[% terms.Bugzilla %] now supports the new <kbd>.htaccess</kbd> format from Apache 2.4.
+    (<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1121477">[% terms.Bug %] 1121477</a>)</li>
+  <li>A regression in [% terms.Bugzilla %] 4.4.3 due to CVE-2014-1517 caused the admin's password
+    to be ignored when starting a sudo session.
+    (<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1132887">[% terms.Bug %] 1132887</a>)</li>
+</ul>
+
+<h3>4.4.8</h3>
+
+<p>This release contains the following [% terms.bug %] fix:</p>
+
+<ul>
+  <li>Fixing a regression caused by <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1090275">
+    [% terms.bug %] 1090275</a>, <kbd>JSON-RPC</kbd> API calls could crash in
+    certain cases instead of displaying the proper error message.
+    (<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1124716">[% terms.Bug %] 1124716</a>)</li>
+</ul>
+
+<h3>4.4.7</h3>
+
+<p>This release contains fixes for a couple of security issues.
+  See the <a href="https://www.bugzilla.org/security/4.0.15/">
+  Security Advisory</a> for details.</p>
+
+<p>In addition, the following important fixes have been made in the release:</p>
+
+<ul>
+  <li>The <kbd>B[%%]ug.add_comment</kbd> WebService method now returns the correct
+    ID for the newly created [% terms.bug %] comment.
+    (<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1111043">[% terms.Bug %] 1111043</a>)</li>
+  <li>Fixing a regression caused by CVE-2014-1571
+    (<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1064140">[% terms.bug %] 1064140</a>),
+    comments made while setting a flag from the attachment details page are again
+    included in the flag notification email.
+    (<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1082887">[% terms.Bug %] 1082887</a>)</li>
+</ul>
+
+<h3>4.4.6</h3>
+
+<p>This release fixes several security issues. See the
+  <a href="http://www.bugzilla.org/security/4.0.14/">Security Advisory</a>
+  for details.</p>
+
+<h3>4.4.5</h3>
+
+<p>This release fixes a security issue. See the
+  <a href="http://www.bugzilla.org/security/4.0.13/">Security Advisory</a>
+  for details.</p>
+
 <h3>4.4.4</h3>
 
 <p>This release fixes one regression introduced in [% terms.Bugzilla %] 4.4.3 by
@@ -2305,7 +2480,7 @@
 
 [% BLOCK db_req %]
   [% SET m = DB_MODULE.$db %]
-  <h3 id="v40_req_[% db FILTER html %]">For [% m.name FILTER html %] Users</h3>
+  <h3 id="v44_req_[% db FILTER html %]">For [% m.name FILTER html %] Users</h3>
 
   <ul>
     <li>[% m.name FILTER html %]
diff --git a/template/en/default/reports/chart.html.tmpl b/template/en/default/reports/chart.html.tmpl
index ab334639c..1e908d956 100644
--- a/template/en/default/reports/chart.html.tmpl
+++ b/template/en/default/reports/chart.html.tmpl
@@ -20,6 +20,12 @@
   header_addl_info = time
 %]
 
+[% IF debug %]
+  <p>Bugzilla::Chart object:</p>
+  <pre>
+  [% debug_dump FILTER html %]
+  </pre>
+[% END %]
 <div align="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 471a9cb55..543d8bd33 100644
--- a/template/en/default/reports/create-chart.html.tmpl
+++ b/template/en/default/reports/create-chart.html.tmpl
@@ -17,6 +17,13 @@
   title = "Create Chart"
 %]
 
+[% 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-table.csv.tmpl b/template/en/default/reports/report-table.csv.tmpl
index e2a92b51d..e94014b92 100644
--- a/template/en/default/reports/report-table.csv.tmpl
+++ b/template/en/default/reports/report-table.csv.tmpl
@@ -23,11 +23,13 @@
   [% END %]
   [% tbl_field_disp FILTER csv %]: [% tbl_disp FILTER csv %]
 [% END %]
-[% IF row_field %]
+[% IF row_field && col_field %]
+  [% row_field_disp _ ' / ' _ col_field_disp FILTER csv %]
+[% ELSIF row_field %]
   [% row_field_disp FILTER csv %]
+[% ELSE %]
+  [% col_field_disp FILTER csv %]
 [% END %]
-[% " / " IF col_field AND row_field %]
-[% col_field_disp FILTER csv %]
 [% IF col_field -%]
   [% FOREACH col = col_names -%]
     [% colsepchar %]
diff --git a/template/en/default/reports/report.html.tmpl b/template/en/default/reports/report.html.tmpl
index 2ca5dd90f..4825e0a66 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/search/search-specific.html.tmpl b/template/en/default/search/search-specific.html.tmpl
index 1093f70bc..f09d4bdc0 100644
--- a/template/en/default/search/search-specific.html.tmpl
+++ b/template/en/default/search/search-specific.html.tmpl
@@ -27,7 +27,7 @@ for "crash secure SSL flash".
 
 <form name="queryform" method="get" action="buglist.cgi">
 <input type="hidden" name="query_format" value="specific">
-<input type="hidden" name="order" value="relevance desc">
+<input type="hidden" name="order" value="Importance">
 <input type="hidden" id="no_redirect" name="no_redirect" value="0">
 <script type="text/javascript">
   if (history && history.replaceState) {
diff --git a/testserver.pl b/testserver.pl
index d296b730f..12cdfe5f0 100755
--- a/testserver.pl
+++ b/testserver.pl
@@ -37,9 +37,9 @@ my @pscmds = ('ps -eo comm,gid', 'ps -acxo command,gid', 'ps -acxo command,rgid'
 my $sgid = 0;
 if (!ON_WINDOWS) {
     foreach my $pscmd (@pscmds) {
-        open PH, "$pscmd 2>/dev/null |";
+        open PH, '-|', "$pscmd 2>/dev/null";
         while (my $line = <PH>) {
-            if ($line =~ /^(?:\S*\/)?(?:httpd|apache)2?\s+(\d+)$/) {
+            if ($line =~ /^(?:\S*\/)?(?:httpd|apache?)2?\s+(\d+)$/) {
                 $sgid = $1 if $1 > $sgid;
             }
         }
@@ -264,7 +264,7 @@ sub check_image {
 
 sub create_file {
     my ($filename, $content) = @_;
-    open(FH, ">$filename")
+    open(FH, ">", $filename)
         or die "Failed to create $filename: $!\n";
     binmode FH;
     print FH $content;
@@ -273,7 +273,7 @@ sub create_file {
 
 sub read_file {
     my ($filename) = @_;
-    open(FH, $filename)
+    open(FH, '<', $filename)
         or die "Failed to open $filename: $!\n";
     binmode FH;
     my $content = <FH>;
diff --git a/token.cgi b/token.cgi
index 62f1f5121..05cb30c9f 100755
--- a/token.cgi
+++ b/token.cgi
@@ -163,6 +163,7 @@ sub cancelChangePassword {
 # password and that the new password is valid.
 sub changePassword {
     my ($user_id, $token) = @_;
+    my $dbh = Bugzilla->dbh;
 
     my $password = $cgi->param('password');
     (defined $password && defined $cgi->param('matchpassword'))
@@ -176,6 +177,8 @@ sub changePassword {
     $user->set_password($password);
     $user->update();
     delete_token($token);
+    $dbh->do(q{DELETE FROM tokens WHERE userid = ?
+               AND tokentype = 'password'}, undef, $user_id);
 
     Bugzilla->logout_user_by_id($user_id);
 
@@ -254,7 +257,7 @@ sub cancelChangeEmail {
         # check to see if it has been altered
         if ($user->login ne $old_email) {
             $user->set_login($old_email);
-            $user->update({ keep_session => 1 });
+            $user->update({ keep_tokens => 1 });
 
             $vars->{'message'} = "email_change_canceled_reinstated";
         } 
@@ -306,7 +309,7 @@ sub confirm_create_account {
 
     my $otheruser = Bugzilla::User->create({
         login_name => $login_name, 
-        realname   => $cgi->param('realname'), 
+        realname   => scalar $cgi->param('realname'),
         cryptpassword => $password});
 
     # Now delete this token.
