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 @@
+
+
+#### Additional info
+
+* [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/.gitignore b/.gitignore
new file mode 100644
index 000000000..7ab83e7ad
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,32 @@
+.htaccess
+/lib/*
+/template/en/custom
+/docs/bugzilla.ent
+/docs/en/xml/bugzilla.ent
+/docs/en/txt
+/docs/en/html
+/docs/en/pdf
+/skins/custom
+/graphs
+/data
+/localconfig
+/index.html
+
+/skins/contrib/Dusk/IE-fixes.css
+/skins/contrib/Dusk/admin.css
+/skins/contrib/Dusk/attachment.css
+/skins/contrib/Dusk/create_attachment.css
+/skins/contrib/Dusk/dependency-tree.css
+/skins/contrib/Dusk/duplicates.css
+/skins/contrib/Dusk/editusers.css
+/skins/contrib/Dusk/enter_bug.css
+/skins/contrib/Dusk/help.css
+/skins/contrib/Dusk/panel.css
+/skins/contrib/Dusk/page.css
+/skins/contrib/Dusk/params.css
+/skins/contrib/Dusk/reports.css
+/skins/contrib/Dusk/show_bug.css
+/skins/contrib/Dusk/search_form.css
+/skins/contrib/Dusk/show_multiple.css
+/skins/contrib/Dusk/summarize-time.css
+.DS_Store
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
- deny from all
+
+
+ Deny from all
+
+ = 2.4>
+ Require all denied
+
+
+
+ Deny from all
+
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} = ;
@@ -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.pm b/Bugzilla/Auth.pm
index 477dbffaa..09a2c1da4 100644
--- a/Bugzilla/Auth.pm
+++ b/Bugzilla/Auth.pm
@@ -108,6 +108,15 @@ sub can_logout {
return $getter->can_logout;
}
+sub login_token {
+ my ($self) = @_;
+ my $getter = $self->{_info_getter}->{successful};
+ if ($getter && $getter->isa('Bugzilla::Auth::Login::Cookie')) {
+ return $getter->login_token;
+ }
+ return undef;
+}
+
sub user_can_create_account {
my ($self) = @_;
my $verifier = $self->{_verifier}->{successful};
@@ -143,7 +152,7 @@ sub _handle_login_result {
if ($self->{_info_getter}->{successful}->requires_persistence
and !Bugzilla->request_cache->{auth_no_automatic_login})
{
- $self->{_persister}->persist_login($user);
+ $user->{_login_token} = $self->{_persister}->persist_login($user);
}
}
elsif ($fail_code == AUTH_ERROR) {
@@ -409,6 +418,14 @@ Params: None
Returns: C if users can change their own email address,
C otherwise.
+=item C
+
+Description: If a login token was used instead of a cookie then this
+ will return the current login token data such as user id
+ and the token itself.
+Params: None
+Returns: A hash containing C and C.
+
=back
=head1 STRUCTURE
diff --git a/Bugzilla/Auth/Login/CGI.pm b/Bugzilla/Auth/Login/CGI.pm
index 47ec556a7..f29e8c9c1 100644
--- a/Bugzilla/Auth/Login/CGI.pm
+++ b/Bugzilla/Auth/Login/CGI.pm
@@ -14,19 +14,52 @@ use Bugzilla::Constants;
use Bugzilla::WebService::Constants;
use Bugzilla::Util;
use Bugzilla::Error;
+use Bugzilla::Token;
sub get_login_info {
my ($self) = @_;
my $params = Bugzilla->input_params;
+ my $cgi = Bugzilla->cgi;
+
+ my $login = trim(delete $params->{'Bugzilla_login'});
+ my $password = delete $params->{'Bugzilla_password'};
+ # The token must match the cookie to authenticate the request.
+ my $login_token = delete $params->{'Bugzilla_login_token'};
+ my $login_cookie = $cgi->cookie('Bugzilla_login_request_cookie');
- my $username = trim(delete $params->{"Bugzilla_login"});
- my $password = delete $params->{"Bugzilla_password"};
+ my $valid = 0;
+ # If the web browser accepts cookies, use them.
+ if ($login_token && $login_cookie) {
+ my ($time, undef) = split(/-/, $login_token);
+ # Regenerate the token based on the information we have.
+ my $expected_token = issue_hash_token(['login_request', $login_cookie], $time);
+ $valid = 1 if $expected_token eq $login_token;
+ $cgi->remove_cookie('Bugzilla_login_request_cookie');
+ }
+ # WebServices and other local scripts can bypass this check.
+ # This is safe because we won't store a login cookie in this case.
+ elsif (Bugzilla->usage_mode != USAGE_MODE_BROWSER) {
+ $valid = 1;
+ }
+ # Else falls back to the Referer header and accept local URLs.
+ # Attachments are served from a separate host (ideally), and so
+ # an evil attachment cannot abuse this check with a redirect.
+ elsif (my $referer = $cgi->referer) {
+ my $urlbase = correct_urlbase();
+ $valid = 1 if $referer =~ /^\Q$urlbase\E/;
+ }
+ # If the web browser doesn't accept cookies and the Referer header
+ # is missing, we have no way to make sure that the authentication
+ # request comes from the user.
+ elsif ($login && $password) {
+ ThrowUserError('auth_untrusted_request', { login => $login });
+ }
- if (!defined $username || !defined $password) {
+ if (!defined($login) || !defined($password) || !$valid) {
return { failure => AUTH_NODATA };
}
- return { username => $username, password => $password };
+ return { username => $login, password => $password };
}
sub fail_nodata {
diff --git a/Bugzilla/Auth/Login/Cookie.pm b/Bugzilla/Auth/Login/Cookie.pm
index 274e064ff..b20357307 100644
--- a/Bugzilla/Auth/Login/Cookie.pm
+++ b/Bugzilla/Auth/Login/Cookie.pm
@@ -6,8 +6,11 @@
# defined by the Mozilla Public License, v. 2.0.
package Bugzilla::Auth::Login::Cookie;
+
use strict;
+
use base qw(Bugzilla::Auth::Login);
+use fields qw(_login_token);
use Bugzilla::Constants;
use Bugzilla::Util;
@@ -17,7 +20,8 @@ use List::Util qw(first);
use constant requires_persistence => 0;
use constant requires_verification => 0;
use constant can_login => 0;
-use constant is_automatic => 1;
+
+sub is_automatic { return $_[0]->login_token ? 0 : 1; }
# Note that Cookie never consults the Verifier, it always assumes
# it has a valid DB account or it fails.
@@ -25,24 +29,35 @@ sub get_login_info {
my ($self) = @_;
my $cgi = Bugzilla->cgi;
my $dbh = Bugzilla->dbh;
+ my ($user_id, $login_cookie);
- my $ip_addr = remote_ip();
- my $login_cookie = $cgi->cookie("Bugzilla_logincookie");
- my $user_id = $cgi->cookie("Bugzilla_login");
+ if (!Bugzilla->request_cache->{auth_no_automatic_login}) {
+ $login_cookie = $cgi->cookie("Bugzilla_logincookie");
+ $user_id = $cgi->cookie("Bugzilla_login");
- # If cookies cannot be found, this could mean that they haven't
- # been made available yet. In this case, look at Bugzilla_cookie_list.
- unless ($login_cookie) {
- my $cookie = first {$_->name eq 'Bugzilla_logincookie'}
- @{$cgi->{'Bugzilla_cookie_list'}};
- $login_cookie = $cookie->value if $cookie;
+ # If cookies cannot be found, this could mean that they haven't
+ # been made available yet. In this case, look at Bugzilla_cookie_list.
+ unless ($login_cookie) {
+ my $cookie = first {$_->name eq 'Bugzilla_logincookie'}
+ @{$cgi->{'Bugzilla_cookie_list'}};
+ $login_cookie = $cookie->value if $cookie;
+ }
+ unless ($user_id) {
+ my $cookie = first {$_->name eq 'Bugzilla_login'}
+ @{$cgi->{'Bugzilla_cookie_list'}};
+ $user_id = $cookie->value if $cookie;
+ }
}
- unless ($user_id) {
- my $cookie = first {$_->name eq 'Bugzilla_login'}
- @{$cgi->{'Bugzilla_cookie_list'}};
- $user_id = $cookie->value if $cookie;
+
+ # If no cookies were provided, we also look for a login token
+ # passed in the parameters of a webservice
+ my $token = $self->login_token;
+ if ($token && (!$login_cookie || !$user_id)) {
+ ($user_id, $login_cookie) = ($token->{'user_id'}, $token->{'login_token'});
}
+ my $ip_addr = remote_ip();
+
if ($login_cookie && $user_id) {
# Anything goes for these params - they're just strings which
# we're going to verify against the db
@@ -75,4 +90,32 @@ sub get_login_info {
return { failure => AUTH_NODATA };
}
+sub login_token {
+ my ($self) = @_;
+ my $input = Bugzilla->input_params;
+ my $usage_mode = Bugzilla->usage_mode;
+
+ return $self->{'_login_token'} if exists $self->{'_login_token'};
+
+ if ($usage_mode ne USAGE_MODE_XMLRPC
+ && $usage_mode ne USAGE_MODE_JSON)
+ {
+ return $self->{'_login_token'} = undef;
+ }
+
+ # Check if a token was passed in via requests for WebServices
+ my $token = trim(delete $input->{'Bugzilla_token'});
+ return $self->{'_login_token'} = undef if !$token;
+
+ my ($user_id, $login_token) = split('-', $token, 2);
+ if (!detaint_natural($user_id) || !$login_token) {
+ return $self->{'_login_token'} = undef;
+ }
+
+ return $self->{'_login_token'} = {
+ user_id => $user_id,
+ login_token => $login_token
+ };
+}
+
1;
diff --git a/Bugzilla/Auth/Persist/Cookie.pm b/Bugzilla/Auth/Persist/Cookie.pm
index ec212088d..b0aeb4f0f 100644
--- a/Bugzilla/Auth/Persist/Cookie.pm
+++ b/Bugzilla/Auth/Persist/Cookie.pm
@@ -52,6 +52,10 @@ sub persist_login {
$dbh->bz_commit_transaction();
+ # We do not want WebServices to generate login cookies.
+ # All we need is the login token for User.login.
+ return $login_cookie if i_am_webservice();
+
# Prevent JavaScript from accessing login cookies.
my %cookieargs = ('-httponly' => 1);
@@ -84,6 +88,7 @@ sub logout {
my $dbh = Bugzilla->dbh;
my $cgi = Bugzilla->cgi;
+ my $input = Bugzilla->input_params;
$param = {} unless $param;
my $user = $param->{user} || Bugzilla->user;
my $type = $param->{type} || LOGOUT_ALL;
@@ -97,16 +102,24 @@ sub logout {
# The LOGOUT_*_CURRENT options require the current login cookie.
# If a new cookie has been issued during this run, that's the current one.
# If not, it's the one we've received.
+ my @login_cookies;
my $cookie = first {$_->name eq 'Bugzilla_logincookie'}
@{$cgi->{'Bugzilla_cookie_list'}};
- my $login_cookie;
if ($cookie) {
- $login_cookie = $cookie->value;
+ push(@login_cookies, $cookie->value);
+ }
+ elsif ($cookie = $cgi->cookie("Bugzilla_logincookie")) {
+ push(@login_cookies, $cookie);
}
- else {
- $login_cookie = $cgi->cookie("Bugzilla_logincookie");
+
+ # If we are a webservice using a token instead of cookie
+ # then add that as well to the login cookies to delete
+ if (my $login_token = $user->authorizer->login_token) {
+ push(@login_cookies, $login_token->{'login_token'});
}
- trick_taint($login_cookie);
+
+ # Make sure that @login_cookies is not empty to not break SQL statements.
+ push(@login_cookies, '') unless @login_cookies;
# These queries use both the cookie ID and the user ID as keys. Even
# though we know the userid must match, we still check it in the SQL
@@ -115,12 +128,18 @@ sub logout {
# logged in and got the same cookie, we could be logging the other
# user out here. Yes, this is very very very unlikely, but why take
# chances? - bbaetz
+ map { trick_taint($_) } @login_cookies;
+ @login_cookies = map { $dbh->quote($_) } @login_cookies;
if ($type == LOGOUT_KEEP_CURRENT) {
- $dbh->do("DELETE FROM logincookies WHERE cookie != ? AND userid = ?",
- undef, $login_cookie, $user->id);
+ $dbh->do("DELETE FROM logincookies WHERE " .
+ $dbh->sql_in('cookie', \@login_cookies, 1) .
+ " AND userid = ?",
+ undef, $user->id);
} elsif ($type == LOGOUT_CURRENT) {
- $dbh->do("DELETE FROM logincookies WHERE cookie = ? AND userid = ?",
- undef, $login_cookie, $user->id);
+ $dbh->do("DELETE FROM logincookies WHERE " .
+ $dbh->sql_in('cookie', \@login_cookies) .
+ " AND userid = ?",
+ undef, $user->id);
} else {
die("Invalid type $type supplied to logout()");
}
@@ -128,7 +147,6 @@ sub logout {
if ($type != LOGOUT_KEEP_CURRENT) {
clear_browser_cookies();
}
-
}
sub clear_browser_cookies {
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 0148180a7..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
@@ -511,8 +512,10 @@ sub possible_duplicates {
my $dbh = Bugzilla->dbh;
my $user = Bugzilla->user;
my @words = split(/[\b\s]+/, $short_desc || '');
- # Exclude punctuation from the array.
- @words = map { /(\w+)/; $1 } @words;
+ # Remove leading/trailing punctuation from words
+ foreach my $word (@words) {
+ $word =~ s/(?:^\W+|\W+$)//g;
+ }
# And make sure that each word is longer than 2 characters.
@words = grep { defined $_ and length($_) > 2 } @words;
@@ -910,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
@@ -938,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;
@@ -970,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};
@@ -2747,31 +2753,23 @@ sub add_comment {
push(@{$self->{added_comments}}, $params);
}
-# There was a lot of duplicate code when I wrote this as three separate
-# functions, so I just combined them all into one. This is also easier for
-# process_bug to use.
sub modify_keywords {
my ($self, $keywords, $action) = @_;
-
- $action ||= 'set';
- if (!grep($action eq $_, qw(add remove set))) {
+
+ if (!$action || !grep { $action eq $_ } qw(add remove set)) {
$action = 'set';
}
-
+
$keywords = $self->_check_keywords($keywords);
+ my @old_keywords = @{ $self->keyword_objects };
+ my @result;
- my (@result, $any_changes);
if ($action eq 'set') {
@result = @$keywords;
- # Check if anything was added or removed.
- my @old_ids = map { $_->id } @{$self->keyword_objects};
- my @new_ids = map { $_->id } @result;
- my ($removed, $added) = diff_arrays(\@old_ids, \@new_ids);
- $any_changes = scalar @$removed || scalar @$added;
}
else {
# We're adding or deleting specific keywords.
- my %keys = map {$_->id => $_} @{$self->keyword_objects};
+ my %keys = map { $_->id => $_ } @old_keywords;
if ($action eq 'add') {
$keys{$_->id} = $_ foreach @$keywords;
}
@@ -2779,11 +2777,17 @@ sub modify_keywords {
delete $keys{$_->id} foreach @$keywords;
}
@result = values %keys;
- $any_changes = scalar @$keywords;
}
+
+ # Check if anything was added or removed.
+ my @old_ids = map { $_->id } @old_keywords;
+ my @new_ids = map { $_->id } @result;
+ my ($removed, $added) = diff_arrays(\@old_ids, \@new_ids);
+ my $any_changes = scalar @$removed || scalar @$added;
+
# Make sure we retain the sort order.
@result = sort {lc($a->name) cmp lc($b->name)} @result;
-
+
if ($any_changes) {
my $privs;
my $new = join(', ', (map {$_->name} @result));
@@ -3776,17 +3780,24 @@ sub editable_bug_fields {
# Join with bug_status and bugs tables to show bugs with open statuses first,
# and then the others
sub EmitDependList {
- my ($myfield, $targetfield, $bug_id) = (@_);
+ my ($my_field, $target_field, $bug_id, $exclude_resolved) = @_;
+ my $cache = Bugzilla->request_cache->{bug_dependency_list} ||= {};
+
my $dbh = Bugzilla->dbh;
- my $list_ref = $dbh->selectcol_arrayref(
- "SELECT $targetfield
+ $exclude_resolved = $exclude_resolved ? 1 : 0;
+ my $is_open_clause = $exclude_resolved ? 'AND is_open = 1' : '';
+
+ $cache->{"${target_field}_sth_$exclude_resolved"} ||= $dbh->prepare(
+ "SELECT $target_field
FROM dependencies
- INNER JOIN bugs ON dependencies.$targetfield = bugs.bug_id
+ INNER JOIN bugs ON dependencies.$target_field = bugs.bug_id
INNER JOIN bug_status ON bugs.bug_status = bug_status.value
- WHERE $myfield = ?
- ORDER BY is_open DESC, $targetfield",
- undef, $bug_id);
- return $list_ref;
+ WHERE $my_field = ? $is_open_clause
+ ORDER BY is_open DESC, $target_field");
+
+ return $dbh->selectcol_arrayref(
+ $cache->{"${target_field}_sth_$exclude_resolved"},
+ undef, $bug_id);
}
# Creates a lot of bug objects in the same order as the input array.
@@ -3932,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
@@ -3957,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 442d19fcd..644ed1f1c 100644
--- a/Bugzilla/BugMail.pm
+++ b/Bugzilla/BugMail.pm
@@ -87,15 +87,17 @@ sub Send {
if ($params->{dep_only}) {
push(@diffs, { field_name => 'bug_status',
- old => $params->{changes}->{bug_status}->[0],
- new => $params->{changes}->{bug_status}->[1],
+ old => $params->{changes}->{bug_status}->[0],
+ new => $params->{changes}->{bug_status}->[1],
login_name => $changer->login,
- blocker => $params->{blocker} },
+ who => $changer,
+ blocker => $params->{blocker} },
{ field_name => 'resolution',
- old => $params->{changes}->{resolution}->[0],
- new => $params->{changes}->{resolution}->[1],
+ old => $params->{changes}->{resolution}->[0],
+ new => $params->{changes}->{resolution}->[1],
login_name => $changer->login,
- blocker => $params->{blocker} });
+ who => $changer,
+ blocker => $params->{blocker} });
}
else {
push(@diffs, _get_diffs($bug, $end, \%user_cache));
@@ -385,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'};
@@ -494,7 +497,10 @@ sub _get_new_bugmail_fields {
# If there isn't anything to show, don't include this header.
next unless $value;
- push(@diffs, {field_name => $name, new => $value});
+ push(@diffs, {
+ field_name => $name,
+ who => $bug->reporter,
+ new => $value});
}
return @diffs;
diff --git a/Bugzilla/CGI.pm b/Bugzilla/CGI.pm
index b67f9f751..19332b17a 100644
--- a/Bugzilla/CGI.pm
+++ b/Bugzilla/CGI.pm
@@ -265,18 +265,110 @@ sub multipart_start {
$headers .= "Set-Cookie: ${cookie}${CGI::CRLF}";
}
$headers .= $CGI::CRLF;
+ $self->{_multipart_in_progress} = 1;
return $headers;
}
+sub close_standby_message {
+ my ($self, $contenttype, $disposition) = @_;
+
+ if ($self->{_multipart_in_progress}) {
+ print $self->multipart_end();
+ print $self->multipart_start(-type => $contenttype,
+ -content_disposition => $disposition);
+ }
+ else {
+ print $self->header(-type => $contenttype,
+ -content_disposition => $disposition);
+ }
+}
+
+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;
+ my $user = Bugzilla->user;
# If there's only one parameter, then it's a Content-Type.
if (scalar(@_) == 1) {
# 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'))
+ {
+ my %args;
+ $args{'-secure'} = 1 if Bugzilla->params->{ssl_redirect};
+
+ $self->send_cookie(-name => 'Bugzilla_login_request_cookie',
+ -value => generate_random_password(),
+ -httponly => 1,
+ %args);
+ }
# Add the cookies in if we have any
if (scalar(@{$self->{Bugzilla_cookie_list}})) {
@@ -316,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) {
@@ -632,6 +725,15 @@ instead of calling this directly.
Redirects from the current URL to one prefixed by the urlbase parameter.
+=item C
+
+Starts a new part of the multipart document using the specified MIME type.
+If not specified, text/html is assumed.
+
+=item C
+
+Ends a part of the multipart document, and starts another part.
+
=back
=head1 SEE ALSO
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 "
";
+ return Data::Dumper::Dumper($self);
}
1;
diff --git a/Bugzilla/Component.pm b/Bugzilla/Component.pm
index eb8341d08..1ce4e02ea 100644
--- a/Bugzilla/Component.pm
+++ b/Bugzilla/Component.pm
@@ -417,10 +417,10 @@ use constant is_default => 0;
sub is_set_on_bug {
my ($self, $bug) = @_;
- # We treat it like a hash always, so that we don't have to check if it's
- # a hash or an object.
- return 0 if !defined $bug->{component_id};
- $bug->{component_id} == $self->id ? 1 : 0;
+ my $value = blessed($bug) ? $bug->component_id : $bug->{component};
+ $value = $value->id if blessed($value);
+ return 0 unless $value;
+ return $value == $self->id ? 1 : 0;
}
###############################
@@ -506,7 +506,7 @@ Component.pm represents a Product Component object.
Returns: Integer with the number of bugs.
-=item C
+=item C
Description: Returns all bug IDs that belong to the component.
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\$/,)) {
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 9f4364338..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.1";
+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';
@@ -592,6 +592,13 @@ use constant AUDIT_CREATE => '__create__';
use constant AUDIT_REMOVE => '__remove__';
sub bz_locations {
+ # Force memoize() to re-compute data per project, to avoid
+ # sharing the same data across different installations.
+ return _bz_locations($ENV{'PROJECT'});
+}
+
+sub _bz_locations {
+ my $project = shift;
# We know that Bugzilla/Constants.pm must be in %INC at this point.
# So the only question is, what's the name of the directory
# above it? This is the most reliable way to get our current working
@@ -608,12 +615,13 @@ sub bz_locations {
$libpath =~ /(.*)/;
$libpath = $1;
- my ($project, $localconfig, $datadir);
- if ($ENV{'PROJECT'} && $ENV{'PROJECT'} =~ /^(\w+)$/) {
+ my ($localconfig, $datadir);
+ if ($project && $project =~ /^(\w+)$/) {
$project = $1;
$localconfig = "localconfig.$project";
$datadir = "data/$project";
} else {
+ $project = undef;
$localconfig = "localconfig";
$datadir = "data";
}
@@ -648,6 +656,6 @@ sub bz_locations {
# This makes us not re-compute all the bz_locations data every time it's
# called.
-BEGIN { memoize('bz_locations') };
+BEGIN { memoize('_bz_locations') };
1;
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 e9ed13b2b..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) {
@@ -92,6 +93,10 @@ sub new {
# into bugs_fulltext).
$self->do('SET SESSION group_concat_max_len = 128000000');
+ # MySQL 5.5.2 and older have this variable set to true, which causes
+ # trouble, see bug 870369.
+ $self->do('SET SESSION sql_auto_is_null = 0');
+
return $self;
}
diff --git a/Bugzilla/DB/Sqlite.pm b/Bugzilla/DB/Sqlite.pm
index e0197402f..3470ffc12 100644
--- a/Bugzilla/DB/Sqlite.pm
+++ b/Bugzilla/DB/Sqlite.pm
@@ -213,8 +213,9 @@ sub sql_to_days {
sub sql_date_format {
my ($self, $date, $format) = @_;
- $format = "%Y.%m.%d %H:%M:%s" if !$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 e1df5ddbb..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;
}
@@ -92,8 +92,10 @@ sub _throw_error {
message => \$message });
if (Bugzilla->error_mode == ERROR_MODE_WEBPAGE) {
- print Bugzilla->cgi->header();
+ my $cgi = Bugzilla->cgi;
+ $cgi->close_standby_message('text/html', 'inline');
print $message;
+ print $cgi->multipart_final() if $cgi->{_multipart_in_progress};
}
elsif (Bugzilla->error_mode == ERROR_MODE_TEST) {
die Dumper($vars);
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 => <
+
+ Deny from all
+
+ = 2.4>
+ Require all denied
+
+
+
+ Deny from all
+
EOT
###############
@@ -329,11 +339,31 @@ EOT
"$graphsdir/.htaccess" => { perms => WS_SERVE, contents => <
- Allow from all
+
+
+ Allow from all
+
+ = 2.4>
+ Require all granted
+
+
+
+ Allow from all
+
# And no directory listings, either.
-Deny from all
+
+
+ Deny from all
+
+ = 2.4>
+ Require all denied
+
+
+
+ Deny from all
+
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
- Allow from 192.20.225.0/24
- Deny from all
+
+
+ Allow from 192.20.225.0/24
+ Deny from all
+
+ = 2.4>
+ Require ip 192.20.225.0/24
+ Require all denied
+
+
+
+ Allow from 192.20.225.0/24
+ Deny from all
+
# Allow access to .png files created by a local copy of 'dot'
- Allow from all
+
+
+ Allow from all
+
+ = 2.4>
+ Require all granted
+
+
+
+ Allow from all
+
# And no directory listings, either.
-Deny from all
+
+
+ Deny from all
+
+ = 2.4>
+ Require all denied
+
+
+
+ Deny from all
+
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 82f340394..16a06d5eb 100644
--- a/Bugzilla/Install/Requirements.pm
+++ b/Bugzilla/Install/Requirements.pm
@@ -14,6 +14,7 @@ package Bugzilla::Install::Requirements;
# MUST NOT "use."
use strict;
+use version;
use Bugzilla::Constants;
use Bugzilla::Install::Util qw(vers_cmp install_string bin_loc
@@ -128,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).
{
@@ -140,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',
@@ -167,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.
@@ -178,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');
@@ -199,7 +215,9 @@ sub OPTIONAL_MODULES {
package => 'Chart',
module => 'Chart::Lines',
# Versions below 2.1 cannot be detected accurately.
- version => '2.1',
+ # There is no 2.1.0 release (it was 2.1), but .0 is required to fix
+ # https://rt.cpan.org/Public/Bug/Display.html?id=28218.
+ version => '2.1.0',
feature => [qw(new_charts old_charts)],
},
{
@@ -365,6 +383,12 @@ sub OPTIONAL_MODULES {
version => 0,
feature => ['jobqueue'],
},
+ {
+ package => 'File-Slurp',
+ module => 'File::Slurp',
+ version => '9999.13',
+ feature => ['jobqueue'],
+ },
# mod_perl
{
@@ -665,8 +689,8 @@ sub check_graphviz {
return $return;
}
-# This was originally clipped from the libnet Makefile.PL, adapted here to
-# use the below vers_cmp routine for accurate version checking.
+# This was originally clipped from the libnet Makefile.PL, adapted here for
+# accurate version checking.
sub have_vers {
my ($params, $output) = @_;
my $module = $params->{module};
@@ -691,21 +715,17 @@ sub have_vers {
if ($@) {
no strict 'refs';
$vnum = ${"${module}::VERSION"};
- }
- $vnum ||= -1;
- # CGI's versioning scheme went 2.75, 2.751, 2.752, 2.753, 2.76
- # That breaks the standard version tests, so we need to manually correct
- # the version
- if ($module eq 'CGI' && $vnum =~ /(2\.7\d)(\d+)/) {
- $vnum = $1 . "." . $2;
- }
- # CPAN did a similar thing, where it has versions like 1.9304.
- if ($module eq 'CPAN' and $vnum =~ /^(\d\.\d{2})\d{2}$/) {
- $vnum = $1;
+ # If we come here, then the version is not a valid one.
+ # We try to sanitize it.
+ if ($vnum =~ /^((\d+)(\.\d+)*)/) {
+ $vnum = $1;
+ }
}
+ $vnum ||= -1;
- my $vok = (vers_cmp($vnum,$wanted) > -1);
+ # Must do a string comparison as $vnum may be of the form 5.10.1.
+ my $vok = ($vnum ne '-1' && version->new($vnum) >= version->new($wanted)) ? 1 : 0;
my $blacklisted;
if ($vok && $params->{blacklist}) {
$blacklisted = grep($vnum =~ /$_/, @{$params->{blacklist}});
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/Milestone.pm b/Bugzilla/Milestone.pm
index caa4afcdd..b4ddaeafe 100644
--- a/Bugzilla/Milestone.pm
+++ b/Bugzilla/Milestone.pm
@@ -97,10 +97,12 @@ sub run_create_validators {
sub update {
my $self = shift;
+ my $dbh = Bugzilla->dbh;
+
+ $dbh->bz_start_transaction();
my $changes = $self->SUPER::update(@_);
if (exists $changes->{value}) {
- my $dbh = Bugzilla->dbh;
# The milestone value is stored in the bugs table instead of its ID.
$dbh->do('UPDATE bugs SET target_milestone = ?
WHERE target_milestone = ? AND product_id = ?',
@@ -111,6 +113,8 @@ sub update {
WHERE id = ? AND defaultmilestone = ?',
undef, ($self->name, $self->product_id, $changes->{value}->[0]));
}
+ $dbh->bz_commit_transaction();
+
return $changes;
}
@@ -118,6 +122,8 @@ sub remove_from_db {
my $self = shift;
my $dbh = Bugzilla->dbh;
+ $dbh->bz_start_transaction();
+
# The default milestone cannot be deleted.
if ($self->name eq $self->product->default_milestone) {
ThrowUserError('milestone_is_default', { milestone => $self });
@@ -146,8 +152,9 @@ sub remove_from_db {
Bugzilla->user->id, $timestamp);
}
}
-
$self->SUPER::remove_from_db();
+
+ $dbh->bz_commit_transaction();
}
################################
diff --git a/Bugzilla/Search.pm b/Bugzilla/Search.pm
index b2c703e5b..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) {
@@ -1951,16 +1966,30 @@ sub _quote_unless_numeric {
my $numeric_field = $self->_chart_fields->{$field}->is_numeric;
my $numeric_value = ($value =~ NUMBER_REGEX) ? 1 : 0;
my $is_numeric = $numeric_operator && $numeric_field && $numeric_value;
+
+ # These operators are really numeric operators with numeric fields.
+ $numeric_operator = grep { $_ eq $operator } keys %{ SIMPLE_OPERATORS() };
+
if ($is_numeric) {
my $quoted = $value;
trick_taint($quoted);
return $quoted;
}
+ elsif ($numeric_field && !$numeric_value && $numeric_operator) {
+ ThrowUserError('number_not_numeric', { field => $field, num => $value });
+ }
return Bugzilla->dbh->quote($value);
}
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";
@@ -2100,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);
}
@@ -2276,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.
@@ -2290,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,
@@ -2357,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 {
@@ -2369,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 c1f49a224..b9cbfcce0 100644
--- a/Bugzilla/Template.pm
+++ b/Bugzilla/Template.pm
@@ -151,13 +151,11 @@ sub quoteUrls {
# (http://foo/bug#3 for example). Filtering that out filters valid
# bug refs out, so we have to do replacements.
# mailto can't contain space or #, so we don't have to bother for that
- # Do this by escaping \0 to \1\0, and replacing matches with \0\0$count\0\0
- # \0 is used because it's unlikely to occur in the text, so the cost of
- # doing this should be very small
-
- # escape the 2nd escape char we're using
- my $chr1 = chr(1);
- $text =~ s/\0/$chr1\0/g;
+ # Do this by replacing matches with \x{FDD2}$count\x{FDD3}
+ # \x{FDDx} is used because it's unlikely to occur in the text
+ # and are reserved unicode characters. We disable warnings for now
+ # until we require Perl 5.13.9 or newer.
+ no warnings 'utf8';
# However, note that adding the title (for buglinks) can affect things
# In particular, attachment matches go before bug titles, so that titles
@@ -184,11 +182,11 @@ sub quoteUrls {
$1, $2, $3, $4,
$5, $6, $7, $8,
$9, $10]}))
- && ("\0\0" . ($count-1) . "\0\0")/egx;
+ && ("\x{FDD2}" . ($count-1) . "\x{FDD3}")/egx;
}
else {
$text =~ s/$match/($things[$count++] = $replace)
- && ("\0\0" . ($count-1) . "\0\0")/egx;
+ && ("\x{FDD2}" . ($count-1) . "\x{FDD3}")/egx;
}
}
@@ -198,7 +196,7 @@ sub quoteUrls {
Bugzilla->params->{'sslbase'})) . ')';
$text =~ s~\b(${urlbase_re}\Qshow_bug.cgi?id=\E([0-9]+)(\#c([0-9]+))?)\b
~($things[$count++] = get_bug_link($3, $1, { comment_num => $5, user => $user })) &&
- ("\0\0" . ($count-1) . "\0\0")
+ ("\x{FDD2}" . ($count-1) . "\x{FDD3}")
~egox;
# non-mailto protocols
@@ -206,7 +204,7 @@ sub quoteUrls {
$text =~ s~\b($safe_protocols)
~($tmp = html_quote($1)) &&
($things[$count++] = "$tmp") &&
- ("\0\0" . ($count-1) . "\0\0")
+ ("\x{FDD2}" . ($count-1) . "\x{FDD3}")
~egox;
# We have to quote now, otherwise the html itself is escaped
@@ -227,7 +225,7 @@ sub quoteUrls {
# attachment links
$text =~ s~\b(attachment\s*\#?\s*(\d+)(?:\s+\[details\])?)
~($things[$count++] = get_attachment_link($2, $1, $user)) &&
- ("\0\0" . ($count-1) . "\0\0")
+ ("\x{FDD2}" . ($count-1) . "\x{FDD3}")
~egmxi;
# Current bug ID this comment belongs to
@@ -257,9 +255,8 @@ sub quoteUrls {
# Now remove the encoding hacks in reverse order
for (my $i = $#things; $i >= 0; $i--) {
- $text =~ s/\0\0($i)\0\0/$things[$i]/eg;
+ $text =~ s/\x{FDD2}($i)\x{FDD3}/$things[$i]/eg;
}
- $text =~ s/$chr1\0/\0/g;
return $text;
}
@@ -634,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;
@@ -645,6 +644,17 @@ sub create {
my ($data) = @_;
return encode_base64($data);
},
+
+ # Strips out control characters excepting whitespace
+ strip_control_chars => sub {
+ my ($data) = @_;
+ # Only run for utf8 to avoid issues with other multibyte encodings
+ # that may be reassigning meaning to ascii characters.
+ if (Bugzilla->params->{'utf8'}) {
+ $data =~ s/(?![\t\r\n])[[:cntrl:]]//g;
+ }
+ return $data;
+ },
# HTML collapses newlines in element attributes to a single space,
# so form elements which may have whitespace (ie comments) need
@@ -704,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\"";
@@ -889,6 +905,11 @@ sub create {
# Allow templates to generate a token themselves.
'issue_hash_token' => \&Bugzilla::Token::issue_hash_token,
+ 'get_login_request_token' => sub {
+ my $cookie = Bugzilla->cgi->cookie('Bugzilla_login_request_cookie');
+ return $cookie ? issue_hash_token(['login_request', $cookie]) : '';
+ },
+
# A way for all templates to get at Field data, cached.
'bug_fields' => sub {
my $cache = Bugzilla->request_cache;
diff --git a/Bugzilla/Token.pm b/Bugzilla/Token.pm
index a924324dc..c7e9f645f 100644
--- a/Bugzilla/Token.pm
+++ b/Bugzilla/Token.pm
@@ -171,6 +171,10 @@ sub issue_hash_token {
my @args = ($time, $user_id, @$data);
my $token = join('*', @args);
+ # Wide characters cause Digest::SHA to die.
+ if (Bugzilla->params->{'utf8'}) {
+ utf8::encode($token) if utf8::is_utf8($token);
+ }
$token = hmac_sha256_base64($token, Bugzilla->localconfig->{'site_wide_secret'});
$token =~ s/\+/-/g;
$token =~ s/\//_/g;
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
+
+C - Sets C to the inverse of the boolean provided.
+
=back
=head1 CLASS FUNCTIONS
diff --git a/Bugzilla/UserAgent.pm b/Bugzilla/UserAgent.pm
index a4135dd20..5615f86ee 100644
--- a/Bugzilla/UserAgent.pm
+++ b/Bugzilla/UserAgent.pm
@@ -103,6 +103,8 @@ 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"],
qr/\(.*Windows NT 6\.0.*\)/ => ["Windows Vista"],
diff --git a/Bugzilla/Util.pm b/Bugzilla/Util.pm
index a96d8656a..527bae85a 100644
--- a/Bugzilla/Util.pm
+++ b/Bugzilla/Util.pm
@@ -13,8 +13,8 @@ use base qw(Exporter);
@Bugzilla::Util::EXPORT = qw(trick_taint detaint_natural detaint_signed
html_quote url_quote xml_quote
css_class_quote html_light_quote
- i_am_cgi correct_urlbase remote_ip validate_ip
- do_ssl_redirect_if_required use_attachbase
+ i_am_cgi i_am_webservice correct_urlbase remote_ip
+ validate_ip do_ssl_redirect_if_required use_attachbase
diff_arrays on_main_db say
trim wrap_hard wrap_comment find_wrap_point
format_time validate_date validate_time datetime_from
@@ -68,6 +68,10 @@ sub html_quote {
# Obscure '@'.
$var =~ s/\@/\@/g;
if (Bugzilla->params->{'utf8'}) {
+ # Remove control characters if the encoding is utf8.
+ # Other multibyte encodings may be using this range; so ignore if not utf8.
+ $var =~ s/(?![\t\r\n])[[:cntrl:]]//g;
+
# Remove the following characters because they're
# influencing BiDi:
# --------------------------------------------------------
@@ -230,6 +234,12 @@ sub i_am_cgi {
return exists $ENV{'SERVER_SOFTWARE'} ? 1 : 0;
}
+sub i_am_webservice {
+ my $usage_mode = Bugzilla->usage_mode;
+ return $usage_mode == USAGE_MODE_XMLRPC
+ || $usage_mode == USAGE_MODE_JSON;
+}
+
# This exists as a separate function from Bugzilla::CGI::redirect_to_https
# because we don't want to create a CGI object during XML-RPC calls
# (doing so can mess up XML-RPC).
@@ -494,8 +504,9 @@ sub join_activity_entries {
return $current_change . $new_change;
}
- # All other fields get a space
- if (substr($new_change, 0, 1) eq ' ') {
+ # All other fields get a space unless the first character of the second
+ # string is a comma or space
+ if (substr($new_change, 0, 1) eq ',' || substr($new_change, 0, 1) eq ' ') {
return $current_change . $new_change;
} else {
return $current_change . ' ' . $new_change;
@@ -558,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],
@@ -617,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);
@@ -666,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 {
@@ -861,6 +882,7 @@ Bugzilla::Util - Generic utility functions for bugzilla
# Functions that tell you about your environment
my $is_cgi = i_am_cgi();
+ my $is_webservice = i_am_webservice();
my $urlbase = correct_urlbase();
# Data manipulation
@@ -988,6 +1010,11 @@ Tells you whether or not you are being run as a CGI script in a web
server. For example, it would return false if the caller is running
in a command-line script.
+=item C
+
+Tells you whether or not the current usage mode is WebServices related
+such as JSONRPC or XMLRPC.
+
=item C
Returns either the C or C parameter, depending on the
diff --git a/Bugzilla/Version.pm b/Bugzilla/Version.pm
index 4a2c4a5e1..7c341b654 100644
--- a/Bugzilla/Version.pm
+++ b/Bugzilla/Version.pm
@@ -117,14 +117,18 @@ sub bug_count {
sub update {
my $self = shift;
+ my $dbh = Bugzilla->dbh;
+
+ $dbh->bz_start_transaction();
my ($changes, $old_self) = $self->SUPER::update(@_);
if (exists $changes->{value}) {
- my $dbh = Bugzilla->dbh;
$dbh->do('UPDATE bugs SET version = ?
WHERE version = ? AND product_id = ?',
undef, ($self->name, $old_self->name, $self->product_id));
}
+ $dbh->bz_commit_transaction();
+
return $changes;
}
diff --git a/Bugzilla/WebService.pm b/Bugzilla/WebService.pm
index 4d018772f..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};
@@ -128,9 +132,7 @@ There are various ways to log in:
=item C
You can use L to log in as a Bugzilla
-user. This issues standard HTTP cookies that you must then use in future
-calls, so your client must be capable of receiving and transmitting
-cookies.
+user. This issues a token that you must then use in future calls.
=item C and C
@@ -150,19 +152,21 @@ WebService method to perform a login:
=item C (boolean) - Optional. If true,
then your login will only be valid for your IP address.
-=item C (boolean) - Optional. If true,
-then the cookie sent back to you with the method response will
-not expire.
-
=back
-The C and C options
-are only used when you have also specified C and
-C.
+The C option is only used when you have also
+specified C and C.
+
+=item C
+
+B
+
+You can specify C as argument to any WebService method,
+and you will be logged in as that user if the token is correct. This is
+the token returned when calling C mentioned above.
-Note that Bugzilla will return HTTP cookies along with the method
-response when you use these arguments (just like the C method
-above).
+Support for using login cookies for authentication has been dropped
+for security reasons.
=back
@@ -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 5552b3cfc..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 });
@@ -2230,7 +2241,7 @@ narrowed down to specific products.
=item C (string) B - A string of keywords defining
the type of bug you are trying to report.
-=item C (array) - One or more product names to narrow the
+=item C (array) - One or more product names to narrow the
duplicate search to. If omitted, all bugs are searched.
=back
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 804d7874e..0a0afd400 100644
--- a/Bugzilla/WebService/Server/JSONRPC.pm
+++ b/Bugzilla/WebService/Server/JSONRPC.pm
@@ -23,11 +23,12 @@ BEGIN {
use Bugzilla::Error;
use Bugzilla::WebService::Constants;
-use Bugzilla::WebService::Util qw(taint_data);
+use Bugzilla::WebService::Util qw(taint_data fix_credentials);
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.
@@ -349,6 +351,10 @@ sub _argument_type_check {
}
}
+ # Update the params to allow for several convenience key/values
+ # use for authentication
+ fix_credentials($params);
+
Bugzilla->input_params($params);
if ($self->request->method eq 'POST') {
@@ -373,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 40cc6ec54..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;
}
@@ -82,8 +93,18 @@ our @ISA = qw(XMLRPC::Deserializer);
use Bugzilla::Error;
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;
@@ -105,6 +126,11 @@ sub deserialize {
my $params = $som->paramsin;
# This allows positional parameters for Testopia.
$params = {} if ref $params ne 'HASH';
+
+ # Update the params to allow for several convenience key/values
+ # use for authentication
+ fix_credentials($params);
+
Bugzilla->input_params($params);
return $som;
}
diff --git a/Bugzilla/WebService/User.pm b/Bugzilla/WebService/User.pm
index ba94c0e71..469e5c5cd 100644
--- a/Bugzilla/WebService/User.pm
+++ b/Bugzilla/WebService/User.pm
@@ -15,9 +15,13 @@ use Bugzilla::Constants;
use Bugzilla::Error;
use Bugzilla::Group;
use Bugzilla::User;
-use Bugzilla::Util qw(trim);
+use Bugzilla::Util qw(trim detaint_natural);
use Bugzilla::WebService::Util qw(filter validate translate params_to_objects);
+use List::Util qw(min);
+
+use List::Util qw(first);
+
# Don't need auth to login
use constant LOGIN_EXEMPT => {
login => 1,
@@ -28,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'
};
##############
@@ -48,38 +59,26 @@ use constant MAPPED_RETURNS => {
sub login {
my ($self, $params) = @_;
- my $remember = $params->{remember};
+
+ # 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 });
}
- # Convert $remember from a boolean 0/1 value to a CGI-compatible one.
- if (defined($remember)) {
- $remember = $remember? 'on': '';
- }
- else {
- # Use Bugzilla's default if $remember is not supplied.
- $remember =
- Bugzilla->params->{'rememberlogin'} eq 'defaulton'? 'on': '';
- }
-
- # 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_remember'} = $remember;
-
- Bugzilla->login();
- return { id => $self->type('int', Bugzilla->user->id) };
+ $user = Bugzilla->login();
+ return $self->_login_to_hash($user);
}
sub logout {
my $self = shift;
Bugzilla->logout;
- return undef;
}
#################
@@ -184,12 +183,17 @@ sub get {
userid => $obj->id});
}
}
-
+
# User Matching
- my $limit;
- if ($params->{'maxusermatches'}) {
- $limit = $params->{'maxusermatches'} + 1;
+ my $limit = Bugzilla->params->{maxusermatches};
+ if ($params->{limit}) {
+ detaint_natural($params->{limit})
+ || ThrowCodeError('param_must_be_numeric',
+ { function => 'Bugzilla::WebService::User::match',
+ param => 'limit' });
+ $limit = $limit ? min($params->{limit}, $limit) : $params->{limit};
}
+
my $exclude_disabled = $params->{'include_disabled'} ? 0 : 1;
foreach my $match_string (@{ $params->{'match'} || [] }) {
my $matched = Bugzilla::User::match($match_string, $limit, $exclude_disabled);
@@ -200,7 +204,7 @@ sub get {
}
}
}
-
+
my $in_group = $self->_filter_users_by_group(
\@user_objects, $params);
@@ -380,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__
@@ -420,22 +433,19 @@ etc. This method logs in an user.
=item C (string) - The user's password.
-=item C (bool) B - if the cookies returned by the
-call to login should expire with the session or not. In order for
-this option to have effect the Bugzilla server must be configured to
-allow the user to set this option - the Bugzilla parameter
-I must be set to "defaulton" or
-"defaultoff". Addionally, the client application must implement
-management of cookies across sessions.
+=item C (bool) B - If set to a true value,
+the token returned by this method will only be valid from the IP address
+which called this method.
=back
=item B
-On success, a hash containing one item, C, the numeric id of the
-user that was logged in. A set of http cookies is also sent with the
-response. These cookies must be sent along with any future requests
-to the webservice, for the duration of the session.
+On success, a hash containing two items, C, the numeric id of the
+user that was logged in, and a C which can be passed in the parameters
+as authentication in other calls. The token can be sent along with any future
+requests to the webservice, for the duration of the session, i.e. till
+L is called.
=item B
@@ -461,6 +471,19 @@ A login or password parameter was not provided.
=back
+=item B
+
+=over
+
+=item C was removed in Bugzilla B<4.4> as this method no longer
+creates a login cookie.
+
+=item C was added in Bugzilla B<4.4>.
+
+=item C was added in Bugzilla B<4.4>.
+
+=back
+
=back
=head2 logout
@@ -729,9 +752,6 @@ Bugzilla itself. Users will be returned whose real name or login name
contains any one of the specified strings. Users that you cannot see will
not be included in the returned list.
-Some Bugzilla installations have user-matching turned off, in which
-case you will only be returned exact matches.
-
Most installations have a limit on how many matches are returned for
each string, which defaults to 1000 but can be changed by the Bugzilla
administrator.
@@ -741,6 +761,13 @@ if they try. (This is to make it harder for spammers to harvest email
addresses from Bugzilla, and also to enforce the user visibility
restrictions that are implemented on some Bugzillas.)
+=item C (int)
+
+Limit the number of users matched by the C parameter. If value
+is greater than the system limit, the system limit will be used. This
+parameter is only used when user matching using the C parameter
+is being performed.
+
=item C (array)
=item C (array)
@@ -885,6 +912,10 @@ querying your own account, even if you are in the editusers group.
You passed an invalid login name in the "names" array or a bad
group ID in the C argument.
+=item 52 (Invalid Parameter)
+
+The value used must be an integer greater then zero.
+
=item 304 (Authorization Required)
You are logged in, but you are not authorized to see one of the users you
diff --git a/Bugzilla/WebService/Util.pm b/Bugzilla/WebService/Util.pm
index 6e935fe20..c7d63b336 100644
--- a/Bugzilla/WebService/Util.pm
+++ b/Bugzilla/WebService/Util.pm
@@ -20,6 +20,7 @@ our @EXPORT_OK = qw(
validate
translate
params_to_objects
+ fix_credentials
);
sub filter ($$;$) {
@@ -143,6 +144,22 @@ sub params_to_objects {
return \@objects;
}
+sub fix_credentials {
+ my ($params) = @_;
+ # Allow user to pass in login=foo&password=bar as a convenience
+ # 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'} = 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'} = delete $params->{'token'};
+ }
+}
+
__END__
=head1 NAME
@@ -205,3 +222,9 @@ parameters passed to a WebService method (the first parameter to this function).
Helps make life simpler for WebService methods that internally create objects
via both "ids" and "names" fields. Also de-duplicates objects that were loaded
by both "ids" and "names". Returns an arrayref of objects.
+
+=head2 fix_credentials
+
+Allows for certain parameters related to authentication such as Bugzilla_login,
+Bugzilla_password, and Bugzilla_token to have shorter named equivalents passed in.
+This function converts the shorter versions to their respective internal names.
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 => < '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
-
-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 4e3d2bd7a..e3d8fe711 100755
--- a/buglist.cgi
+++ b/buglist.cgi
@@ -284,23 +284,6 @@ sub GetGroups {
return [values %legal_groups];
}
-sub _close_standby_message {
- my ($contenttype, $disposition, $serverpush) = @_;
- my $cgi = Bugzilla->cgi;
-
- # Close the "please wait" page, then open the buglist page
- if ($serverpush) {
- print $cgi->multipart_end();
- print $cgi->multipart_start(-type => $contenttype,
- -content_disposition => $disposition);
- }
- else {
- print $cgi->header(-type => $contenttype,
- -content_disposition => $disposition);
- }
-}
-
-
################################################################################
# Command Execution
################################################################################
@@ -933,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
@@ -945,7 +928,6 @@ if ($one_product && $user->can_enter_product($one_product)) {
# The following variables are used when the user is making changes to multiple bugs.
if ($dotweak && scalar @bugs) {
if (!$vars->{'caneditbugs'}) {
- _close_standby_message('text/html', 'inline', $serverpush);
ThrowUserError('auth_failure', {group => 'editbugs',
action => 'modify',
object => 'multiple_bugs'});
@@ -1055,7 +1037,7 @@ if ($format->{'extension'} eq "csv") {
# Suggest a name for the bug list if the user wants to save it as a file.
$disposition .= "; filename=\"$filename\"";
-_close_standby_message($contenttype, $disposition, $serverpush);
+$cgi->close_standby_message($contenttype, $disposition);
################################################################################
# Content Generation
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 < \$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 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.
=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 and C calls to log in and out, as shown
in this script.
-The C parameter is optional.
-If omitted, Bugzilla's defaults apply (as specified by its C
+The C parameter is optional.
+If omitted, Bugzilla's defaults apply (as specified by its C
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 with the ID of the bug you want to know more of.
-The call will return a C object.
+The call will return a C 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 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 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.
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/contrib/jb2bz.py b/contrib/jb2bz.py
index 55cb056b5..85f95423a 100755
--- a/contrib/jb2bz.py
+++ b/contrib/jb2bz.py
@@ -17,7 +17,7 @@ This code requires a recent version of Andy Dustman's MySQLdb interface,
Share and enjoy.
"""
-import rfc822, mimetools, multifile, mimetypes
+import rfc822, mimetools, multifile, mimetypes, email.utils
import sys, re, glob, StringIO, os, stat, time
import MySQLdb, getopt
@@ -91,7 +91,7 @@ def process_reply_file(current, fname):
reply = open(fname, "r")
msg = rfc822.Message(reply)
new_note['text'] = "%s\n%s" % (msg['From'], msg.fp.read())
- new_note['timestamp'] = rfc822.parsedate_tz(msg['Date'])
+ new_note['timestamp'] = email.utils.parsedate_tz(msg['Date'])
current["notes"].append(new_note)
def add_notes(current):
@@ -129,17 +129,16 @@ def maybe_add_attachment(current, file, submsg):
def process_mime_body(current, file, submsg):
data = StringIO.StringIO()
- mimetools.decode(file, data, submsg.getencoding())
- current['description'] = data.getvalue()
-
-
+ try:
+ mimetools.decode(file, data, submsg.getencoding())
+ current['description'] = data.getvalue()
+ except:
+ return
def process_text_plain(msg, current):
- print "Processing: %d" % current['number']
current['description'] = msg.fp.read()
def process_multi_part(file, msg, current):
- print "Processing: %d" % current['number']
mf = multifile.MultiFile(file)
mf.push(msg.getparam("boundary"))
while mf.next():
@@ -160,17 +159,31 @@ def process_jitterbug(filename):
current['date-reported'] = ()
current['short-description'] = ''
+ print "Processing: %d" % current['number']
+
file = open(filename, "r")
+ create_date = os.fstat(file.fileno())
msg = mimetools.Message(file)
msgtype = msg.gettype()
add_notes(current)
- current['date-reported'] = rfc822.parsedate_tz(msg['Date'])
- current['short-description'] = msg['Subject']
+ current['date-reported'] = email.utils.parsedate_tz(msg['Date'])
+ if current['date-reported'] is None:
+ current['date-reported'] = time.gmtime(create_date[stat.ST_MTIME])
+
+ if current['date-reported'][0] < 1900:
+ current['date-reported'] = time.gmtime(create_date[stat.ST_MTIME])
+
+ if msg.getparam('Subject') is not None:
+ current['short-description'] = msg['Subject']
+ else:
+ current['short-description'] = "Unknown"
if msgtype[:5] == 'text/':
process_text_plain(msg, current)
+ elif msgtype[:5] == 'text':
+ process_text_plain(msg, current)
elif msgtype[:10] == "multipart/":
process_multi_part(file, msg, current)
else:
@@ -200,62 +213,79 @@ def process_jitterbug(filename):
# the resolution will need to be set manually
resolution=""
- db = MySQLdb.connect(db='bugs',user='root',host='localhost')
+ db = MySQLdb.connect(db='bugs',user='root',host='localhost',passwd='password')
cursor = db.cursor()
- cursor.execute( "INSERT INTO bugs SET " \
- "bug_id=%s," \
- "bug_severity='normal'," \
- "bug_status=%s," \
- "creation_ts=%s," \
- "delta_ts=%s," \
- "short_desc=%s," \
- "product=%s," \
- "rep_platform='All'," \
- "assigned_to=%s,"
- "reporter=%s," \
- "version=%s," \
- "component=%s," \
- "resolution=%s",
- [ current['number'],
- bug_status,
- time.strftime("%Y-%m-%d %H:%M:%S", current['date-reported'][:9]),
- time.strftime("%Y-%m-%d %H:%M:%S", current['date-reported'][:9]),
- current['short-description'],
- product,
- reporter,
- reporter,
- version,
- component,
- resolution] )
-
- # This is the initial long description associated with the bug report
- cursor.execute( "INSERT INTO longdescs VALUES (%s,%s,%s,%s)",
- [ current['number'],
- reporter,
- time.strftime("%Y-%m-%d %H:%M:%S", current['date-reported'][:9]),
- current['description'] ] )
-
- # Add whatever notes are associated with this defect
- for n in current['notes']:
- cursor.execute( "INSERT INTO longdescs VALUES (%s,%s,%s,%s)",
- [current['number'],
- reporter,
- time.strftime("%Y-%m-%d %H:%M:%S", n['timestamp'][:9]),
- n['text']])
-
- # add attachments associated with this defect
- for a in current['attachments']:
- cursor.execute( "INSERT INTO attachments SET " \
- "bug_id=%s, creation_ts=%s, description='', mimetype=%s," \
- "filename=%s, submitter_id=%s",
+ try:
+ cursor.execute( "INSERT INTO bugs SET " \
+ "bug_id=%s," \
+ "bug_severity='normal'," \
+ "bug_status=%s," \
+ "creation_ts=%s," \
+ "delta_ts=%s," \
+ "short_desc=%s," \
+ "product_id=%s," \
+ "rep_platform='All'," \
+ "assigned_to=%s," \
+ "reporter=%s," \
+ "version=%s," \
+ "component_id=%s," \
+ "resolution=%s",
+ [ current['number'],
+ bug_status,
+ time.strftime("%Y-%m-%d %H:%M:%S", current['date-reported'][:9]),
+ time.strftime("%Y-%m-%d %H:%M:%S", current['date-reported'][:9]),
+ current['short-description'],
+ product,
+ reporter,
+ reporter,
+ version,
+ component,
+ resolution] )
+
+ # This is the initial long description associated with the bug report
+ cursor.execute( "INSERT INTO longdescs SET " \
+ "bug_id=%s," \
+ "who=%s," \
+ "bug_when=%s," \
+ "thetext=%s",
[ current['number'],
+ reporter,
time.strftime("%Y-%m-%d %H:%M:%S", current['date-reported'][:9]),
- a[1], a[0], reporter ])
- cursor.execute( "INSERT INTO attach_data SET " \
- "id=LAST_INSERT_ID(), thedata=%s",
- [ a[2] ])
+ current['description'] ] )
+
+ # Add whatever notes are associated with this defect
+ for n in current['notes']:
+ cursor.execute( "INSERT INTO longdescs SET " \
+ "bug_id=%s," \
+ "who=%s," \
+ "bug_when=%s," \
+ "thetext=%s",
+ [current['number'],
+ reporter,
+ time.strftime("%Y-%m-%d %H:%M:%S", n['timestamp'][:9]),
+ n['text']])
+
+ # add attachments associated with this defect
+ for a in current['attachments']:
+ cursor.execute( "INSERT INTO attachments SET " \
+ "bug_id=%s, creation_ts=%s, description='', mimetype=%s," \
+ "filename=%s, submitter_id=%s",
+ [ current['number'],
+ time.strftime("%Y-%m-%d %H:%M:%S", current['date-reported'][:9]),
+ a[1], a[0], reporter ])
+ cursor.execute( "INSERT INTO attach_data SET " \
+ "id=LAST_INSERT_ID(), thedata=%s",
+ [ a[2] ])
+
+ except MySQLdb.IntegrityError, message:
+ errorcode = message[0]
+ if errorcode == 1062: # duplicate
+ return
+ else:
+ raise
+ cursor.execute("COMMIT")
cursor.close()
db.close()
diff --git a/contrib/sendunsentbugmail.pl b/contrib/sendunsentbugmail.pl
index 94ad25de4..46105776f 100755
--- a/contrib/sendunsentbugmail.pl
+++ b/contrib/sendunsentbugmail.pl
@@ -17,10 +17,9 @@ use Bugzilla::BugMail;
my $dbh = Bugzilla->dbh;
my $list = $dbh->selectcol_arrayref(
- 'SELECT bug_id FROM bugs
- WHERE lastdiffed IS NULL
- OR lastdiffed < delta_ts
- AND delta_ts < '
+ 'SELECT bug_id FROM bugs
+ WHERE (lastdiffed IS NULL OR lastdiffed < delta_ts)
+ AND delta_ts < '
. $dbh->sql_date_math('NOW()', '-', 30, 'MINUTE') .
' ORDER BY bug_id');
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 f5b90fa9c..97c022f6a 100755
--- a/importxml.pl
+++ b/importxml.pl
@@ -53,6 +53,7 @@ use lib qw(. lib);
use Bugzilla;
use Bugzilla::Object;
use Bugzilla::Bug;
+use Bugzilla::Attachment;
use Bugzilla::Product;
use Bugzilla::Version;
use Bugzilla::Component;
@@ -1043,6 +1044,7 @@ sub process_bug {
$dbh->do( $query, undef, @values );
my $id = $dbh->bz_last_key( 'bugs', 'bug_id' );
+ my $bug_obj = Bugzilla::Bug->new($id);
# We are almost certain to get some uninitialized warnings
# Since this is just for debugging the query, let's shut them up
@@ -1125,31 +1127,41 @@ sub process_bug {
$err .= "No attachment ID specified, dropping attachment\n";
next;
}
- if (!$exporter->is_insider && $att->{'isprivate'}) {
- $err .= "Exporter not in insidergroup and attachment marked private.\n";
+
+ my $attacher;
+ if ($att->{'attacher'}) {
+ $attacher = Bugzilla::User->new({name => $att->{'attacher'}, cache => 1});
+ }
+ my $new_attacher = $attacher || $exporter;
+
+ if ($att->{'isprivate'} && !$new_attacher->is_insider) {
+ my $who = $new_attacher->login;
+ $err .= "$who not in insidergroup and attachment marked private.\n";
$err .= " Marking attachment public\n";
$att->{'isprivate'} = 0;
}
- my $attacher_id = $att->{'attacher'} ? login_to_id($att->{'attacher'}) : undef;
-
- $dbh->do("INSERT INTO attachments
- (bug_id, creation_ts, modification_time, filename, description,
- mimetype, ispatch, isprivate, isobsolete, submitter_id)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
- undef, $id, $att->{'date'}, $att->{'date'}, $att->{'filename'},
- $att->{'desc'}, $att->{'ctype'}, $att->{'ispatch'},
- $att->{'isprivate'}, $att->{'isobsolete'}, $attacher_id || $exporterid);
- my $att_id = $dbh->bz_last_key( 'attachments', 'attach_id' );
- my $att_data = $att->{'data'};
- my $sth = $dbh->prepare("INSERT INTO attach_data (id, thedata)
- VALUES ($att_id, ?)" );
- trick_taint($att_data);
- $sth->bind_param( 1, $att_data, $dbh->BLOB_TYPE );
- $sth->execute();
+ # We log in the user so that the attachment creator is set correctly.
+ Bugzilla->set_user($new_attacher);
+
+ my $attachment = Bugzilla::Attachment->create(
+ { bug => $bug_obj,
+ creation_ts => $att->{date},
+ data => $att->{data},
+ description => $att->{desc},
+ filename => $att->{filename},
+ ispatch => $att->{ispatch},
+ isprivate => $att->{isprivate},
+ isobsolete => $att->{isobsolete},
+ mimetype => $att->{ctype},
+ });
+ my $att_id = $attachment->id;
+
+ # We log out the attacher as the remaining steps are not on his behalf.
+ Bugzilla->logout_request;
$comments .= "Imported an attachment (id=$att_id)\n";
- if (!$attacher_id) {
+ if (!$attacher) {
if ($att->{'attacher'}) {
$err .= "The original submitter of attachment $att_id was\n ";
$err .= $att->{'attacher'} . ", but he doesn't have an account here.\n";
@@ -1252,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
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/process_bug.cgi b/process_bug.cgi
index 7c8984d40..e063c3db4 100755
--- a/process_bug.cgi
+++ b/process_bug.cgi
@@ -167,7 +167,7 @@ if ($delta_ts) {
my $token = $cgi->param('token');
if ($cgi->param('id')) {
- check_hash_token($token, [$first_bug->id, $delta_ts]);
+ check_hash_token($token, [$first_bug->id, $delta_ts || $first_bug->delta_ts]);
}
else {
check_token_data($token, 'buglist_mass_change', 'query.cgi');
diff --git a/relogin.cgi b/relogin.cgi
index 57240db43..b86463bb8 100755
--- a/relogin.cgi
+++ b/relogin.cgi
@@ -51,6 +51,22 @@ elsif ($action eq 'prepare-sudo') {
# Keep a temporary record of the user visiting this page
$vars->{'token'} = issue_session_token('sudo_prepared');
+ if ($user->authorizer->can_login) {
+ my $value = generate_random_password();
+ my %args;
+ $args{'-secure'} = 1 if Bugzilla->params->{ssl_redirect};
+
+ $cgi->send_cookie(-name => 'Bugzilla_login_request_cookie',
+ -value => $value,
+ -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]);
+ }
+
# Show the sudo page
$vars->{'target_login_default'} = $cgi->param('target_login');
$vars->{'reason_default'} = $cgi->param('reason');
@@ -71,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',
@@ -107,45 +125,47 @@ 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');
# For future sessions, store the unique ID of the target user
my $token = Bugzilla::Token::_create_token($user->id, 'sudo', $target_user->id);
+
+ my %args;
+ if (Bugzilla->params->{ssl_redirect}) {
+ $args{'-secure'} = 1;
+ }
+
$cgi->send_cookie('-name' => 'sudo',
'-expires' => $time_string,
- '-value' => $token
- );
-
+ '-value' => $token,
+ '-httponly' => 1,
+ %args);
+
# 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 "
data hash:";
- say html_quote(Data::Dumper::Dumper(%data));
- say "\ndata array:";
- say html_quote(Data::Dumper::Dumper(@image_data)) . "\n\n
";
+ $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'} = ;
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 = "