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 9fffc3530..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 {
@@ -799,10 +799,10 @@ not an arrayref.
=item C
-C if there is no currently logged in user or if the login code has not
-yet been run. If an sudo session is in progress, the C
-corresponding to the person who is being impersonated. If no session is in
-progress, the current C.
+Default C object if there is no currently logged in user or
+if the login code has not yet been run. If an sudo session is in progress,
+the C corresponding to the person who is being impersonated.
+If no session is in progress, the current C.
=item C
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 e9cb189ef..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);
@@ -268,8 +268,7 @@ sub setup_template_patch_reader {
&& Bugzilla->params->{'cvsroot_get'} && !$vars->{'newid'};
# Print everything out.
- print $cgi->header(-type => 'text/html',
- -expires => '+3M');
+ print $cgi->header(-type => 'text/html');
$last_reader->sends_data_to(new PatchReader::DiffPrinter::template($template,
"attachment/diff-header.$format.tmpl",
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 5d4c8279c..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
@@ -50,8 +65,8 @@ sub get_login_info {
trick_taint($login_cookie);
detaint_natural($user_id);
- my $is_valid =
- $dbh->selectrow_array('SELECT 1
+ my $db_cookie =
+ $dbh->selectrow_array('SELECT cookie
FROM logincookies
WHERE cookie = ?
AND userid = ?
@@ -59,7 +74,7 @@ sub get_login_info {
undef, ($login_cookie, $user_id, $ip_addr));
# If the cookie is valid, return a valid username.
- if ($is_valid) {
+ if (defined $db_cookie && $login_cookie eq $db_cookie) {
# If we logged in successfully, then update the lastused
# time on the login cookie
$dbh->do("UPDATE logincookies SET lastused = NOW()
@@ -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 4d1181315..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};
@@ -2508,6 +2514,7 @@ sub _set_product {
my @idlist = ($self->id);
push(@idlist, map {$_->id} @{ $params->{other_bugs} })
if $params->{other_bugs};
+ @idlist = uniq @idlist;
# Get the ID of groups which are no longer valid in the new product.
my $gids = $dbh->selectcol_arrayref(
'SELECT bgm.group_id
@@ -2522,9 +2529,13 @@ sub _set_product {
. Bugzilla->user->groups_as_string . '))
OR gcm.othercontrol != ?) )',
undef, (@idlist, $product->id, CONTROLMAPNA, CONTROLMAPNA));
- $vars{'old_groups'} = Bugzilla::Group->new_from_list($gids);
+ $vars{'old_groups'} = Bugzilla::Group->new_from_list($gids);
+
+ # Did we come here from editing multiple bugs? (affects how we
+ # show optional group changes)
+ $vars{multiple_bugs} = (@idlist > 1) ? 1 : 0;
}
-
+
if (%vars) {
$vars{product} = $product;
$vars{bug} = $self;
@@ -2742,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;
}
@@ -2774,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));
@@ -3771,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.
@@ -3895,11 +3911,12 @@ sub get_activity {
if ($operation->{'who'} && $who eq $operation->{'who'}
&& $when eq $operation->{'when'}
&& $fieldname eq $operation->{'fieldname'}
+ && ($comment_id || 0) == ($operation->{'comment_id'} || 0)
&& ($attachid || 0) == ($operation->{'attachid'} || 0))
{
my $old_change = pop @$changes;
- $removed = _join_activity_entries($fieldname, $old_change->{'removed'}, $removed);
- $added = _join_activity_entries($fieldname, $old_change->{'added'}, $added);
+ $removed = join_activity_entries($fieldname, $old_change->{'removed'}, $removed);
+ $added = join_activity_entries($fieldname, $old_change->{'added'}, $added);
}
$operation->{'who'} = $who;
$operation->{'when'} = $when;
@@ -3909,7 +3926,7 @@ sub get_activity {
$change{'added'} = $added;
if ($comment_id) {
- $change{'comment'} = Bugzilla::Comment->new($comment_id);
+ $operation->{comment_id} = $change{'comment'} = Bugzilla::Comment->new($comment_id);
}
push (@$changes, \%change);
@@ -3924,38 +3941,10 @@ sub get_activity {
return(\@operations, $incomplete_data);
}
-sub _join_activity_entries {
- my ($field, $current_change, $new_change) = @_;
- # We need to insert characters as these were removed by old
- # LogActivityEntry code.
-
- return $new_change if $current_change eq '';
-
- # Buglists and see_also need the comma restored
- if ($field eq 'dependson' || $field eq 'blocked' || $field eq 'see_also') {
- if (substr($new_change, 0, 1) eq ',' || substr($new_change, 0, 1) eq ' ') {
- return $current_change . $new_change;
- } else {
- return $current_change . ', ' . $new_change;
- }
- }
-
- # Assume bug_file_loc contain a single url, don't insert a delimiter
- if ($field eq 'bug_file_loc') {
- return $current_change . $new_change;
- }
-
- # All other fields get a space
- if (substr($new_change, 0, 1) eq ' ') {
- return $current_change . $new_change;
- } else {
- return $current_change . ' ' . $new_change;
- }
-}
-
# 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
@@ -3980,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 0fabce67b..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'};
@@ -418,7 +421,8 @@ sub _get_diffs {
ON fielddefs.id = bugs_activity.fieldid
WHERE bugs_activity.bug_id = ?
$when_restriction
- ORDER BY bugs_activity.bug_when", {Slice=>{}}, @args);
+ ORDER BY bugs_activity.bug_when, bugs_activity.id",
+ {Slice=>{}}, @args);
foreach my $diff (@$diffs) {
$user_cache->{$diff->{who}} ||= new Bugzilla::User($diff->{who});
@@ -435,7 +439,25 @@ sub _get_diffs {
}
}
- return @$diffs;
+ my @changes = ();
+ foreach my $diff (@$diffs) {
+ # If this is the same field as the previous item, then concatenate
+ # the data into the same change.
+ if (scalar(@changes)
+ && $diff->{field_name} eq $changes[-1]->{field_name}
+ && $diff->{bug_when} eq $changes[-1]->{bug_when}
+ && $diff->{who} eq $changes[-1]->{who}
+ && ($diff->{attach_id} || 0) == ($changes[-1]->{attach_id} || 0)
+ && ($diff->{comment_id} || 0) == ($changes[-1]->{comment_id} || 0)
+ ) {
+ my $old_change = pop @changes;
+ $diff->{old} = join_activity_entries($diff->{field_name}, $old_change->{old}, $diff->{old});
+ $diff->{new} = join_activity_entries($diff->{field_name}, $old_change->{new}, $diff->{new});
+ }
+ push @changes, $diff;
+ }
+
+ return @changes;
}
sub _get_new_bugmail_fields {
@@ -475,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 a68195f22..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) {
@@ -466,9 +559,9 @@ sub redirect_search_url {
# GET requests that lacked a list_id are always redirected. POST requests
# are only redirected if they're under the CGI_URI_LIMIT though.
- my $uri_length = length($self->self_url());
- if ($self->request_method() ne 'POST' or $uri_length < CGI_URI_LIMIT) {
- print $self->redirect(-url => $self->self_url());
+ my $self_url = $self->self_url();
+ if ($self->request_method() ne 'POST' or length($self_url) < CGI_URI_LIMIT) {
+ print $self->redirect(-url => $self_url);
exit;
}
}
@@ -522,7 +615,7 @@ sub url_is_attachment_base {
$regex =~ s/\\\%bugid\\\%/\\d+/;
}
$regex = "^$regex";
- return ($self->self_url =~ $regex) ? 1 : 0;
+ return ($self->url =~ $regex) ? 1 : 0;
}
##########################
@@ -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 f0bba06db..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";
+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/Schema/Oracle.pm b/Bugzilla/DB/Schema/Oracle.pm
index 381906d2e..a97929726 100644
--- a/Bugzilla/DB/Schema/Oracle.pm
+++ b/Bugzilla/DB/Schema/Oracle.pm
@@ -204,6 +204,10 @@ sub get_add_column_ddl {
}
else {
@sql = $self->SUPER::get_add_column_ddl(@_);
+ # Create triggers to deal with empty string.
+ if ($definition->{TYPE} =~ /varchar|TEXT/i && $definition->{NOTNULL}) {
+ push(@sql, _get_notnull_trigger_ddl($table, $column));
+ }
}
return @sql;
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 a1b8c2c23..affeaee68 100644
--- a/Bugzilla/Flag.pm
+++ b/Bugzilla/Flag.pm
@@ -399,7 +399,7 @@ sub _validate {
my $old_requestee_id = $obj_flag->requestee_id;
$obj_flag->_set_status($params->{status});
- $obj_flag->_set_requestee($params->{requestee}, $attachment, $params->{skip_roe});
+ $obj_flag->_set_requestee($params->{requestee}, $bug, $attachment, $params->{skip_roe});
# The requestee ID can be undefined.
my $requestee_changed = ($obj_flag->requestee_id || 0) != ($old_requestee_id || 0);
@@ -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;
}
@@ -625,10 +626,10 @@ sub force_retarget {
###############################
sub _set_requestee {
- my ($self, $requestee, $attachment, $skip_requestee_on_error) = @_;
+ my ($self, $requestee, $bug, $attachment, $skip_requestee_on_error) = @_;
$self->{requestee} =
- $self->_check_requestee($requestee, $attachment, $skip_requestee_on_error);
+ $self->_check_requestee($requestee, $bug, $attachment, $skip_requestee_on_error);
$self->{requestee_id} =
$self->{requestee} ? $self->{requestee}->id : undef;
@@ -650,7 +651,7 @@ sub _set_status {
}
sub _check_requestee {
- my ($self, $requestee, $attachment, $skip_requestee_on_error) = @_;
+ my ($self, $requestee, $bug, $attachment, $skip_requestee_on_error) = @_;
# If the flag status is not "?", then no requestee can be defined.
return undef if ($self->status ne '?');
@@ -677,8 +678,16 @@ sub _check_requestee {
# Note that can_see_bug() will query the DB, so if the bug
# is being added/removed from some groups and these changes
# haven't been committed to the DB yet, they won't be taken
- # into account here. In this case, old restrictions matters.
- if (!$requestee->can_see_bug($self->bug_id)) {
+ # into account here. In this case, old group restrictions matter.
+ # However, if the user has just been changed to the assignee,
+ # qa_contact, or added to the cc list of the bug and the bug
+ # is cclist_accessible, the requestee is allowed.
+ if (!$requestee->can_see_bug($self->bug_id)
+ && (!$bug->cclist_accessible
+ || !grep($_->id == $requestee->id, @{ $bug->cc_users })
+ && $requestee->id != $bug->assigned_to->id
+ && (!$bug->qa_contact || $requestee->id != $bug->qa_contact->id)))
+ {
if ($skip_requestee_on_error) {
undef $requestee;
}
@@ -998,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 92ab5a267..8a880df80 100644
--- a/Bugzilla/Install/CPAN.pm
+++ b/Bugzilla/Install/CPAN.pm
@@ -24,7 +24,6 @@ use Config;
use CPAN;
use Cwd qw(abs_path);
use File::Path qw(rmtree);
-use List::Util qw(shuffle);
# These are required for install-module.pl to be able to install
# all modules properly.
@@ -86,12 +85,7 @@ use constant CPAN_DEFAULTS => {
unzip => bin_loc('unzip'),
wget => bin_loc('wget'),
- urllist => [shuffle qw(
- http://cpan.pair.com/
- http://mirror.hiwaay.net/CPAN/
- ftp://ftp.dc.aleron.net/pub/CPAN/
- http://mirrors.kernel.org/cpan/
- http://mirrors2.kernel.org/cpan/)],
+ urllist => ['http://www.cpan.org/'],
};
sub check_cpan_requirements {
@@ -209,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 c86c119b0..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)],
},
{
@@ -272,6 +290,8 @@ sub OPTIONAL_MODULES {
version => 0,
feature => ['auth_radius'],
},
+ # XXX - Once we require XMLRPC::Lite 0.717 or higher, we can
+ # remove SOAP::Lite from the list.
{
package => 'SOAP-Lite',
module => 'SOAP::Lite',
@@ -280,6 +300,14 @@ sub OPTIONAL_MODULES {
version => '0.712',
feature => ['xmlrpc'],
},
+ # Since SOAP::Lite 1.0, XMLRPC::Lite is no longer included
+ # and so it must be checked separately.
+ {
+ package => 'XMLRPC-Lite',
+ module => 'XMLRPC::Lite',
+ version => '0.712',
+ feature => ['xmlrpc'],
+ },
{
package => 'JSON-RPC',
module => 'JSON::RPC',
@@ -345,7 +373,8 @@ sub OPTIONAL_MODULES {
{
package => 'TheSchwartz',
module => 'TheSchwartz',
- version => 0,
+ # 1.07 supports the prioritization of jobs.
+ version => 1.07,
feature => ['jobqueue'],
},
{
@@ -354,6 +383,12 @@ sub OPTIONAL_MODULES {
version => 0,
feature => ['jobqueue'],
},
+ {
+ package => 'File-Slurp',
+ module => 'File::Slurp',
+ version => '9999.13',
+ feature => ['jobqueue'],
+ },
# mod_perl
{
@@ -654,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};
@@ -673,21 +708,24 @@ sub have_vers {
Bugzilla::Install::Util::set_output_encoding();
# VERSION is provided by UNIVERSAL::, and can be called even if
- # the module isn't loaded.
- my $vnum = $module->VERSION || -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;
+ # the module isn't loaded. We eval'uate ->VERSION because it can die
+ # when the version is not valid (yes, this happens from time to time).
+ # In that case, we use an uglier method to get the version.
+ my $vnum = eval { $module->VERSION };
+ if ($@) {
+ no strict 'refs';
+ $vnum = ${"${module}::VERSION"};
+
+ # 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/Keyword.pm b/Bugzilla/Keyword.pm
index a6e0b6d27..3f3213be4 100644
--- a/Bugzilla/Keyword.pm
+++ b/Bugzilla/Keyword.pm
@@ -104,7 +104,7 @@ sub _check_name {
# We only want to validate the non-existence of the name if
# we're creating a new Keyword or actually renaming the keyword.
- if (!ref($self) || $self->name ne $name) {
+ if (!ref($self) || lc($self->name) ne lc($name)) {
my $keyword = new Bugzilla::Keyword({ name => $name });
ThrowUserError("keyword_already_exists", { name => $name }) if $keyword;
}
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 795eb2afc..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) {
@@ -1477,6 +1492,8 @@ sub _special_parse_chfield {
@fields = map { $_ eq '[Bug creation]' ? 'creation_ts' : $_ } @fields;
+ return undef unless ($date_from ne '' || $date_to ne '' || $value_to ne '');
+
my $clause = new Bugzilla::Search::Clause();
# It is always safe and useful to push delta_ts into the charts
@@ -1498,44 +1515,21 @@ sub _special_parse_chfield {
$clause->add('delta_ts', 'lessthaneq', $date_to);
}
- # Basically, we construct the chart like:
- #
- # (added_for_field1 = value OR added_for_field2 = value)
- # AND (date_field1_changed >= date_from OR date_field2_changed >= date_from)
- # AND (date_field1_changed <= date_to OR date_field2_changed <= date_to)
- #
- # Theoretically, all we *really* would need to do is look for the field id
- # in the bugs_activity table, because we've already limited the search
- # by delta_ts above, but there's no chart to do that, so we check the
- # change date of the fields.
-
- if ($value_to ne '') {
- my $value_clause = new Bugzilla::Search::Clause('OR');
- foreach my $field (@fields) {
- $value_clause->add($field, 'changedto', $value_to);
- }
- $clause->add($value_clause);
- }
+ # chfieldto is supposed to be a relative date or a date of the form
+ # YYYY-MM-DD, i.e. without the time appended to it. We append the
+ # time ourselves so that the end date is correctly taken into account.
+ $date_to .= ' 23:59:59' if $date_to =~ /^\d{4}-\d{1,2}-\d{1,2}$/;
- if ($date_from ne '') {
- my $from_clause = new Bugzilla::Search::Clause('OR');
- foreach my $field (@fields) {
- $from_clause->add($field, 'changedafter', $date_from);
- }
- $clause->add($from_clause);
- }
- if ($date_to ne '') {
- # chfieldto is supposed to be a relative date or a date of the form
- # YYYY-MM-DD, i.e. without the time appended to it. We append the
- # time ourselves so that the end date is correctly taken into account.
- $date_to .= ' 23:59:59' if $date_to =~ /^\d{4}-\d{1,2}-\d{1,2}$/;
+ my $join_clause = new Bugzilla::Search::Clause('OR');
- my $to_clause = new Bugzilla::Search::Clause('OR');
- foreach my $field (@fields) {
- $to_clause->add($field, 'changedbefore', $date_to);
- }
- $clause->add($to_clause);
+ foreach my $field (@fields) {
+ my $sub_clause = new Bugzilla::Search::ClauseGroup();
+ $sub_clause->add(condition($field, 'changedto', $value_to)) if $value_to ne '';
+ $sub_clause->add(condition($field, 'changedafter', $date_from)) if $date_from ne '';
+ $sub_clause->add(condition($field, 'changedbefore', $date_to)) if $date_to ne '';
+ $join_clause->add($sub_clause);
}
+ $clause->add($join_clause);
return @{$clause->children} ? $clause : undef;
}
@@ -1972,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";
@@ -2121,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);
}
@@ -2297,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.
@@ -2311,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,
@@ -2378,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 {
@@ -2390,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));
@@ -2659,7 +2688,7 @@ sub _owner_idle_time_greater_less {
"$ld_table.who IS NULL AND $act_table.who IS NULL";
} else {
$args->{term} =
- "$ld_table.who IS NOT NULL OR $act_table.who IS NOT NULL";
+ "($ld_table.who IS NOT NULL OR $act_table.who IS NOT NULL)";
}
}
@@ -2903,14 +2932,14 @@ sub _anywordsubstr {
my ($self, $args) = @_;
my @terms = $self->_substring_terms($args);
- $args->{term} = join("\n\tOR ", @terms);
+ $args->{term} = @terms ? '(' . join("\n\tOR ", @terms) . ')' : '';
}
sub _allwordssubstr {
my ($self, $args) = @_;
my @terms = $self->_substring_terms($args);
- $args->{term} = join("\n\tAND ", @terms);
+ $args->{term} = @terms ? '(' . join("\n\tAND ", @terms) . ')' : '';
}
sub _nowordssubstr {
@@ -2922,19 +2951,19 @@ sub _nowordssubstr {
sub _anywords {
my ($self, $args) = @_;
-
+
my @terms = $self->_word_terms($args);
# Because _word_terms uses AND, we need to parenthesize its terms
# if there are more than one.
@terms = map("($_)", @terms) if scalar(@terms) > 1;
- $args->{term} = join("\n\tOR ", @terms);
+ $args->{term} = @terms ? '(' . join("\n\tOR ", @terms) . ')' : '';
}
sub _allwords {
my ($self, $args) = @_;
-
+
my @terms = $self->_word_terms($args);
- $args->{term} = join("\n\tAND ", @terms);
+ $args->{term} = @terms ? '(' . join("\n\tAND ", @terms) . ')' : '';
}
sub _nowords {
diff --git a/Bugzilla/Search/ClauseGroup.pm b/Bugzilla/Search/ClauseGroup.pm
index 5b437afec..83961e12b 100644
--- a/Bugzilla/Search/ClauseGroup.pm
+++ b/Bugzilla/Search/ClauseGroup.pm
@@ -65,7 +65,10 @@ sub add {
# Unsupported fields
if (grep { $_ eq $field } UNSUPPORTED_FIELDS ) {
- ThrowUserError('search_grouped_field_invalid', { field => $field });
+ # XXX - Hack till bug 916882 is fixed.
+ my $operator = scalar(@args) == 3 ? $args[1] : $args[0]->{operator};
+ ThrowUserError('search_grouped_field_invalid', { field => $field })
+ unless (($field eq 'product' || $field eq 'component') && $operator =~ /^changed/);
}
$self->SUPER::add(@args);
diff --git a/Bugzilla/Search/Quicksearch.pm b/Bugzilla/Search/Quicksearch.pm
index e6d3d4402..3e8340c86 100644
--- a/Bugzilla/Search/Quicksearch.pm
+++ b/Bugzilla/Search/Quicksearch.pm
@@ -138,7 +138,7 @@ sub quicksearch {
# Retain backslashes and quotes, to know which strings are quoted,
# and which ones are not.
- my @words = parse_line('\s+', 1, $searchstring);
+ my @words = _parse_line('\s+', 1, $searchstring);
# If parse_line() returns no data, this means strings are badly quoted.
# Rather than trying to guess what the user wanted to do, we throw an error.
scalar(@words)
@@ -194,7 +194,7 @@ sub quicksearch {
# Loop over all main-level QuickSearch words.
foreach my $qsword (@qswords) {
- my @or_operand = parse_line('\|', 1, $qsword);
+ my @or_operand = _parse_line('\|', 1, $qsword);
foreach my $term (@or_operand) {
my $negate = substr($term, 0, 1) eq '-';
if ($negate) {
@@ -208,7 +208,7 @@ sub quicksearch {
# Having ruled out the special cases, we may now split
# by comma, which is another legal boolean OR indicator.
# Remove quotes from quoted words, if any.
- @words = parse_line(',', 0, $term);
+ @words = _parse_line(',', 0, $term);
foreach my $word (@words) {
if (!_special_field_syntax($word, $negate)) {
_default_quicksearch_word($word, $negate);
@@ -260,6 +260,27 @@ sub quicksearch {
# Parts of quicksearch() #
##########################
+sub _parse_line {
+ my ($delim, $keep, $line) = @_;
+ # parse_line always treats ' as a quote character, making it impossible
+ # to sanely search for contractions. As this behavour isn't
+ # configurable, we replace ' with a placeholder to hide it from the
+ # parser.
+
+ # only treat ' at the start or end of words as quotes
+ # it's easier to do this in reverse with regexes
+ $line =~ s/(^|\s|:)'/$1\001/g;
+ $line =~ s/'($|\s)/\001$1/g;
+ $line =~ s/\\?'/\000/g;
+ $line =~ tr/\001/'/;
+
+ my @words = parse_line($delim, $keep, $line);
+ foreach my $word (@words) {
+ $word =~ tr/\000/'/;
+ }
+ return @words;
+}
+
sub _bug_numbers_only {
my $searchstring = shift;
my $cgi = Bugzilla->cgi;
@@ -363,25 +384,12 @@ sub _handle_special_first_chars {
sub _handle_field_names {
my ($or_operand, $negate, $unknownFields, $ambiguous_fields) = @_;
- # Flag and requestee shortcut
- if ($or_operand =~ /^(?:flag:)?([^\?]+\?)([^\?]*)$/) {
- my ($flagtype, $requestee) = ($1, $2);
- addChart('flagtypes.name', 'substring', $flagtype, $negate);
- if ($requestee) {
- # AND
- $chart++;
- $and = $or = 0;
- addChart('requestees.login_name', 'substring', $requestee, $negate);
- }
- return 1;
- }
-
# Generic field1,field2,field3:value1,value2 notation.
# We have to correctly ignore commas and colons in quotes.
- my @field_values = parse_line(':', 1, $or_operand);
+ my @field_values = _parse_line(':', 1, $or_operand);
if (scalar @field_values == 2) {
- my @fields = parse_line(',', 1, $field_values[0]);
- my @values = parse_line(',', 1, $field_values[1]);
+ my @fields = _parse_line(',', 1, $field_values[0]);
+ my @values = _parse_line(',', 1, $field_values[1]);
foreach my $field (@fields) {
my $translated = _translate_field_name($field);
# Skip and record any unknown fields
@@ -406,15 +414,48 @@ sub _handle_field_names {
$value = $2;
$value =~ s/\\(["'])/$1/g;
}
+ # If a requestee is set, we need to handle it separately.
+ if ($translated eq 'flagtypes.name' && $value =~ /^([^\?]+\?)([^\?]+)$/) {
+ _handle_flags($1, $2, $negate);
+ next;
+ }
addChart($translated, $operator, $value, $negate);
}
}
}
return 1;
}
+
+ # Do not look inside quoted strings.
+ return 0 if ($or_operand =~ /^(["']).*\1$/);
+
+ # Flag and requestee shortcut.
+ if ($or_operand =~ /^([^\?]+\?)([^\?]*)$/) {
+ _handle_flags($1, $2, $negate);
+ return 1;
+ }
+
return 0;
}
+sub _handle_flags {
+ my ($flag, $requestee, $negate) = @_;
+
+ addChart('flagtypes.name', 'substring', $flag, $negate);
+ if ($requestee) {
+ # FIXME - Every time a requestee is involved and you use OR somewhere
+ # in your quick search, the logic will be wrong because boolean charts
+ # are unable to run queries of the form (a AND b) OR c. In our case:
+ # (flag name is foo AND requestee is bar) OR (any other criteria).
+ # But this has never been possible, so this is not a regression. If one
+ # needs to run such queries, he must use the Custom Search section of
+ # the Advanced Search page.
+ $chart++;
+ $and = $or = 0;
+ addChart('requestees.login_name', 'substring', $requestee, $negate);
+ }
+}
+
sub _translate_field_name {
my $field = shift;
$field = lc($field);
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 264a28db1..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;
@@ -258,13 +262,18 @@ sub Cancel {
# Get information about the token being canceled.
trick_taint($token);
- my ($issuedate, $tokentype, $eventdata, $userid) =
- $dbh->selectrow_array('SELECT ' . $dbh->sql_date_format('issuedate') . ',
+ my ($db_token, $issuedate, $tokentype, $eventdata, $userid) =
+ $dbh->selectrow_array('SELECT token, ' . $dbh->sql_date_format('issuedate') . ',
tokentype, eventdata, userid
FROM tokens
WHERE token = ?',
undef, $token);
+ # Some DBs such as MySQL are case-insensitive by default so we do
+ # a quick comparison to make sure the tokens are indeed the same.
+ (defined $db_token && $db_token eq $token)
+ || ThrowCodeError("cancel_token_does_not_exist");
+
# If we are canceling the creation of a new user account, then there
# is no entry in the 'profiles' table.
my $user = new Bugzilla::User($userid);
@@ -329,10 +338,17 @@ sub GetTokenData {
$token = clean_text($token);
trick_taint($token);
- return $dbh->selectrow_array(
- "SELECT userid, " . $dbh->sql_date_format('issuedate') . ", eventdata, tokentype
- FROM tokens
+ my @token_data = $dbh->selectrow_array(
+ "SELECT token, userid, " . $dbh->sql_date_format('issuedate') . ", eventdata, tokentype
+ FROM tokens
WHERE token = ?", undef, $token);
+
+ # Some DBs such as MySQL are case-insensitive by default so we do
+ # a quick comparison to make sure the tokens are indeed the same.
+ my $db_token = shift @token_data;
+ return undef if (!defined $db_token || $db_token ne $token);
+
+ return @token_data;
}
# Deletes specified token
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 c787e0332..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) = @_;
@@ -2096,7 +2108,7 @@ sub validate_password {
my $complexity_level = Bugzilla->params->{password_complexity};
if ($complexity_level eq 'letters_numbers_specialchars') {
ThrowUserError('password_not_complex')
- if ($password !~ /\w/ || $password !~ /\d/ || $password !~ /[[:punct:]]/);
+ if ($password !~ /[[:alpha:]]/ || $password !~ /\d/ || $password !~ /[[:punct:]]/);
} elsif ($complexity_level eq 'letters_numbers') {
ThrowUserError('password_not_complex')
if ($password !~ /[[:lower:]]/ || $password !~ /[[:upper:]]/ || $password !~ /\d/);
@@ -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 b5d552130..5615f86ee 100644
--- a/Bugzilla/UserAgent.pm
+++ b/Bugzilla/UserAgent.pm
@@ -47,6 +47,7 @@ use constant PLATFORMS_MAP => (
# HP
qr/\(.*9000.*\)/ => ["PA-RISC", "HP"],
# ARM
+ qr/\(.*(?:iPod|iPad|iPhone).*\)/ => ["ARM"],
qr/\(.*ARM.*\)/ => ["ARM", "PocketPC"],
# PocketPC intentionally before PowerPC
qr/\(.*Windows CE.*PPC.*\)/ => ["ARM", "PocketPC"],
@@ -102,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"],
@@ -117,6 +120,12 @@ use constant OS_MAP => (
qr/\(.*Win(?:dows[ -]|)NT.*\)/ => ["Windows NT"],
qr/\(.*Windows.*NT.*\)/ => ["Windows NT"],
# OS X
+ qr/\(.*(?:iPad|iPhone).*OS 7.*\)/ => ["iOS 7"],
+ qr/\(.*(?:iPad|iPhone).*OS 6.*\)/ => ["iOS 6"],
+ qr/\(.*(?:iPad|iPhone).*OS 5.*\)/ => ["iOS 5"],
+ qr/\(.*(?:iPad|iPhone).*OS 4.*\)/ => ["iOS 4"],
+ qr/\(.*(?:iPad|iPhone).*OS 3.*\)/ => ["iOS 3"],
+ qr/\(.*(?:iPod|iPad|iPhone).*\)/ => ["iOS"],
qr/\(.*Mac OS X (?:|Mach-O |\()10.8.*\)/ => ["Mac OS X 10.8"],
qr/\(.*Mac OS X (?:|Mach-O |\()10.7.*\)/ => ["Mac OS X 10.7"],
qr/\(.*Mac OS X (?:|Mach-O |\()10.6.*\)/ => ["Mac OS X 10.6"],
diff --git a/Bugzilla/Util.pm b/Bugzilla/Util.pm
index ee8674b72..527bae85a 100644
--- a/Bugzilla/Util.pm
+++ b/Bugzilla/Util.pm
@@ -13,15 +13,16 @@ 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
is_7bit_clean bz_crypt generate_random_password
validate_email_syntax check_email_syntax clean_text
get_text template_var disable_utf8
- detect_encoding);
+ detect_encoding
+ join_activity_entries);
use Bugzilla::Constants;
use Bugzilla::RNG qw(irand);
@@ -67,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:
# --------------------------------------------------------
@@ -229,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).
@@ -472,6 +483,36 @@ sub find_wrap_point {
return $wrappoint;
}
+sub join_activity_entries {
+ my ($field, $current_change, $new_change) = @_;
+ # We need to insert characters as these were removed by old
+ # LogActivityEntry code.
+
+ return $new_change if $current_change eq '';
+
+ # Buglists and see_also need the comma restored
+ if ($field eq 'dependson' || $field eq 'blocked' || $field eq 'see_also') {
+ if (substr($new_change, 0, 1) eq ',' || substr($new_change, 0, 1) eq ' ') {
+ return $current_change . $new_change;
+ } else {
+ return $current_change . ', ' . $new_change;
+ }
+ }
+
+ # Assume bug_file_loc contain a single url, don't insert a delimiter
+ if ($field eq 'bug_file_loc') {
+ return $current_change . $new_change;
+ }
+
+ # 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;
+ }
+}
+
sub wrap_hard {
my ($string, $columns) = @_;
local $Text::Wrap::columns = $columns;
@@ -528,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],
@@ -587,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);
@@ -636,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 {
@@ -831,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
@@ -958,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
@@ -1029,6 +1086,12 @@ Search for a comma, a whitespace or a hyphen to split $string, within the first
$maxpos characters. If none of them is found, just split $string at $maxpos.
The search starts at $maxpos and goes back to the beginning of the string.
+=item C
+
+Joins two strings together so they appear as one. The field name is specified
+as the method of joining the two strings depends on this. Returns the
+combined string.
+
=item C
Returns true is the string contains only 7-bit characters (ASCII 32 through 126,
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 2c88232ac..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 });
@@ -866,8 +877,6 @@ sub _bug_to_hash {
# database call to get the info.
my %item = (
alias => $self->type('string', $bug->alias),
- classification => $self->type('string', $bug->classification),
- component => $self->type('string', $bug->component),
creation_time => $self->type('dateTime', $bug->creation_ts),
id => $self->type('int', $bug->bug_id),
is_confirmed => $self->type('boolean', $bug->everconfirmed),
@@ -875,7 +884,6 @@ sub _bug_to_hash {
op_sys => $self->type('string', $bug->op_sys),
platform => $self->type('string', $bug->rep_platform),
priority => $self->type('string', $bug->priority),
- product => $self->type('string', $bug->product),
resolution => $self->type('string', $bug->resolution),
severity => $self->type('string', $bug->bug_severity),
status => $self->type('string', $bug->bug_status),
@@ -897,6 +905,12 @@ sub _bug_to_hash {
my @blocks = map { $self->type('int', $_) } @{ $bug->blocked };
$item{'blocks'} = \@blocks;
}
+ if (filter_wants $params, 'classification') {
+ $item{classification} = $self->type('string', $bug->classification);
+ }
+ if (filter_wants $params, 'component') {
+ $item{component} = $self->type('string', $bug->component);
+ }
if (filter_wants $params, 'cc') {
my @cc = map { $self->type('string', $_) } @{ $bug->cc };
$item{'cc'} = \@cc;
@@ -924,6 +938,9 @@ sub _bug_to_hash {
@{ $bug->keyword_objects };
$item{'keywords'} = \@keywords;
}
+ if (filter_wants $params, 'product') {
+ $item{product} = $self->type('string', $bug->product);
+ }
if (filter_wants $params, 'qa_contact') {
my $qa_login = $bug->qa_contact ? $bug->qa_contact->login : '';
$item{'qa_contact'} = $self->type('string', $qa_login);
@@ -964,7 +981,10 @@ sub _bug_to_hash {
# No need to format $bug->deadline specially, because Bugzilla::Bug
# already does it for us.
$item{'deadline'} = $self->type('string', $bug->deadline);
- $item{'actual_time'} = $self->type('double', $bug->actual_time);
+
+ if (filter_wants $params, 'actual_time') {
+ $item{'actual_time'} = $self->type('double', $bug->actual_time);
+ }
}
if (Bugzilla->user->id) {
@@ -2202,6 +2222,59 @@ names used by L for consistency.
=back
+=head2 possible_duplicates
+
+B
+
+=over
+
+=item B
+
+Allows a user to find possible duplicate bugs based on a set of keywords
+such as a user may use as a bug summary. Optionally the search can be
+narrowed down to specific products.
+
+=item B
+
+=over
+
+=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
+duplicate search to. If omitted, all bugs are searched.
+
+=back
+
+=item B
+
+The same as L.
+
+Note that you will only be returned information about bugs that you
+can see. Bugs that you can't see will be entirely excluded from the
+results. So, if you want to see private bugs, you will have to first
+log in and I call this method.
+
+=item B
+
+=over
+
+=item 50 (Param Required)
+
+You must specify a value for C containing a string of keywords to
+search for duplicates.
+
+=back
+
+=item B
+
+=over
+
+=item Added in Bugzilla B<4.0>.
+
+=back
+
+=back
=head2 search
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 b3ba0942d..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',
@@ -839,10 +849,6 @@ You specified the name of a product that already exists.
You must specify a description for this product.
-=item 704 (Product must have version)
-
-You must specify a version for this product.
-
=item 705 (Product must define a default milestone)
You must define a default milestone.
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 e8fb5de99..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 {
@@ -48,8 +51,16 @@ sub make_response {
# XMLRPC::Transport::HTTP::CGI doesn't know about Bugzilla carrying around
# its cookies in Bugzilla::CGI, so we need to copy them over.
- foreach (@{Bugzilla->cgi->{'Bugzilla_cookie_list'}}) {
- $self->response->headers->push_header('Set-Cookie', $_);
+ foreach my $cookie (@{Bugzilla->cgi->{'Bugzilla_cookie_list'}}) {
+ $self->response->headers->push_header('Set-Cookie', $cookie);
+ }
+
+ # Copy across security related headers from Bugzilla::CGI
+ foreach my $header (split(/[\r\n]+/, Bugzilla->cgi->header)) {
+ my ($name, $value) = $header =~ /^([^:]+): (.*)/;
+ if (!$self->response->headers->header($name)) {
+ $self->response->headers->header($name => $value);
+ }
}
}
@@ -57,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;
}
@@ -74,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;
@@ -97,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 12606fb70..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 ($$;$) {
@@ -35,28 +36,38 @@ sub filter ($$;$) {
sub filter_wants ($$;$) {
my ($params, $field, $prefix) = @_;
- my %include = map { $_ => 1 } @{ $params->{'include_fields'} || [] };
- my %exclude = map { $_ => 1 } @{ $params->{'exclude_fields'} || [] };
+ # Since this is operation is resource intensive, we will cache the results
+ # This assumes that $params->{*_fields} doesn't change between calls
+ my $cache = Bugzilla->request_cache->{filter_wants} ||= {};
$field = "${prefix}.${field}" if $prefix;
+ if (exists $cache->{$field}) {
+ return $cache->{$field};
+ }
+
+ my %include = map { $_ => 1 } @{ $params->{'include_fields'} || [] };
+ my %exclude = map { $_ => 1 } @{ $params->{'exclude_fields'} || [] };
+
+ my $wants = 1;
if (defined $params->{exclude_fields} && $exclude{$field}) {
- return 0;
+ $wants = 0;
}
- if (defined $params->{include_fields} && !$include{$field}) {
+ elsif (defined $params->{include_fields} && !$include{$field}) {
if ($prefix) {
# Include the field if the parent is include (and this one is not excluded)
- return 0 if !$include{$prefix};
+ $wants = 0 if !$include{$prefix};
}
else {
# We want to include this if one of the sub keys is included
my $key = $field . '.';
my $len = length($key);
- return 0 if ! grep { substr($_, 0, $len) eq $key } keys %include;
+ $wants = 0 if ! grep { substr($_, 0, $len) eq $key } keys %include;
}
}
- return 1;
+ $cache->{$field} = $wants;
+ return $wants;
}
sub taint_data {
@@ -133,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
@@ -195,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 903fe5b59..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.
@@ -648,19 +655,22 @@ sub update {
$attachment->set_filename(scalar $cgi->param('filename'));
# Now make sure the attachment has not been edited since we loaded the page.
- if (defined $cgi->param('delta_ts')
- && $cgi->param('delta_ts') ne $attachment->modification_time)
- {
- ($vars->{'operations'}) = $bug->get_activity($attachment->id, $cgi->param('delta_ts'));
+ my $delta_ts = $cgi->param('delta_ts');
+ my $modification_time = $attachment->modification_time;
- # The token contains the old modification_time. We need a new one.
- $cgi->param('token', issue_hash_token([$attachment->id, $attachment->modification_time]));
+ if ($delta_ts && $delta_ts ne $modification_time) {
+ datetime_from($delta_ts)
+ or ThrowCodeError('invalid_timestamp', { timestamp => $delta_ts });
+ ($vars->{'operations'}) = $bug->get_activity($attachment->id, $delta_ts);
# If the modification date changed but there is no entry in
# the activity table, this means someone commented only.
# In this case, there is no reason to midair.
if (scalar(@{$vars->{'operations'}})) {
- $cgi->param('delta_ts', $attachment->modification_time);
+ $cgi->param('delta_ts', $modification_time);
+ # The token contains the old modification_time. We need a new one.
+ $cgi->param('token', issue_hash_token([$attachment->id, $modification_time]));
+
$vars->{'attachment'} = $attachment;
print $cgi->header();
@@ -697,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
@@ -704,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 fbdbb8593..e3d8fe711 100755
--- a/buglist.cgi
+++ b/buglist.cgi
@@ -110,7 +110,7 @@ my $serverpush =
&& exists $ENV{'HTTP_USER_AGENT'}
&& $ENV{'HTTP_USER_AGENT'} =~ /(Mozilla.[3-9]|Opera)/
&& $ENV{'HTTP_USER_AGENT'} !~ /compatible/i
- && $ENV{'HTTP_USER_AGENT'} !~ /WebKit/
+ && $ENV{'HTTP_USER_AGENT'} !~ /(?:WebKit|Trident|KHTML)/
&& !defined($cgi->param('serverpush'))
|| $cgi->param('serverpush');
@@ -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/contrib/syncLDAP.pl b/contrib/syncLDAP.pl
index ec0839ed5..6ad96477b 100755
--- a/contrib/syncLDAP.pl
+++ b/contrib/syncLDAP.pl
@@ -240,22 +240,15 @@ if($readonly == 0) {
print "Phase 2: updating existing users... " unless $quiet;
- my $sth_update_login = $dbh->prepare(
- 'UPDATE profiles
- SET login_name = ?
- WHERE ' . $dbh->sql_istrcmp('login_name', '?'));
- my $sth_update_realname = $dbh->prepare(
- 'UPDATE profiles
- SET realname = ?
- WHERE ' . $dbh->sql_istrcmp('login_name', '?'));
-
if($noupdate == 0) {
while( my ($key, $value) = each(%update_users) ) {
+ my $user = Bugzilla::User->check($key);
if(defined $value->{'new_login_name'}) {
- $sth_update_login->execute($value->{'new_login_name'}, $key);
+ $user->set_login($value->{'new_login_name'});
} else {
- $sth_update_realname->execute($value->{'realname'}, $key);
+ $user->set_name($value->{'realname'});
}
+ $user->update();
}
print "done!\n" unless $quiet;
}
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/editwhines.cgi b/editwhines.cgi
index 5227f1d62..537c159dc 100755
--- a/editwhines.cgi
+++ b/editwhines.cgi
@@ -25,6 +25,8 @@ use Bugzilla::Whine::Schedule;
use Bugzilla::Whine::Query;
use Bugzilla::Whine;
+use DateTime;
+
# require the user to have logged in
my $user = Bugzilla->login(LOGIN_REQUIRED);
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/extensions/Voting/Extension.pm b/extensions/Voting/Extension.pm
index 49ab99e13..d186e442a 100644
--- a/extensions/Voting/Extension.pm
+++ b/extensions/Voting/Extension.pm
@@ -19,7 +19,7 @@ use Bugzilla::User;
use Bugzilla::Util qw(detaint_natural);
use Bugzilla::Token;
-use List::Util qw(min);
+use List::Util qw(min sum);
use constant VERSION => BUGZILLA_VERSION;
use constant DEFAULT_VOTES_PER_BUG => 1;
@@ -185,7 +185,7 @@ sub bug_end_of_update {
# If some votes have been removed, RemoveVotes() returns
# a list of messages to send to voters.
@msgs = _remove_votes($bug->id, 0, 'votes_bug_moved');
- _confirm_if_vote_confirmed($bug->id);
+ _confirm_if_vote_confirmed($bug);
foreach my $msg (@msgs) {
MessageToMTA($msg);
@@ -418,53 +418,38 @@ sub _page_user {
foreach my $product (@{ $user->get_selectable_products }) {
next unless ($product->{votesperuser} > 0);
- my @bugs;
- my @bug_ids;
- my $total = 0;
- my $onevoteonly = 0;
-
my $vote_list =
- $dbh->selectall_arrayref('SELECT votes.bug_id, votes.vote_count,
- bugs.short_desc
- FROM votes
- INNER JOIN bugs
- ON votes.bug_id = bugs.bug_id
- WHERE votes.who = ?
- AND bugs.product_id = ?
- ORDER BY votes.bug_id',
- undef, ($who->id, $product->id));
-
- $user->visible_bugs([map { $_->[0] } @$vote_list]);
- foreach (@$vote_list) {
- my ($id, $count, $summary) = @$_;
- $total += $count;
-
- # Next if user can't see this bug. So, the totals will be correct
- # and they can see there are votes 'missing', but not on what bug
- # they are. This seems a reasonable compromise; the alternative is
- # to lie in the totals.
- next if !$user->can_see_bug($id);
-
- push (@bugs, { id => $id,
- summary => $summary,
- count => $count });
- push (@bug_ids, $id);
- push (@all_bug_ids, $id);
- }
+ $dbh->selectall_arrayref('SELECT votes.bug_id, votes.vote_count
+ FROM votes
+ INNER JOIN bugs
+ ON votes.bug_id = bugs.bug_id
+ WHERE votes.who = ?
+ AND bugs.product_id = ?',
+ undef, ($who->id, $product->id));
+
+ my %votes = map { $_->[0] => $_->[1] } @$vote_list;
+ my @bug_ids = sort keys %votes;
+ # Exclude bugs that the user can no longer see.
+ @bug_ids = @{ $user->visible_bugs(\@bug_ids) };
+ next unless scalar @bug_ids;
+
+ push(@all_bug_ids, @bug_ids);
+ my @bugs = @{ Bugzilla::Bug->new_from_list(\@bug_ids) };
+ $_->{count} = $votes{$_->id} foreach @bugs;
+ # We include votes from bugs that the user can no longer see.
+ my $total = sum(values %votes) || 0;
+ my $onevoteonly = 0;
$onevoteonly = 1 if (min($product->{votesperuser},
$product->{maxvotesperbug}) == 1);
- # Only add the product for display if there are any bugs in it.
- if ($#bugs > -1) {
- push (@products, { name => $product->name,
- bugs => \@bugs,
- bug_ids => \@bug_ids,
- onevoteonly => $onevoteonly,
- total => $total,
- maxvotes => $product->{votesperuser},
- maxperbug => $product->{maxvotesperbug} });
- }
+ push(@products, { name => $product->name,
+ bugs => \@bugs,
+ bug_ids => \@bug_ids,
+ onevoteonly => $onevoteonly,
+ total => $total,
+ maxvotes => $product->{votesperuser},
+ maxperbug => $product->{maxvotesperbug} });
}
if ($canedit && $bug) {
@@ -498,6 +483,7 @@ sub _update_votes {
# IDs and the field values are the number of votes.
my @buglist = grep {/^\d+$/} keys %$input;
+ my (%bugs, %votes);
# If no bugs are in the buglist, let's make sure the user gets notified
# that their votes will get nuked if they continue.
@@ -513,20 +499,23 @@ sub _update_votes {
exit;
}
}
+ else {
+ $user->visible_bugs(\@buglist);
+ my $bugs_obj = Bugzilla::Bug->new_from_list(\@buglist);
+ $bugs{$_->id} = $_ foreach @$bugs_obj;
+ }
- # Call check() on each bug ID to make sure it is a positive
- # integer representing an existing bug that the user is authorized
- # to access, and make sure the number of votes submitted is also
- # a non-negative integer (a series of digits not preceded by a
- # minus sign).
- my (%votes, @bugs);
+ # Call check_is_visible() on each bug to make sure it is an existing bug
+ # that the user is authorized to access, and make sure the number of votes
+ # submitted is also an integer.
foreach my $id (@buglist) {
- my $bug = Bugzilla::Bug->check($id);
- push(@bugs, $bug);
- $id = $bug->id;
- $votes{$id} = $input->{$id};
- detaint_natural($votes{$id})
- || ThrowUserError("voting_must_be_nonnegative");
+ my $bug = $bugs{$id}
+ or ThrowUserError('bug_id_does_not_exist', { bug_id => $id });
+ $bug->check_is_visible;
+ $id = $bug->id;
+ $votes{$id} = $input->{$id};
+ detaint_natural($votes{$id})
+ || ThrowUserError("voting_must_be_nonnegative");
}
my $token = $cgi->param('token');
@@ -539,10 +528,10 @@ sub _update_votes {
# If the user is voting for bugs, make sure they aren't overstuffing
# the ballot box.
- if (scalar @bugs) {
+ if (scalar @buglist) {
my (%prodcount, %products);
- foreach my $bug (@bugs) {
- my $bug_id = $bug->id;
+ foreach my $bug_id (keys %bugs) {
+ my $bug = $bugs{$bug_id};
my $prod = $bug->product;
$products{$prod} ||= $bug->product_obj;
$prodcount{$prod} ||= 0;
@@ -566,56 +555,65 @@ sub _update_votes {
}
}
- # Update the user's votes in the database. If the user did not submit
- # any votes, they may be using a form with checkboxes to remove all their
- # votes (checkboxes are not submitted along with other form data when
- # they are not checked, and Bugzilla uses them to represent single votes
- # for products that only allow one vote per bug). In that case, we still
- # need to clear the user's votes from the database.
- my %affected;
+ # Update the user's votes in the database.
$dbh->bz_start_transaction();
- # Take note of, and delete the user's old votes from the database.
- my $bug_list = $dbh->selectcol_arrayref('SELECT bug_id FROM votes
+ my $old_list = $dbh->selectall_arrayref('SELECT bug_id, vote_count FROM votes
WHERE who = ?', undef, $who);
- foreach my $id (@$bug_list) {
- $affected{$id} = 1;
- }
- $dbh->do('DELETE FROM votes WHERE who = ?', undef, $who);
+ my %old_votes = map { $_->[0] => $_->[1] } @$old_list;
my $sth_insertVotes = $dbh->prepare('INSERT INTO votes (who, bug_id, vote_count)
VALUES (?, ?, ?)');
+ my $sth_updateVotes = $dbh->prepare('UPDATE votes SET vote_count = ?
+ WHERE bug_id = ? AND who = ?');
- # Insert the new values in their place
- foreach my $id (@buglist) {
- if ($votes{$id} > 0) {
+ my %affected = map { $_ => 1 } (@buglist, keys %old_votes);
+ my @deleted_votes;
+
+ foreach my $id (keys %affected) {
+ if (!$votes{$id}) {
+ push(@deleted_votes, $id);
+ next;
+ }
+ if ($votes{$id} == ($old_votes{$id} || 0)) {
+ delete $affected{$id};
+ next;
+ }
+ # We use 'defined' in case 0 was accidentally stored in the DB.
+ if (defined $old_votes{$id}) {
+ $sth_updateVotes->execute($votes{$id}, $id, $who);
+ }
+ else {
$sth_insertVotes->execute($who, $id, $votes{$id});
}
- $affected{$id} = 1;
+ }
+
+ if (@deleted_votes) {
+ $dbh->do('DELETE FROM votes WHERE who = ? AND ' .
+ $dbh->sql_in('bug_id', \@deleted_votes), undef, $who);
}
# Update the cached values in the bugs table
- print $cgi->header();
my @updated_bugs = ();
my $sth_getVotes = $dbh->prepare("SELECT SUM(vote_count) FROM votes
WHERE bug_id = ?");
- my $sth_updateVotes = $dbh->prepare("UPDATE bugs SET votes = ?
- WHERE bug_id = ?");
+ $sth_updateVotes = $dbh->prepare('UPDATE bugs SET votes = ? WHERE bug_id = ?');
foreach my $id (keys %affected) {
$sth_getVotes->execute($id);
my $v = $sth_getVotes->fetchrow_array || 0;
$sth_updateVotes->execute($v, $id);
- my $confirmed = _confirm_if_vote_confirmed($id);
+ my $confirmed = _confirm_if_vote_confirmed($bugs{$id} || $id);
push (@updated_bugs, $id) if $confirmed;
}
$dbh->bz_commit_transaction();
+ print $cgi->header() if scalar @updated_bugs;
$vars->{'type'} = "votes";
$vars->{'title_tag'} = 'change_votes';
foreach my $bug_id (@updated_bugs) {
@@ -826,7 +824,7 @@ sub _remove_votes {
# confirm a bug has been reduced, check if the bug is now confirmed.
sub _confirm_if_vote_confirmed {
my $id = shift;
- my $bug = new Bugzilla::Bug($id);
+ my $bug = ref $id ? $id : new Bugzilla::Bug($id);
my $ret = 0;
if (!$bug->everconfirmed
diff --git a/extensions/Voting/template/en/default/pages/voting/user.html.tmpl b/extensions/Voting/template/en/default/pages/voting/user.html.tmpl
index 27ce2acda..8777e036b 100644
--- a/extensions/Voting/template/en/default/pages/voting/user.html.tmpl
+++ b/extensions/Voting/template/en/default/pages/voting/user.html.tmpl
@@ -95,8 +95,7 @@
[% FOREACH bug = product.bugs %]
-
+
[% IF bug.id == this_bug.id && canedit %]
[% IF product.onevoteonly %]
@@ -106,25 +105,25 @@
[% END %]
[%- END %]
- [% bug.summary FILTER html %]
- (Show Votes)
+ [% bug.short_desc FILTER html %]
+ (Show Votes)
[% END %]
diff --git a/importxml.pl b/importxml.pl
index 7a1181158..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;
@@ -481,7 +482,7 @@ sub process_bug {
foreach my $comment ( $bug->children('long_desc') ) {
Debug( "Parsing Long Description", DEBUG_LEVEL );
my %long_desc = ( who => $comment->field('who'),
- bug_when => $comment->field('bug_when'),
+ bug_when => format_time($comment->field('bug_when'), '%Y-%m-%d %T'),
isprivate => $comment->{'att'}->{'isprivate'} || 0 );
# If the exporter is not in the insidergroup, keep the comment public.
@@ -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 79eeca424..e063c3db4 100755
--- a/process_bug.cgi
+++ b/process_bug.cgi
@@ -111,25 +111,24 @@ print $cgi->header() unless Bugzilla->usage_mode == USAGE_MODE_EMAIL;
# Check for a mid-air collision. Currently this only works when updating
# an individual bug.
-if (defined $cgi->param('delta_ts'))
-{
- my $delta_ts_z = datetime_from($cgi->param('delta_ts'));
+my $delta_ts = $cgi->param('delta_ts') || '';
+
+if ($delta_ts) {
+ my $delta_ts_z = datetime_from($delta_ts)
+ or ThrowCodeError('invalid_timestamp', { timestamp => $delta_ts });
+
my $first_delta_tz_z = datetime_from($first_bug->delta_ts);
- if ($first_delta_tz_z ne $delta_ts_z) {
- ($vars->{'operations'}) = $first_bug->get_activity(undef, $cgi->param('delta_ts'));
- ThrowCodeError('undefined_field', { field => 'longdesclength' })
- if !defined $cgi->param('longdesclength');
+ if ($first_delta_tz_z ne $delta_ts_z) {
+ ($vars->{'operations'}) = $first_bug->get_activity(undef, $delta_ts);
- my $start_at = $cgi->param('longdesclength');
+ my $start_at = $cgi->param('longdesclength')
+ or ThrowCodeError('undefined_field', { field => 'longdesclength' });
# Always sort midair collision comments oldest to newest,
# regardless of the user's personal preference.
my $comments = $first_bug->comments({ order => "oldest_to_newest" });
- # The token contains the old delta_ts. We need a new one.
- $cgi->param('token', issue_hash_token([$first_bug->id, $first_bug->delta_ts]));
-
# Show midair if previous changes made other than CC
# and/or one or more comments were made
my $do_midair = scalar @$comments > $start_at ? 1 : 0;
@@ -137,8 +136,10 @@ if (defined $cgi->param('delta_ts'))
if (!$do_midair) {
foreach my $operation (@{ $vars->{'operations'} }) {
foreach my $change (@{ $operation->{'changes'} }) {
- $do_midair = 1 if $change->{'fieldname'} ne 'cc';
- last;
+ if ($change->{'fieldname'} ne 'cc') {
+ $do_midair = 1;
+ last;
+ }
}
last if $do_midair;
}
@@ -149,6 +150,8 @@ if (defined $cgi->param('delta_ts'))
$vars->{'start_at'} = $start_at;
$vars->{'comments'} = $comments;
$vars->{'bug'} = $first_bug;
+ # The token contains the old delta_ts. We need a new one.
+ $cgi->param('token', issue_hash_token([$first_bug->id, $first_bug->delta_ts]));
# Warn the user about the mid-air collision and ask them what to do.
$template->process("bug/process/midair.html.tmpl", $vars)
@@ -164,7 +167,7 @@ if (defined $cgi->param('delta_ts'))
my $token = $cgi->param('token');
if ($cgi->param('id')) {
- check_hash_token($token, [$first_bug->id, $first_bug->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/request.cgi b/request.cgi
index 3bae8c094..bbed1c79b 100755
--- a/request.cgi
+++ b/request.cgi
@@ -159,22 +159,8 @@ sub queue {
# need to display a "status" column in the report because the value for that
# column will always be the same.
my @excluded_columns = ();
-
my $do_union = $cgi->param('do_union');
- # Filter requests by status: "pending", "granted", "denied", "all"
- # (which means any), or "fulfilled" (which means "granted" or "denied").
- if ($status) {
- if ($status eq "+-") {
- push(@criteria, "flags.status IN ('+', '-')");
- push(@excluded_columns, 'status') unless $do_union;
- }
- elsif ($status ne "all") {
- push(@criteria, "flags.status = '$status'");
- push(@excluded_columns, 'status') unless $do_union;
- }
- }
-
# Filter results by exact email address of requester or requestee.
if (defined $cgi->param('requester') && $cgi->param('requester') ne "") {
my $requester = $dbh->quote($cgi->param('requester'));
@@ -186,23 +172,44 @@ sub queue {
if ($cgi->param('requestee') ne "-") {
my $requestee = $dbh->quote($cgi->param('requestee'));
trick_taint($requestee); # Quoted above
- push(@criteria, $dbh->sql_istrcmp('requestees.login_name',
- $requestee));
+ push(@criteria, $dbh->sql_istrcmp('requestees.login_name', $requestee));
+ }
+ else {
+ push(@criteria, "flags.requestee_id IS NULL");
}
- else { push(@criteria, "flags.requestee_id IS NULL") }
push(@excluded_columns, 'requestee') unless $do_union;
}
-
+
+ # If the user wants requester = foo OR requestee = bar, we have to join
+ # these criteria separately as all other criteria use AND.
+ if (@criteria == 2 && $do_union) {
+ my $union = join(' OR ', @criteria);
+ @criteria = ("($union)");
+ }
+
+ # Filter requests by status: "pending", "granted", "denied", "all"
+ # (which means any), or "fulfilled" (which means "granted" or "denied").
+ if ($status) {
+ if ($status eq "+-") {
+ push(@criteria, "flags.status IN ('+', '-')");
+ push(@excluded_columns, 'status');
+ }
+ elsif ($status ne "all") {
+ push(@criteria, "flags.status = '$status'");
+ push(@excluded_columns, 'status');
+ }
+ }
+
# Filter results by exact product or component.
if (defined $cgi->param('product') && $cgi->param('product') ne "") {
my $product = Bugzilla::Product->check(scalar $cgi->param('product'));
push(@criteria, "bugs.product_id = " . $product->id);
- push(@excluded_columns, 'product') unless $do_union;
+ push(@excluded_columns, 'product');
if (defined $cgi->param('component') && $cgi->param('component') ne "") {
my $component = Bugzilla::Component->check({ product => $product,
name => scalar $cgi->param('component') });
push(@criteria, "bugs.component_id = " . $component->id);
- push(@excluded_columns, 'component') unless $do_union;
+ push(@excluded_columns, 'component');
}
}
@@ -220,14 +227,11 @@ sub queue {
my $quoted_form_type = $dbh->quote($form_type);
trick_taint($quoted_form_type); # Already SQL quoted
push(@criteria, "flagtypes.name = " . $quoted_form_type);
- push(@excluded_columns, 'type') unless $do_union;
+ push(@excluded_columns, 'type');
}
-
- # Add the criteria to the query. Do a union if OR is selected.
- # Otherwise do an intersection.
- my $and_or = $do_union ? ' OR ' : ' AND ';
- $query .= " AND (" . join($and_or, @criteria) . ") " if scalar(@criteria);
-
+
+ $query .= ' AND ' . join(' AND ', @criteria) if scalar(@criteria);
+
# Group the records by flag ID so we don't get multiple rows of data
# for each flag. This is only necessary because of the code that
# removes flags on bugs the user is unauthorized to access.
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 = "