diff --git a/.bzrignore b/.bzrignore index f30e8f9ae..4b27e5744 100644 --- a/.bzrignore +++ b/.bzrignore @@ -1,4 +1,3 @@ -.htaccess /lib/* /template/en/custom /docs/en/html diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..f0e4ee1f4 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,12 @@ +#### Details + +This PR fixes/adds a feature... + +#### Additional info +* [bmo#](https://bugzilla.mozilla.org/show_bug.cgi?id=) + +#### Test Plan + +1. Open the show_bug view +2. Edit the bug +3. ... diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..c9198c50e --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,74 @@ +# This is a basic workflow to help you get started with Actions + +name: Release Tests + +# Controls when the action will run. Triggers the workflow on push or pull request +# events but only for the main branch +on: + push: + branches: [ 5.0.4 ] + pull_request: + branches: [ 5.0.4 ] + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + ubuntu: + name: Release Tests on Ubuntu 20.04 + runs-on: ubuntu-20.04 + steps: + - name: Checkout the repository + uses: actions/checkout@v4 + - name: apt install + run: | + sudo apt-get update + sudo apt-get -y dist-upgrade + sudo apt-get install --ignore-hold --allow-downgrades -y \ + apache2 \ + mariadb-client-10.3 \ + netcat \ + libappconfig-perl \ + libdate-calc-perl \ + libtemplate-perl \ + build-essential \ + libdatetime-timezone-perl \ + libdatetime-perl \ + libemail-address-perl \ + libemail-sender-perl \ + libemail-mime-perl \ + libemail-mime-modifier-perl \ + libdbi-perl \ + libdbix-connector-perl \ + libdbd-mysql-perl \ + libcgi-pm-perl \ + libmath-random-isaac-perl \ + libmath-random-isaac-xs-perl \ + libapache2-mod-perl2 \ + libapache2-mod-perl2-dev \ + libchart-perl \ + libxml-perl \ + libxml-twig-perl \ + perlmagick \ + libgd-graph-perl \ + libtemplate-plugin-gd-perl \ + libsoap-lite-perl \ + libhtml-scrubber-perl \ + libjson-rpc-perl \ + libdaemon-generic-perl \ + libtheschwartz-perl \ + libtest-taint-perl \ + libauthen-radius-perl \ + libfile-slurp-perl \ + libencode-detect-perl \ + libmodule-build-perl \ + libnet-ldap-perl \ + libauthen-sasl-perl \ + libfile-mimeinfo-perl \ + libhtml-formattext-withlinks-perl \ + libpod-coverage-perl \ + graphviz + - name: Get Perl Version and debug info + run: '/usr/bin/perl -V' + - name: Run tests + run: '/usr/bin/perl runtests.pl' diff --git a/.gitignore b/.gitignore index f30e8f9ae..ba98f70c2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -.htaccess +/**/.htaccess /lib/* /template/en/custom /docs/en/html diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 000000000..7eada0dd6 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,12 @@ +version: 2 +build: + os: "ubuntu-20.04" + tools: + python: "3.10" + apt_packages: + - libfile-copy-recursive-perl + jobs: + post_build: + - perl docs/makedocs.pl --pod-only + - cp -rp docs/en/html/integrating/api/ "$READTHEDOCS_OUTPUT/html/integrating/api" + - cp -p docs/en/html/style.css "$READTHEDOCS_OUTPUT/html/" diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 01ecfcfb4..000000000 --- a/.travis.yml +++ /dev/null @@ -1,78 +0,0 @@ -language: perl - -addons: - postgresql: "9.1" - -perl: - - 5.10 - - 5.12 - - 5.14 - - 5.16 - -env: - - TEST_SUITE=sanity - - TEST_SUITE=docs - - TEST_SUITE=webservices DB=mysql - - TEST_SUITE=selenium DB=mysql - - TEST_SUITE=webservices DB=pg - - TEST_SUITE=selenium DB=pg - -matrix: - exclude: - - perl: 5.10 - env: TEST_SUITE=docs - - perl: 5.12 - env: TEST_SUITE=docs - - perl: 5.14 - env: TEST_SUITE=docs - - perl: 5.10 - env: TEST_SUITE=webservices DB=mysql - - perl: 5.10 - env: TEST_SUITE=webservices DB=pg - - perl: 5.10 - env: TEST_SUITE=selenium DB=mysql - - perl: 5.10 - env: TEST_SUITE=selenium DB=pg - - perl: 5.12 - env: TEST_SUITE=webservices DB=mysql - - perl: 5.12 - env: TEST_SUITE=webservices DB=pg - - perl: 5.12 - env: TEST_SUITE=selenium DB=mysql - - perl: 5.12 - env: TEST_SUITE=selenium DB=pg - - perl: 5.14 - env: TEST_SUITE=webservices DB=mysql - - perl: 5.14 - env: TEST_SUITE=webservices DB=pg - - perl: 5.14 - env: TEST_SUITE=selenium DB=mysql - - perl: 5.14 - env: TEST_SUITE=selenium DB=pg - -before_install: - - git clone https://github.com/bugzilla/qa.git -b 5.0 qa - -install: true - -before_script: - - mysql -u root mysql -e "GRANT ALL PRIVILEGES ON *.* TO bugs@localhost IDENTIFIED BY 'bugs'; FLUSH PRIVILEGES;" - - psql -c "CREATE USER bugs WITH PASSWORD 'bugs' CREATEDB;" -U postgres - -script: ./qa/travis.sh - -after_failure: - - sudo cat /var/log/apache2/error.log - -notifications: - irc: - channels: - - "irc.mozilla.org#qa-bugzilla" - - "irc.mozilla.org#bugzilla" - template: - - "Bugzilla %{branch} : %{author} : %{message}" - - "Commit Message : %{commit_message}" - - "Commit Link : %{compare_url}" - - "Build Link : %{build_url}" - on_success: change - on_failure: always diff --git a/Bugzilla.pm b/Bugzilla.pm index aa78f1483..e8c078cf8 100644 --- a/Bugzilla.pm +++ b/Bugzilla.pm @@ -11,6 +11,8 @@ use 5.10.1; use strict; use warnings; +BEGIN { eval { utf8->import; require 'utf8_heavy.pl' }; } + # We want any compile errors to get to the browser, if possible. BEGIN { # This makes sure we're in a CGI. @@ -87,7 +89,7 @@ sub init_page { # On Windows, these paths are tainted, preventing # File::Spec::Win32->tmpdir from using them. But we need # a place to temporary store attachments which are uploaded. - foreach my $temp (qw(TMPDIR TMP TEMP)) { + foreach my $temp (qw(TMPDIR TMP TEMP WINDIR)) { trick_taint($ENV{$temp}) if $ENV{$temp}; } # Some DLLs used by Strawberry Perl are also in c\bin, @@ -96,6 +98,7 @@ sub init_page { my $c_path = $path = dirname($^X); $c_path =~ s/\bperl\b(?=\\bin)/c/; $path .= ";$c_path"; + trick_taint($path); } } # Some environment variables are not taint safe diff --git a/Bugzilla/Attachment.pm b/Bugzilla/Attachment.pm index 932fb6b17..33183797b 100644 --- a/Bugzilla/Attachment.pm +++ b/Bugzilla/Attachment.pm @@ -865,6 +865,8 @@ sub create { sub run_create_validators { my ($class, $params) = @_; + $params->{submitter_id} = Bugzilla->user->id || ThrowUserError('invalid_user'); + # Let's validate the attachment content first as it may # alter some other attachment attributes. $params->{data} = $class->_check_data($params); @@ -872,7 +874,6 @@ sub run_create_validators { $params->{creation_ts} ||= Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); $params->{modification_time} = $params->{creation_ts}; - $params->{submitter_id} = Bugzilla->user->id || ThrowUserError('invalid_user'); return $params; } diff --git a/Bugzilla/Bug.pm b/Bugzilla/Bug.pm index 34bf95ff7..8b4493f85 100644 --- a/Bugzilla/Bug.pm +++ b/Bugzilla/Bug.pm @@ -305,15 +305,15 @@ sub new { my $param = shift; # Remove leading "#" mark if we've just been passed an id. - if (!ref $param && $param =~ /^#(\d+)$/) { + if (!ref $param && $param =~ /^#([0-9]+)$/) { $param = $1; } # If we get something that looks like a word (not a number), # make it the "name" param. if (!defined $param - || (!ref($param) && $param !~ /^\d+$/) - || (ref($param) && $param->{id} !~ /^\d+$/)) + || (!ref($param) && $param !~ /^[0-9]+$/) + || (ref($param) && $param->{id} !~ /^[0-9]+$/)) { if ($param) { my $alias = ref($param) ? $param->{id} : $param; @@ -556,15 +556,15 @@ sub _extract_bug_ids { my $s = $comment->already_wrapped ? qr/\s/ : qr/\h/; my $text = $comment->body; # Full bug links - push @bug_ids, $text =~ /\b$urlbase_re\Qshow_bug.cgi?id=\E(\d+)(?:\#c\d+)?/g; + push @bug_ids, $text =~ /\b$urlbase_re\Qshow_bug.cgi?id=\E([0-9]+)(?:\#c[0-9]+)?/g; # bug X - my $bug_re = qr/\Q$bug_word\E$s*\#?$s*(\d+)/i; + my $bug_re = qr/\Q$bug_word\E$s*\#?$s*([0-9]+)/i; push @bug_ids, $text =~ /\b$bug_re/g; # bugs X, Y, Z - my $bugs_re = qr/\Q$bugs_word\E$s*\#?$s*(\d+)(?:$s*,$s*\#?$s*(\d+))+/i; + my $bugs_re = qr/\Q$bugs_word\E$s*\#?$s*([0-9]+)(?:$s*,$s*\#?$s*([0-9]+))+/i; push @bug_ids, $text =~ /\b$bugs_re/g; # Old duplicate markers - push @bug_ids, $text =~ /(?<=^\*\*\*\ This\ bug\ has\ been\ marked\ as\ a\ duplicate\ of\ )(\d+)(?=\ \*\*\*\Z)/; + push @bug_ids, $text =~ /(?<=^\*\*\*\ This\ bug\ has\ been\ marked\ as\ a\ duplicate\ of\ )([0-9]+)(?=\ \*\*\*\Z)/; } # Make sure to filter invalid bug IDs. @bug_ids = grep { $_ < MAX_INT_32 } @bug_ids; diff --git a/Bugzilla/BugMail.pm b/Bugzilla/BugMail.pm index d4a1597ab..1aff405d8 100644 --- a/Bugzilla/BugMail.pm +++ b/Bugzilla/BugMail.pm @@ -21,6 +21,7 @@ use Bugzilla::Mailer; use Bugzilla::Hook; use Bugzilla::MIME; +use Encode qw(); use Date::Parse; use Date::Format; use Scalar::Util qw(blessed); @@ -169,8 +170,8 @@ sub Send { } if ($change->{field_name} eq 'dependson' || $change->{field_name} eq 'blocked') { - push @referenced_bug_ids, split(/[\s,]+/, $change->{old}); - push @referenced_bug_ids, split(/[\s,]+/, $change->{new}); + push @referenced_bug_ids, split(/[\s,]+/, $change->{old} // ''); + push @referenced_bug_ids, split(/[\s,]+/, $change->{new} // ''); } } @@ -451,6 +452,7 @@ sub _generate_bugmail { encoding => 'quoted-printable', }, body_str => $msg_text, + encode_check => Encode::FB_DEFAULT ) ); if ($user->setting('email_format') eq 'html') { @@ -463,13 +465,15 @@ sub _generate_bugmail { encoding => 'quoted-printable', }, body_str => $msg_html, + encode_check => Encode::FB_DEFAULT ); } my $email = Bugzilla::MIME->new($msg_header); - if (scalar(@parts) == 1) { - $email->content_type_set($parts[0]->content_type); - } else { + + # If there's only one part, we don't need to set the overall content type + # because Email::MIME will automatically take it from that part (bug 1657496) + if (scalar(@parts) > 1) { $email->content_type_set('multipart/alternative'); # Some mail clients need same encoding for each part, even empty ones. $email->charset_set('UTF-8') if $use_utf8; diff --git a/Bugzilla/BugUrl.pm b/Bugzilla/BugUrl.pm index 1e836ca1e..1d75fe8f1 100644 --- a/Bugzilla/BugUrl.pm +++ b/Bugzilla/BugUrl.pm @@ -108,8 +108,9 @@ sub _do_list_select { my $objects = $class->SUPER::_do_list_select(@_); foreach my $object (@$objects) { - eval "use " . $object->class; die $@ if $@; - bless $object, $object->class; + eval "use " . $object->class; + # If the class cannot be loaded, then we build a generic object. + bless $object, ($@ ? 'Bugzilla::BugUrl' : $object->class); } return $objects diff --git a/Bugzilla/CGI.pm b/Bugzilla/CGI.pm index 0b8a48697..9b1ff9235 100644 --- a/Bugzilla/CGI.pm +++ b/Bugzilla/CGI.pm @@ -66,7 +66,7 @@ sub new { # else we will be redirected outside Bugzilla. my $script_name = $self->script_name; $path_info =~ s/^\Q$script_name\E//; - if ($path_info) { + if ($script_name && $path_info) { print $self->redirect($self->url(-path => 0, -query => 1)); } } @@ -283,11 +283,74 @@ sub close_standby_message { print $self->multipart_end(); print $self->multipart_start(-type => $contenttype); } - else { + elsif (!$self->{_header_done}) { print $self->header($contenttype); } } +our $ALLOW_UNSAFE_RESPONSE = 0; +# responding to text/plain or text/html is safe +# responding to any request with a referer header is safe +# some things need to have unsafe responses (attachment.cgi) +# everything else should get a 403. +sub _prevent_unsafe_response { + my ($self, $headers) = @_; + my $safe_content_type_re = qr{ + ^ (*COMMIT) # COMMIT makes the regex faster + # by preventing back-tracking. see also perldoc pelre. + # application/x-javascript, xml, atom+xml, rdf+xml, xml-dtd, and json + (?: application/ (?: x(?: -javascript | ml (?: -dtd )? ) + | (?: atom | rdf) \+ xml + | json ) + # text/csv, text/calendar, text/plain, and text/html + | text/ (?: c (?: alendar | sv ) + | plain + | html ) + # used for HTTP push responses + | multipart/x-mixed-replace) + }sx; + my $safe_referer_re = do { + # Note that urlbase must end with a /. + # It almost certainly does, but let's be extra careful. + my $urlbase = correct_urlbase(); + $urlbase =~ s{/$}{}; + qr{ + # Begins with literal urlbase + ^ (*COMMIT) + \Q$urlbase\E + # followed by a slash or end of string + (?: / + | $ ) + }sx + }; + + return if $ALLOW_UNSAFE_RESPONSE; + + if (Bugzilla->usage_mode == USAGE_MODE_BROWSER) { + # Safe content types are ones that arn't images. + # For now let's assume plain text and html are not valid images. + my $content_type = $headers->{'-type'} // $headers->{'-content_type'} // 'text/html'; + my $is_safe_content_type = $content_type =~ $safe_content_type_re; + + # Safe referers are ones that begin with the urlbase. + my $referer = $self->referer; + my $is_safe_referer = $referer && $referer =~ $safe_referer_re; + + if (!$is_safe_referer && !$is_safe_content_type) { + print $self->SUPER::header(-type => 'text/html', -status => '403 Forbidden'); + if ($content_type ne 'text/html') { + print "Untrusted Referer Header\n"; + if ($ENV{MOD_PERL}) { + my $r = $self->r; + $r->rflush; + $r->status(200); + } + } + exit; + } + } +} + # Override header so we can add the cookies in sub header { my $self = shift; @@ -302,6 +365,7 @@ sub header { else { %headers = @_; } + $self->_prevent_unsafe_response(\%headers); if ($self->{'_content_disp'}) { $headers{'-content_disposition'} = $self->{'_content_disp'}; @@ -356,6 +420,7 @@ sub header { Bugzilla::Hook::process('cgi_headers', { cgi => $self, headers => \%headers } ); + $self->{_header_done} = 1; return $self->SUPER::header(%headers) || ""; } diff --git a/Bugzilla/Chart.pm b/Bugzilla/Chart.pm index 3c69006aa..faf7a4785 100644 --- a/Bugzilla/Chart.pm +++ b/Bugzilla/Chart.pm @@ -420,11 +420,9 @@ sub dump { # Make sure we've read in our data my $data = $self->data; - + require Data::Dumper; - say "
Bugzilla::Chart object:";
-    print html_quote(Data::Dumper::Dumper($self));
-    print "
"; + return Data::Dumper::Dumper($self); } 1; diff --git a/Bugzilla/Component.pm b/Bugzilla/Component.pm index 9bc0a4493..d5a6ece5d 100644 --- a/Bugzilla/Component.pm +++ b/Bugzilla/Component.pm @@ -148,7 +148,8 @@ sub remove_from_db { $dbh->bz_start_transaction(); # Products must have at least one component. - if (scalar(@{$self->product->components}) == 1) { + my @components = @{ $self->product->components }; + if (scalar(@components) == 1) { ThrowUserError('component_is_last', { comp => $self }); } @@ -165,6 +166,8 @@ sub remove_from_db { ThrowUserError('component_has_bugs', {nb => $self->bug_count}); } } + # Update the list of components in the product object. + $self->product->{components} = [grep { $_->id != $self->id } @components]; $self->SUPER::remove_from_db(); $dbh->bz_commit_transaction(); diff --git a/Bugzilla/Config.pm b/Bugzilla/Config.pm index 1c02d9dda..458616701 100644 --- a/Bugzilla/Config.pm +++ b/Bugzilla/Config.pm @@ -16,10 +16,9 @@ use autodie qw(:default); use Bugzilla::Constants; use Bugzilla::Hook; -use Bugzilla::Util qw(trick_taint); +use Bugzilla::Util qw(trick_taint read_text write_text); use JSON::XS; -use File::Slurp; use File::Temp; use File::Basename; @@ -284,7 +283,7 @@ sub write_params { my $param_file = bz_locations()->{'datadir'} . '/params.json'; my $json_data = JSON::XS->new->canonical->pretty->encode($param_data); - write_file($param_file, { binmode => ':utf8', atomic => 1 }, \$json_data); + write_text($param_file, $json_data); # It's not common to edit parameters and loading # Bugzilla::Install::Filesystem is slow. @@ -301,8 +300,8 @@ sub read_param_file { my $file = bz_locations()->{'datadir'} . '/params.json'; if (-e $file) { - my $data; - read_file($file, binmode => ':utf8', buf_ref => \$data); + my $data = read_text($file); + trick_taint($data); # If params.json has been manually edited and e.g. some quotes are # missing, we don't want JSON::XS to leak the content of the file diff --git a/Bugzilla/Constants.pm b/Bugzilla/Constants.pm index cd4419b66..0e50f82fc 100644 --- a/Bugzilla/Constants.pm +++ b/Bugzilla/Constants.pm @@ -200,11 +200,11 @@ use Memoize; # CONSTANTS # # Bugzilla version -use constant BUGZILLA_VERSION => "5.0.2"; +use constant BUGZILLA_VERSION => "5.0.4.1"; # A base link to the current REST Documentation. We place it here # as it will need to be updated to whatever the current release is. -use constant REST_DOC => "http://www.bugzilla.org/docs/tip/en/html/api/"; +use constant REST_DOC => 'https://bugzilla.readthedocs.org/en/5.0/api/'; # Location of the remote and local XML files to track new releases. use constant REMOTE_FILE => 'http://updates.bugzilla.org/bugzilla-update.xml'; @@ -508,16 +508,42 @@ use constant INSTALLATION_MODE_NON_INTERACTIVE => 1; use constant DB_MODULE => { # MySQL 5.0.15 was the first production 5.0.x release. 'mysql' => {db => 'Bugzilla::DB::Mysql', db_version => '5.0.15', - dbd => { + db_blocklist => ['^[89]\.'], + # the following is a "human-readable" version to show in the + # release notes + db_blklst_str => '>= 8.0', + dbd => { package => 'DBD-mysql', module => 'DBD::mysql', # Disallow development versions - blacklist => ['_'], + blocklist => ['_'], # For UTF-8 support. 4.001 makes sure that blobs aren't # marked as UTF-8. version => '4.001', }, name => 'MySQL'}, + + # MariaDB is a drop-in replacement for MySQL and works with Bugzilla + 'mariadb' => {db => 'Bugzilla::DB::Mysql', db_version => '5.1', + # MariaDB is indistinguishable from MySQL, but skipped 8 and + # 9 so blocklist it anyway in case someone has the driver set + # to mariadb but actually has MySQL. + db_blocklist => ['^[89]\.'], + # no string to show the user on the release notes though. + dbd => { + package => 'DBD-mysql', + module => 'DBD::mysql', + + # Disallow development versions + blocklist => ['_'], + + # For UTF-8 support. 4.001 makes sure that blobs aren't + # marked as UTF-8. + version => '4.001', + }, + name => 'MariaDB' + }, + # Also see Bugzilla::DB::Pg::bz_check_server_version, which has special # code to require DBD::Pg 2.17.2 for PostgreSQL 9 and above. 'pg' => {db => 'Bugzilla::DB::Pg', db_version => '8.03.0000', diff --git a/Bugzilla/DB.pm b/Bugzilla/DB.pm index 5bc83f9d6..f59a32feb 100644 --- a/Bugzilla/DB.pm +++ b/Bugzilla/DB.pm @@ -204,17 +204,33 @@ sub bz_check_server_version { $self->disconnect; my $sql_want = $db->{db_version}; + my $sql_dontwant = exists $db->{db_blocklist} ? $db->{db_blocklist} : []; my $version_ok = vers_cmp($sql_vers, $sql_want) > -1 ? 1 : 0; + my $blocklisted; + if ($version_ok) { + $blocklisted = grep($sql_vers =~ /$_/, @$sql_dontwant); + $version_ok = 0 if $blocklisted; + } my $sql_server = $db->{name}; if ($output) { Bugzilla::Install::Requirements::_checking_for({ package => $sql_server, wanted => $sql_want, - found => $sql_vers, ok => $version_ok }); + found => $sql_vers, ok => $version_ok, + blocklisted => $blocklisted }); } # Check what version of the database server is installed and let # the user know if the version is too old to be used with Bugzilla. + if ($blocklisted) { + die <quote($default) if !($default =~ /^(-)?(\d+)(.\d+)?$/); + $default = $dbh->quote($default) if !($default =~ /^(-)?([0-9]+)(\.[0-9]+)?$/); $column->{DEFAULT} = $default; } } diff --git a/Bugzilla/DB/Sqlite.pm b/Bugzilla/DB/Sqlite.pm index ddafc1696..a56ed31ad 100644 --- a/Bugzilla/DB/Sqlite.pm +++ b/Bugzilla/DB/Sqlite.pm @@ -219,6 +219,7 @@ sub sql_date_format { my ($self, $date, $format) = @_; $format = "%Y.%m.%d %H:%M:%S" if !$format; $format =~ s/\%i/\%M/g; + $format =~ s/\%s/\%S/g; return "STRFTIME(" . $self->quote($format) . ", $date)"; } diff --git a/Bugzilla/Install/Filesystem.pm b/Bugzilla/Install/Filesystem.pm index 4f133d865..d30ae18dc 100644 --- a/Bugzilla/Install/Filesystem.pm +++ b/Bugzilla/Install/Filesystem.pm @@ -31,7 +31,6 @@ use File::Path; use File::Basename; use File::Copy qw(move); use File::Spec; -use File::Slurp; use IO::File; use POSIX (); @@ -536,7 +535,7 @@ sub update_filesystem { # Remove old assets htaccess file to force recreation with correct values. if (-e "$assetsdir/.htaccess") { - if (read_file("$assetsdir/.htaccess") =~ //) { + if (read_text("$assetsdir/.htaccess") =~ //) { unlink("$assetsdir/.htaccess"); } } @@ -782,22 +781,21 @@ sub _update_old_charts { # to product IDs. sub _update_old_mining_filenames { my ($miningdir) = @_; + my $dbh = Bugzilla->dbh; my @conversion_errors; - require Bugzilla::Product; - # We use a dummy product instance with ID 0, representing all products my $product_all = {id => 0, name => '-All-'}; - bless($product_all, 'Bugzilla::Product'); print "Updating old charting data file names..."; - my @products = Bugzilla::Product->get_all(); + my @products = @{ $dbh->selectall_arrayref('SELECT id, name FROM products + ORDER BY name', {Slice=>{}}) }; push(@products, $product_all); foreach my $product (@products) { - if (-e File::Spec->catfile($miningdir, $product->id)) { + if (-e File::Spec->catfile($miningdir, $product->{id})) { push(@conversion_errors, { product => $product, - message => 'A file named "' . $product->id . + message => 'A file named "' . $product->{id} . '" already exists.' }); } } @@ -805,8 +803,8 @@ sub _update_old_mining_filenames { if (! @conversion_errors) { # Renaming mining files should work now without a hitch. foreach my $product (@products) { - if (! rename(File::Spec->catfile($miningdir, $product->name), - File::Spec->catfile($miningdir, $product->id))) { + if (! rename(File::Spec->catfile($miningdir, $product->{name}), + File::Spec->catfile($miningdir, $product->{id}))) { push(@conversion_errors, { product => $product, message => $! }); @@ -822,7 +820,7 @@ sub _update_old_mining_filenames { print " FAILED:\n"; foreach my $error (@conversion_errors) { printf "Cannot rename charting data file for product %d (%s): %s\n", - $error->{product}->id, $error->{product}->name, + $error->{product}->{id}, $error->{product}->{name}, $error->{message}; } print "You need to empty the \"$miningdir\" directory, then run\n", diff --git a/Bugzilla/Install/Requirements.pm b/Bugzilla/Install/Requirements.pm index a688a0ffa..8d7ccfbc7 100644 --- a/Bugzilla/Install/Requirements.pm +++ b/Bugzilla/Install/Requirements.pm @@ -78,8 +78,8 @@ use constant APACHE_PATH => [qw( # installed or not. "version" is the version we need, or 0 if we'll accept # any version. # -# "blacklist" is an arrayref of regular expressions that describe versions that -# are 'blacklisted'--that is, even if the version is high enough, Bugzilla +# "blocklist" is an arrayref of regular expressions that describe versions that +# are 'blocklisted'--that is, even if the version is high enough, Bugzilla # will refuse to say that it's OK to run with that version. sub REQUIRED_MODULES { my @modules = ( @@ -121,10 +121,12 @@ sub REQUIRED_MODULES { version => ($^V >= v5.13.3) ? '1.614' : '1.54' }, # 2.24 contains several useful text virtual methods. + # 2.28-3.007 are broken, see https://bugzilla.mozilla.org/show_bug.cgi?id=1560873 { package => 'Template-Toolkit', module => 'Template', - version => '2.24' + version => '2.24', + blacklist => ['^2.2[89]$', '^3.00[0-7]$'] }, # 1.300011 has a debug mode for SMTP and automatically pass -i to sendmail. { @@ -138,6 +140,11 @@ sub REQUIRED_MODULES { # This fixes a memory leak in walk_parts that affected jobqueue.pl. version => '1.904' }, + { + package => 'Email-Address', + module => 'Email::Address', + version => 0, + }, { package => 'URI', module => 'URI', @@ -155,11 +162,6 @@ sub REQUIRED_MODULES { module => 'Math::Random::ISAAC', version => '1.0.1', }, - { - package => 'File-Slurp', - module => 'File::Slurp', - version => '9999.13', - }, { package => 'JSON-XS', module => 'JSON::XS', @@ -291,7 +293,7 @@ sub OPTIONAL_MODULES { # throwing warnings with Perl 5.12. version => '0.712', # SOAP::Transport::HTTP 1.12 is bogus. - blacklist => ['^1\.12$'], + blocklist => ['^1\.12$'], feature => ['xmlrpc'], }, # Since SOAP::Lite 1.0, XMLRPC::Lite is no longer included @@ -734,16 +736,16 @@ sub have_vers { # Must do a string comparison as $vnum may be of the form 5.10.1. my $vok = ($vnum ne '-1' && version->new($vnum) >= version->new($wanted)) ? 1 : 0; - my $blacklisted; - if ($vok && $params->{blacklist}) { - $blacklisted = grep($vnum =~ /$_/, @{$params->{blacklist}}); - $vok = 0 if $blacklisted; + my $blocklisted; + if ($vok && $params->{blocklist}) { + $blocklisted = grep($vnum =~ /$_/, @{$params->{blocklist}}); + $vok = 0 if $blocklisted; } if ($output) { _checking_for({ package => $package, ok => $vok, wanted => $wanted, - found => $vnum, blacklisted => $blacklisted + found => $vnum, blocklisted => $blocklisted }); } @@ -752,8 +754,8 @@ sub have_vers { sub _checking_for { my ($params) = @_; - my ($package, $ok, $wanted, $blacklisted, $found) = - @$params{qw(package ok wanted blacklisted found)}; + my ($package, $ok, $wanted, $blocklisted, $found) = + @$params{qw(package ok wanted blocklisted found)}; my $ok_string = $ok ? install_string('module_ok') : ''; @@ -780,10 +782,10 @@ sub _checking_for { $ok_string = install_string('module_not_found'); } - my $black_string = $blacklisted ? install_string('blacklisted') : ''; + my $block_string = $blocklisted ? install_string('blocklisted') : ''; my $want_string = $wanted ? "v$wanted" : install_string('any'); - my $str = sprintf "%s %20s %-11s $ok_string $black_string\n", + my $str = sprintf "%s %20s %-11s $ok_string $block_string\n", install_string('checking_for'), $package, "($want_string)"; print $ok ? $str : colored($str, COLOR_ERROR); } diff --git a/Bugzilla/JobQueue.pm b/Bugzilla/JobQueue.pm index d5ceda8e9..6ff85d84f 100644 --- a/Bugzilla/JobQueue.pm +++ b/Bugzilla/JobQueue.pm @@ -14,8 +14,8 @@ use warnings; use Bugzilla::Constants; use Bugzilla::Error; use Bugzilla::Install::Util qw(install_string); +use Bugzilla::Util qw(read_text); use File::Basename; -use File::Slurp; use base qw(TheSchwartz); use fields qw(_worker_pidfile); @@ -124,7 +124,7 @@ sub subprocess_worker { # And poll the PID to detect when the working has finished. # We do this instead of system() to allow for the INT signal to # interrup us and trigger kill_worker(). - my $pid = read_file($self->{_worker_pidfile}, err_mode => 'quiet'); + my $pid = read_text($self->{_worker_pidfile}, err_mode => 'quiet'); if ($pid) { sleep(3) while(kill(0, $pid)); } @@ -139,7 +139,7 @@ sub subprocess_worker { sub kill_worker { my $self = Bugzilla->job_queue(); if ($self->{_worker_pidfile} && -e $self->{_worker_pidfile}) { - my $worker_pid = read_file($self->{_worker_pidfile}); + my $worker_pid = read_text($self->{_worker_pidfile}); if ($worker_pid && kill(0, $worker_pid)) { $self->debug("Stopping worker process"); system "$0 -f -p '" . $self->{_worker_pidfile} . "' stop"; diff --git a/Bugzilla/MIME.pm b/Bugzilla/MIME.pm index 7b5843a78..d6046198f 100644 --- a/Bugzilla/MIME.pm +++ b/Bugzilla/MIME.pm @@ -13,11 +13,8 @@ use warnings; use parent qw(Email::MIME); -use Encode qw(encode); -use Encode::MIME::Header; - sub new { - my ($class, $msg) = @_; + my ($class, $msg, $args) = @_; state $use_utf8 = Bugzilla->params->{'utf8'}; # Template-Toolkit trims trailing newlines, which is problematic when @@ -57,7 +54,7 @@ sub new { # you're running on. See http://perldoc.perl.org/perlport.html#Newlines $msg =~ s/(?:\015+)?\012/\015\012/msg; - return $class->SUPER::new($msg); + return $class->SUPER::new($msg, $args); } sub as_string { @@ -79,20 +76,12 @@ sub as_string { # MIME-Version must be set otherwise some mailsystems ignore the charset $self->header_set('MIME-Version', '1.0') if !$self->header('MIME-Version'); - # Encode the headers correctly in quoted-printable + # Encode the headers correctly. foreach my $header ($self->header_names) { my @values = $self->header($header); - # We don't recode headers that happen multiple times. - next if scalar(@values) > 1; - if (my $value = $values[0]) { - utf8::decode($value) unless $use_utf8 && utf8::is_utf8($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); - $self->header_set($header, $encoded); - } + $self->header_str_set($header, @values); } # Ensure the character-set and encoding is set correctly on single part @@ -124,7 +113,7 @@ workarounds. =head1 SYNOPSIS use Bugzilla::MIME; - my $email = Bugzilla::MIME->new($message); + my $email = Bugzilla::MIME->new($message, $args); =head1 DESCRIPTION diff --git a/Bugzilla/Mailer.pm b/Bugzilla/Mailer.pm index 7ae81299f..d7c23f857 100644 --- a/Bugzilla/Mailer.pm +++ b/Bugzilla/Mailer.pm @@ -12,20 +12,85 @@ use strict; use warnings; use parent qw(Exporter); -@Bugzilla::Mailer::EXPORT = qw(MessageToMTA build_thread_marker); +@Bugzilla::Mailer::EXPORT = qw(MessageToMTA build_thread_marker generate_email); use Bugzilla::Constants; use Bugzilla::Error; use Bugzilla::Hook; use Bugzilla::MIME; use Bugzilla::Util; +use Bugzilla::User; +use Encode qw(); use Date::Format qw(time2str); use Email::Sender::Simple qw(sendmail); use Email::Sender::Transport::SMTP::Persistent; use Bugzilla::Sender::Transport::Sendmail; +sub generate_email { + my ($vars, $templates) = @_; + my ($lang, $email_format, $msg_text, $msg_html, $msg_header); + state $use_utf8 = Bugzilla->params->{'utf8'}; + + if ($vars->{to_user}) { + $lang = $vars->{to_user}->setting('lang'); + $email_format = $vars->{to_user}->setting('email_format'); + } else { + # If there are users in the CC list who don't have an account, + # use the default language for email notifications. + $lang = Bugzilla::User->new()->setting('lang'); + # However we cannot fall back to the default email_format, since + # it may be HTML, and many of the includes used in the HTML + # template require a valid user object. Instead we fall back to + # the plaintext template. + $email_format = 'text_only'; + } + + my $template = Bugzilla->template_inner($lang); + + $template->process($templates->{header}, $vars, \$msg_header) + || ThrowTemplateError($template->error()); + $template->process($templates->{text}, $vars, \$msg_text) + || ThrowTemplateError($template->error()); + + my @parts = ( + Bugzilla::MIME->create( + attributes => { + content_type => 'text/plain', + charset => $use_utf8 ? 'UTF-8' : 'iso-8859-1', + encoding => 'quoted-printable', + }, + body_str => $msg_text, + encode_check => Encode::FB_DEFAULT + ) + ); + if ($templates->{html} && $email_format eq 'html') { + $template->process($templates->{html}, $vars, \$msg_html) + || ThrowTemplateError($template->error()); + push @parts, Bugzilla::MIME->create( + attributes => { + content_type => 'text/html', + charset => $use_utf8 ? 'UTF-8' : 'iso-8859-1', + encoding => 'quoted-printable', + }, + body_str => $msg_html, + encode_check => Encode::FB_DEFAULT + ); + } + + my $email = Bugzilla::MIME->new($msg_header); + if (scalar(@parts) == 1) { + $email->content_type_set($parts[0]->content_type); + } else { + $email->content_type_set('multipart/alternative'); + # Some mail clients need same encoding for each part, even empty ones. + $email->charset_set('UTF-8') if $use_utf8; + } + $email->parts_set(\@parts); + return $email; +} + sub MessageToMTA { my ($msg, $send_now) = (@_); my $method = Bugzilla->params->{'mail_delivery_method'}; @@ -173,6 +238,10 @@ Bugzilla::Mailer - Provides methods for sending email =over +=item C + +Generates a multi-part email message, using the supplied list of templates. + =item C Sends the passed message to the mail transfer agent. diff --git a/Bugzilla/Migrate.pm b/Bugzilla/Migrate.pm index 0731d4fed..7865c842d 100644 --- a/Bugzilla/Migrate.pm +++ b/Bugzilla/Migrate.pm @@ -403,7 +403,7 @@ sub parse_date { } my $tz; if ($time[6]) { - $tz = Bugzilla->local_timezone->offset_as_string($time[6]); + $tz = DateTime::TimeZone->offset_as_string($time[6]); } else { $tz = $self->config('timezone'); diff --git a/Bugzilla/Product.pm b/Bugzilla/Product.pm index 30ebc7c6c..0c0cb458d 100644 --- a/Bugzilla/Product.pm +++ b/Bugzilla/Product.pm @@ -22,7 +22,6 @@ use Bugzilla::Milestone; use Bugzilla::Field; use Bugzilla::Status; use Bugzilla::Install::Requirements; -use Bugzilla::Mailer; use Bugzilla::Series; use Bugzilla::Hook; use Bugzilla::FlagType; diff --git a/Bugzilla/Search.pm b/Bugzilla/Search.pm index 0785a7e67..646f949f5 100644 --- a/Bugzilla/Search.pm +++ b/Bugzilla/Search.pm @@ -1628,7 +1628,8 @@ sub _special_parse_email { my $email = trim($params->{"email$id"}); next if !$email; my $type = $params->{"emailtype$id"} || 'anyexact'; - $type = "anyexact" if $type eq "exact"; + # for backward compatibility + $type = "equals" if $type eq "exact"; my $or_clause = new Bugzilla::Search::Clause('OR'); foreach my $field (qw(assigned_to reporter cc qa_contact)) { diff --git a/Bugzilla/Template.pm b/Bugzilla/Template.pm index ce027171b..decffe1e8 100644 --- a/Bugzilla/Template.pm +++ b/Bugzilla/Template.pm @@ -32,7 +32,6 @@ use Digest::MD5 qw(md5_hex); use File::Basename qw(basename dirname); use File::Find; use File::Path qw(rmtree mkpath); -use File::Slurp; use File::Spec; use IO::Dir; use List::MoreUtils qw(firstidx); @@ -232,7 +231,7 @@ sub quoteUrls { ~$1$2~igx; # attachment links - $text =~ s~\b(attachment$s*\#?$s*(\d+)(?:$s+\[details\])?) + $text =~ s~\b(attachment$s*\#?$s*([0-9]+)(?:$s+\[details\])?) ~($things[$count++] = get_attachment_link($2, $1, $user)) && ("\x{FDD2}" . ($count-1) . "\x{FDD3}") ~egmxi; @@ -245,9 +244,9 @@ sub quoteUrls { # Also, we can't use $bug_re?$comment_re? because that will match the # empty string my $bug_word = template_var('terms')->{bug}; - my $bug_re = qr/\Q$bug_word\E$s*\#?$s*(\d+)/i; + my $bug_re = qr/\Q$bug_word\E$s*\#?$s*([0-9]+)/i; my $comment_word = template_var('terms')->{comment}; - my $comment_re = qr/(?:\Q$comment_word\E|comment)$s*\#?$s*(\d+)/i; + my $comment_re = qr/(?:\Q$comment_word\E|comment)$s*\#?$s*([0-9]+)/i; $text =~ s~\b($bug_re(?:$s*,?$s*$comment_re)?|$comment_re) ~ # We have several choices. $1 here is the link, and $2-4 are set # depending on which part matched @@ -261,29 +260,29 @@ sub quoteUrls { my $bugs_word = template_var('terms')->{bugs}; my $bugs_re = qr/\Q$bugs_word\E$s*\#?$s* - \d+(?:$s*,$s*\#?$s*\d+)+/ix; + [0-9]+(?:$s*,$s*\#?$s*[0-9]+)+/ix; $text =~ s{($bugs_re)}{ my $match = $1; - $match =~ s/((?:#$s*)?(\d+))/get_bug_link($2, $1);/eg; + $match =~ s/((?:#$s*)?([0-9]+))/get_bug_link($2, $1);/eg; $match; }eg; my $comments_word = template_var('terms')->{comments}; my $comments_re = qr/(?:comments|\Q$comments_word\E)$s*\#?$s* - \d+(?:$s*,$s*\#?$s*\d+)+/ix; + [0-9]+(?:$s*,$s*\#?$s*[0-9]+)+/ix; $text =~ s{($comments_re)}{ my $match = $1; - $match =~ s|((?:#$s*)?(\d+))|$1|g; + $match =~ s|((?:#$s*)?([0-9]+))|$1|g; $match; }eg; # Old duplicate markers. These don't use $bug_word because they are old # and were never customizable. $text =~ s~(?<=^\*\*\*\ This\ bug\ has\ been\ marked\ as\ a\ duplicate\ of\ ) - (\d+) + ([0-9]+) (?=\ \*\*\*\Z) ~get_bug_link($1, $1, { user => $user }) ~egmx; @@ -502,7 +501,7 @@ sub _concatenate_css { next unless -e "$cgi_path/$files{$source}"; my $file = $skins_path . '/' . md5_hex($source) . '.css'; if (!-e $file) { - my $content = read_file("$cgi_path/$files{$source}"); + my $content = read_text("$cgi_path/$files{$source}"); # minify $content =~ s{/\*.*?\*/}{}sg; # comments @@ -512,7 +511,7 @@ sub _concatenate_css { # rewrite urls $content =~ s{url\(([^\)]+)\)}{_css_url_rewrite($source, $1)}eig; - write_file($file, "/* $files{$source} */\n" . $content . "\n"); + write_text($file, "/* $files{$source} */\n" . $content . "\n"); } push @minified, $file; } @@ -522,9 +521,9 @@ sub _concatenate_css { if (!-e $file) { my $content = ''; foreach my $source (@minified) { - $content .= read_file($source); + $content .= read_text($source); } - write_file($file, $content); + write_text($file, $content); } $file =~ s/^\Q$cgi_path\E\///o; @@ -563,7 +562,7 @@ sub _concatenate_js { next unless -e "$cgi_path/$files{$source}"; my $file = $skins_path . '/' . md5_hex($source) . '.js'; if (!-e $file) { - my $content = read_file("$cgi_path/$files{$source}"); + my $content = read_text("$cgi_path/$files{$source}"); # minimal minification $content =~ s#/\*.*?\*/##sg; # block comments @@ -572,7 +571,7 @@ sub _concatenate_js { $content =~ s#\n{2,}#\n#g; # blank lines $content =~ s#(^\s+|\s+$)##g; # whitespace at the start/end of file - write_file($file, ";/* $files{$source} */\n" . $content . "\n"); + write_text($file, ";/* $files{$source} */\n" . $content . "\n"); } push @minified, $file; } @@ -582,9 +581,9 @@ sub _concatenate_js { if (!-e $file) { my $content = ''; foreach my $source (@minified) { - $content .= read_file($source); + $content .= read_text($source); } - write_file($file, $content); + write_text($file, $content); } $file =~ s/^\Q$cgi_path\E\///o; @@ -865,12 +864,13 @@ sub create { }, # In CSV, quotes are doubled, and any value containing a quote or a - # comma is enclosed in quotes. If a field starts with an equals - # sign, it is proceed by a space. + # comma is enclosed in quotes. + # If a field starts with either "=", "+", "-" or "@", it is preceded + # by a space to prevent stupid formula execution from Excel & co. csv => sub { my ($var) = @_; - $var = ' ' . $var if substr($var, 0, 1) eq '='; + $var = ' ' . $var if $var =~ /^[+=@-]/; # backslash is not special to CSV, but it can be used to confuse some browsers... # so we do not allow it to happen. We only do this for logged-in users. $var =~ s/\\/\x{FF3C}/g if Bugzilla->user->id; diff --git a/Bugzilla/Update.pm b/Bugzilla/Update.pm index 72a7108a8..1c092ebe4 100644 --- a/Bugzilla/Update.pm +++ b/Bugzilla/Update.pm @@ -49,14 +49,15 @@ sub get_notifications { 'latest_ver' => $branch->{'att'}->{'vid'}, 'status' => $branch->{'att'}->{'status'}, 'url' => $branch->{'att'}->{'url'}, - 'date' => $branch->{'att'}->{'date'} + 'date' => $branch->{'att'}->{'date'}, + 'eos_date' => exists($branch->{'att'}->{'eos-date'}) ? $branch->{'att'}->{'eos-date'} : undef, }; push(@releases, $release); } # On which branch is the current installation running? my @current_version = - (BUGZILLA_VERSION =~ m/^(\d+)\.(\d+)(?:(rc|\.)(\d+))?\+?$/); + (BUGZILLA_VERSION =~ m/^(\d+)\.(\d+)(?:\.(\d+))?(?:(rc|\.)(\d+))?\+?$/); my @release; if (Bugzilla->params->{'upgrade_notification'} eq 'development_snapshot') { @@ -68,17 +69,60 @@ sub get_notifications { } } elsif (Bugzilla->params->{'upgrade_notification'} eq 'latest_stable_release') { + # We want the latest stable version for the current branch. + # If we are running a development snapshot, we won't match anything. + # This is the 5.0.4 branch and it won't branch again so just hardcode this. + my $branch_version = '5.0.4'; + + # We do a string comparison instead of a numerical one, because + # e.g. 2.2 == 2.20, but 2.2 ne 2.20 (and 2.2 is indeed much older). + @release = grep {$_->{'branch_ver'} eq $branch_version} @releases; + + # If the branch has an end-of-support date listed, we should + # strongly suggest to upgrade to the latest stable release + # available. + if (scalar(@release) && $release[0]->{'status'} ne 'closed' + && defined($release[0]->{'eos_date'})) { + my $eos_date = $release[0]->{'eos_date'}; + @release = grep {$_->{'status'} eq 'stable'} @releases; + return {'data' => $release[0], + 'branch_version' => $branch_version, + 'eos_date' => $eos_date}; + }; + + # If the branch is now closed, we should strongly suggest + # to upgrade to the latest stable release available. + if (scalar(@release) && $release[0]->{'status'} eq 'closed') { + @release = grep {$_->{'status'} eq 'stable'} @releases; + return {'data' => $release[0], 'deprecated' => $branch_version}; + } + + # If we get here, then we want to recommend the lastest stable + # release without any other messages. @release = grep {$_->{'status'} eq 'stable'} @releases; } elsif (Bugzilla->params->{'upgrade_notification'} eq 'stable_branch_release') { # We want the latest stable version for the current branch. # If we are running a development snapshot, we won't match anything. - my $branch_version = $current_version[0] . '.' . $current_version[1]; + # This is the 5.0.4 branch and it won't branch again so just hardcode this. + my $branch_version = '5.0.4'; # We do a string comparison instead of a numerical one, because # e.g. 2.2 == 2.20, but 2.2 ne 2.20 (and 2.2 is indeed much older). @release = grep {$_->{'branch_ver'} eq $branch_version} @releases; + # If the branch has an end-of-support date listed, we should + # strongly suggest to upgrade to the latest stable release + # available. + if (scalar(@release) && $release[0]->{'status'} ne 'closed' + && defined($release[0]->{'eos_date'})) { + my $eos_date = $release[0]->{'eos_date'}; + @release = grep {$_->{'status'} eq 'stable'} @releases; + return {'data' => $release[0], + 'branch_version' => $branch_version, + 'eos_date' => $eos_date}; + }; + # If the branch is now closed, we should strongly suggest # to upgrade to the latest stable release available. if (scalar(@release) && $release[0]->{'status'} eq 'closed') { @@ -97,12 +141,12 @@ sub get_notifications { # Only notify the administrator if the latest version available # is newer than the current one. my @new_version = - ($release[0]->{'latest_ver'} =~ m/^(\d+)\.(\d+)(?:(rc|\.)(\d+))?\+?$/); + ($release[0]->{'latest_ver'} =~ m/^(\d+)\.(\d+)(?:\.(\d+))?(?:(rc|\.)(\d+))?\+?$/); # We convert release candidates 'rc' to integers (rc ? 0 : 1) in order # to compare versions easily. - $current_version[2] = ($current_version[2] && $current_version[2] eq 'rc') ? 0 : 1; - $new_version[2] = ($new_version[2] && $new_version[2] eq 'rc') ? 0 : 1; + @current_version = map { s/^(?:rc|)$/0/; s/^\.$/1/; $_; } @current_version; + @new_version = map { s/^(?:rc|)$/0/; s/^\.$/1/; $_; } @new_version; my $is_newer = _compare_versions(\@current_version, \@new_version); return ($is_newer == 1) ? {'data' => $release[0]} : undef; diff --git a/Bugzilla/User.pm b/Bugzilla/User.pm index 77e6cebb0..e8f4aa77f 100644 --- a/Bugzilla/User.pm +++ b/Bugzilla/User.pm @@ -132,7 +132,19 @@ sub new { $_[0] = $param; } } - return $class->SUPER::new(@_); + + $user = $class->SUPER::new(@_); + + # MySQL considers some non-ascii characters such as umlauts to equal + # ascii characters returning a user when it should not. + if ($user && ref $param eq 'HASH' && exists $param->{name}) { + my $login = $param->{name}; + if (lc $login ne lc $user->login) { + $user = undef; + } + } + + return $user; } sub super_user { diff --git a/Bugzilla/Util.pm b/Bugzilla/Util.pm index 037b38648..5ceef4ef7 100644 --- a/Bugzilla/Util.pm +++ b/Bugzilla/Util.pm @@ -24,7 +24,7 @@ use parent qw(Exporter); validate_email_syntax check_email_syntax clean_text get_text template_var display_value disable_utf8 detect_encoding email_filter - join_activity_entries); + join_activity_entries read_text write_text); use Bugzilla::Constants; use Bugzilla::RNG qw(irand); @@ -39,6 +39,8 @@ use Scalar::Util qw(tainted blessed); use Text::Wrap; use Encode qw(encode decode resolve_alias); use Encode::Guess; +use File::Basename qw(dirname); +use File::Temp qw(tempfile); sub trick_taint { require Carp; @@ -49,13 +51,13 @@ sub trick_taint { } sub detaint_natural { - my $match = $_[0] =~ /^(\d+)$/; + my $match = $_[0] =~ /^([0-9]+)$/; $_[0] = $match ? int($1) : undef; return (defined($_[0])); } sub detaint_signed { - my $match = $_[0] =~ /^([-+]?\d+)$/; + my $match = $_[0] =~ /^([-+]?[0-9]+)$/; # The "int()" call removes any leading plus sign. $_[0] = $match ? int($1) : undef; return (defined($_[0])); @@ -106,6 +108,29 @@ sub html_quote { return $var; } +sub read_text { + my ($filename) = @_; + open my $fh, '<:encoding(utf-8)', $filename; + local $/ = undef; + my $content = <$fh>; + close $fh; + return $content; +} + +sub write_text { + my ($filename, $content) = @_; + my ($tmp_fh, $tmp_filename) = tempfile('.tmp.XXXXXXXXXX', + DIR => dirname($filename), + UNLINK => 0, + ); + binmode $tmp_fh, ':encoding(utf-8)'; + print $tmp_fh $content; + close $tmp_fh; + # File::Temp tries for secure files, but File::Slurp used the umask. + chmod(0666 & ~umask, $tmp_filename); + rename $tmp_filename, $filename; +} + sub html_light_quote { my ($text) = @_; # admin/table.html.tmpl calls |FILTER html_light| many times. @@ -588,7 +613,7 @@ sub datetime_from { second => defined($time[0]) ? int($time[0]) : undef, # If a timezone was specified, use it. Otherwise, use the # local timezone. - time_zone => Bugzilla->local_timezone->offset_as_string($time[6]) + time_zone => DateTime::TimeZone->offset_as_string($time[6]) || Bugzilla->local_timezone, ); @@ -1238,18 +1263,44 @@ if Bugzilla is currently using the shadowdb or not. Used like: =back -=head1 B - =over -=item do_ssl_redirect_if_required +=item C + +Writes $content to $filename. The content will be encoded as UTF-8. Returns 1 if +the atomic write was successful, 0 otherwise. C<$!> will be set to the error +from C. + +=item C + +Reads the contents of $filename and returns it as a string. The string will be +decoded as UTF-8. + +=item C + +Returns true if the given IP address is an IPv4 address. + +=item C + +Returns true if the given IP address is an IPv6 address. + +=item C + +If Bugzilla is configured to redirect all HTTP requests to HTTPS, this function +will redirect the user to the HTTPS version of the current page. It will not do +anything if the user is already on HTTPS, or if there is no C parameter +set. -=item validate_time +=item C -=item is_ipv4 +Validates a time string. Returns true or false depending on whether the time +string is valid. -=item is_ipv6 +=item C -=item display_value +Returns the display value for a given field and value. This value comes from the +value_descs template variable. The value_descs variable is set in the template +file C. This is used for localizing Bugzilla to +other languages. =back diff --git a/Bugzilla/WebService/Bug.pm b/Bugzilla/WebService/Bug.pm index c99651201..b07d3cb01 100644 --- a/Bugzilla/WebService/Bug.pm +++ b/Bugzilla/WebService/Bug.pm @@ -1133,6 +1133,10 @@ sub update_comment_tags { { function => 'Bug.update_comment_tags', param => 'comment_id' }); + ThrowCodeError('param_integer_required', { function => 'Bug.update_comment_tags', + param => 'comment_id' }) + unless $comment_id =~ /^[0-9]+$/; + my $comment = Bugzilla::Comment->new($comment_id) || return []; $comment->bug->check_is_visible(); diff --git a/Bugzilla/WebService/Constants.pm b/Bugzilla/WebService/Constants.pm index 0bdd3517e..557a996f8 100644 --- a/Bugzilla/WebService/Constants.pm +++ b/Bugzilla/WebService/Constants.pm @@ -67,6 +67,8 @@ use constant WS_ERROR_CODE => { number_too_large => 54, number_too_small => 55, illegal_date => 56, + param_integer_required => 57, + param_scalar_array_required => 58, # Bug errors usually occupy the 100-200 range. improper_bug_id_field_value => 100, bug_id_does_not_exist => 101, diff --git a/Bugzilla/WebService/Util.pm b/Bugzilla/WebService/Util.pm index 26a6ebbb0..a879c0e0d 100644 --- a/Bugzilla/WebService/Util.pm +++ b/Bugzilla/WebService/Util.pm @@ -16,6 +16,7 @@ use Bugzilla::FlagType; use Bugzilla::Error; use Storable qw(dclone); +use List::MoreUtils qw(any none); use parent qw(Exporter); @@ -220,14 +221,19 @@ sub validate { # $params should be. return ($self, undef) if (defined $params and !ref $params); + my @id_params = qw(ids comment_ids); # If @keys is not empty then we convert any named # parameters that have scalar values to arrayrefs # that match. foreach my $key (@keys) { if (exists $params->{$key}) { - $params->{$key} = ref $params->{$key} - ? $params->{$key} - : [ $params->{$key} ]; + $params->{$key} = [ $params->{$key} ] unless ref $params->{$key}; + + if (any { $key eq $_ } @id_params) { + my $ids = $params->{$key}; + ThrowCodeError('param_scalar_array_required', { param => $key }) + unless ref($ids) eq 'ARRAY' && none { ref $_ } @$ids; + } } } diff --git a/Build.PL b/Build.PL index 024a56024..0a7c1059a 100644 --- a/Build.PL +++ b/Build.PL @@ -33,10 +33,10 @@ sub build_requires { sub recommends { my $recommends = OPTIONAL_MODULES(); - my @blacklist = ('Apache-SizeLimit', 'mod_perl'); # Does not compile properly on Travis + my @blocklist = ('Apache-SizeLimit', 'mod_perl'); # Does not compile properly on Travis my $hrecommends = {}; foreach my $module (@$recommends) { - next if grep($_ eq $module->{package}, @blacklist); + next if grep($_ eq $module->{package}, @blocklist); $hrecommends->{$module->{module}} = $module->{version}; } return $hrecommends; diff --git a/README b/README index e68afd8e4..335631e5d 100644 --- a/README +++ b/README @@ -6,9 +6,6 @@ developed by an active group of volunteers in the Mozilla community, and used by thousands of projects and companies around the world. It can be installed on Linux and other flavors of Unix, Windows or Mac OS X. -You can try Bugzilla out using our testing installation: -https://landfill.bugzilla.org/bugzilla-tip/ - Documentation ============= diff --git a/attachment.cgi b/attachment.cgi index 5db8f5909..4cd9229fb 100755 --- a/attachment.cgi +++ b/attachment.cgi @@ -26,6 +26,7 @@ use Bugzilla::Attachment::PatchReader; use Bugzilla::Token; use Encode qw(encode find_encoding); +use Encode::MIME::Header; # Required to alter Encode::Encoding{'MIME-Q'}. # For most scripts we don't make $cgi and $template global variables. But # when preparing Bugzilla for mod_perl, this script used these @@ -34,6 +35,7 @@ use Encode qw(encode find_encoding); local our $cgi = Bugzilla->cgi; local our $template = Bugzilla->template; local our $vars = {}; +local $Bugzilla::CGI::ALLOW_UNSAFE_RESPONSE = 1; # All calls to this script should contain an "action" variable whose # value determines what the user wants to do. The code below checks diff --git a/chart.cgi b/chart.cgi index 00b0b8ee5..18ab87e5e 100755 --- a/chart.cgi +++ b/chart.cgi @@ -96,6 +96,13 @@ $user->in_group(Bugzilla->params->{"chartgroup"}) # Only admins may create public queries $user->in_group('admin') || $cgi->delete('public'); +if ($cgi->param('debug') + && Bugzilla->params->{debug_group} + && Bugzilla->user->in_group(Bugzilla->params->{debug_group}) + ) { + $vars->{'debug'} = 1; +} + # All these actions relate to chart construction. if ($action =~ /^(assemble|add|remove|sum|subscribe|unsubscribe)$/) { # These two need to be done before the creation of the Chart object, so @@ -305,11 +312,19 @@ sub plot { $vars->{'chart'} = new Bugzilla::Chart($cgi); my $format = $template->get_format("reports/chart", "", scalar($cgi->param('ctype'))); + $format->{'ctype'} = 'text/html' if $cgi->param('debug'); + + $cgi->set_dated_content_disp('inline', 'chart', $format->{extension}); + print $cgi->header($format->{'ctype'}); + disable_utf8() if ($format->{'ctype'} =~ /^image\//); # Debugging PNGs is a pain; we need to be able to see the error messages - if ($cgi->param('debug')) { - print $cgi->header(); - $vars->{'chart'}->dump(); + if (exists $vars->{'debug'}) { + # Bug 1439260 - if we're using debug mode, always use the HTML template + # which has proper filters in it. Debug forces an HTML content type + # anyway, and can cause XSS if we're not filtering the output. + $format = $template->get_format("reports/chart", "", "html"); + $vars->{'debug_dump'} = $vars->{'chart'}->dump(); } print $cgi->header($format->{'ctype'}); @@ -351,7 +366,9 @@ sub view { # If we have having problems with bad data, we can set debug=1 to dump # the data structure. - $chart->dump() if $cgi->param('debug'); + if (exists $vars->{'debug'}) { + $vars->{'debug_dump'} = $chart->dump(); + } $template->process("reports/create-chart.html.tmpl", $vars) || ThrowTemplateError($template->error()); diff --git a/contrib/bugzilla-submit/README b/contrib/bugzilla-submit/README index f9e74b9d4..9d9811bde 100644 --- a/contrib/bugzilla-submit/README +++ b/contrib/bugzilla-submit/README @@ -19,9 +19,8 @@ Its only requirement is Python 2.3 or higher; you should have the Usage Notes ----------- -* Please constrain testing to your own installation of Bugzilla, or use -* http://landfill.bugzilla.org/ for testing purposes -- opening test -* bugs on production instances of Bugzilla is definitely not a good idea +* Please constrain testing to your own installation of Bugzilla, opening test +* bugs on production instances of Bugzilla is definitely not a good idea. Run "bugzilla-submit --help" for a description of the possible options. diff --git a/contrib/bugzilla-submit/bugzilla-submit.xml b/contrib/bugzilla-submit/bugzilla-submit.xml index 2cfdc9209..297e690cc 100755 --- a/contrib/bugzilla-submit/bugzilla-submit.xml +++ b/contrib/bugzilla-submit/bugzilla-submit.xml @@ -201,12 +201,12 @@ password field is the right password. The URL in the machine field must be enclosed in double quotes. For example, if your Bugzilla instance is at -"http://landfill.bugzilla.org/bztest/", and your login and password +"https://bugzilla.example.org/bztest/", and your login and password there are "john@doe.com" and "foo", respectively, your .netrc entry should look something like: - machine "http://landfill.bugzilla.org/bztest/" + machine "https://bugzilla.example.org/bztest/" login john@doe.com password foo diff --git a/contrib/jb2bz.py b/contrib/jb2bz.py index 85f95423a..170e82d70 100755 --- a/contrib/jb2bz.py +++ b/contrib/jb2bz.py @@ -17,8 +17,8 @@ This code requires a recent version of Andy Dustman's MySQLdb interface, Share and enjoy. """ -import rfc822, mimetools, multifile, mimetypes, email.utils -import sys, re, glob, StringIO, os, stat, time +import email, mimetypes, email.utils +import sys, re, glob, os, stat, time import MySQLdb, getopt # mimetypes doesn't include everything we might encounter, yet. @@ -30,7 +30,7 @@ if not mimetypes.encodings_map.has_key('.bz2'): bug_status='CONFIRMED' component="default" -version="" +version="unspecified" product="" # this is required, the rest of these are defaulted as above """ @@ -89,10 +89,24 @@ def process_notes_file(current, fname): def process_reply_file(current, fname): new_note = {} reply = open(fname, "r") - msg = rfc822.Message(reply) - new_note['text'] = "%s\n%s" % (msg['From'], msg.fp.read()) - new_note['timestamp'] = email.utils.parsedate_tz(msg['Date']) - current["notes"].append(new_note) + msg = email.message_from_file(reply) + + # Add any attachments that may have been in a followup or reply + msgtype = msg.get_content_maintype() + if msgtype == "multipart": + for part in msg.walk(): + new_note = {} + if part.get_filename() is None: + if part.get_content_type() == "text/plain": + new_note['timestamp'] = time.gmtime(email.utils.mktime_tz(email.utils.parsedate_tz(msg['Date']))) + new_note['text'] = "%s\n%s" % (msg['From'], part.get_payload()) + current["notes"].append(new_note) + else: + maybe_add_attachment(part, current) + else: + new_note['text'] = "%s\n%s" % (msg['From'], msg.get_payload()) + new_note['timestamp'] = time.gmtime(email.utils.mktime_tz(email.utils.parsedate_tz(msg['Date']))) + current["notes"].append(new_note) def add_notes(current): """Add any notes that have been recorded for the current bug.""" @@ -104,51 +118,48 @@ def add_notes(current): for f in glob.glob("%d.followup.*" % current['number']): process_reply_file(current, f) -def maybe_add_attachment(current, file, submsg): +def maybe_add_attachment(submsg, current): """Adds the attachment to the current record""" - cd = submsg["Content-Disposition"] - m = re.search(r'filename="([^"]+)"', cd) - if m == None: + attachment_filename = submsg.get_filename() + if attachment_filename is None: return - attachment_filename = m.group(1) - if (submsg.gettype() == 'application/octet-stream'): + + if (submsg.get_content_type() == 'application/octet-stream'): # try get a more specific content-type for this attachment - type, encoding = mimetypes.guess_type(m.group(1)) - if type == None: - type = submsg.gettype() + mtype, encoding = mimetypes.guess_type(attachment_filename) + if mtype == None: + mtype = submsg.get_content_type() else: - type = submsg.gettype() + mtype = submsg.get_content_type() - try: - data = StringIO.StringIO() - mimetools.decode(file, data, submsg.getencoding()) - except: + if mtype == 'application/x-pkcs7-signature': + return + + if mtype == 'application/pkcs7-signature': + return + + if mtype == 'application/pgp-signature': return - current['attachments'].append( ( attachment_filename, type, data.getvalue() ) ) + if mtype == 'message/rfc822': + return -def process_mime_body(current, file, submsg): - data = StringIO.StringIO() try: - mimetools.decode(file, data, submsg.getencoding()) - current['description'] = data.getvalue() + data = submsg.get_payload(decode=True) except: return + current['attachments'].append( ( attachment_filename, mtype, data ) ) + def process_text_plain(msg, current): - current['description'] = msg.fp.read() - -def process_multi_part(file, msg, current): - mf = multifile.MultiFile(file) - mf.push(msg.getparam("boundary")) - while mf.next(): - submsg = mimetools.Message(file) - if submsg.has_key("Content-Disposition"): - maybe_add_attachment(current, mf, submsg) + current['description'] = msg.get_payload() + +def process_multi_part(msg, current): + for part in msg.walk(): + if part.get_filename() is None: + process_text_plain(part, current) else: - # This is the message body itself (always?), so process - # accordingly - process_mime_body(current, mf, submsg) + maybe_add_attachment(part, current) def process_jitterbug(filename): current = {} @@ -158,39 +169,37 @@ def process_jitterbug(filename): current['description'] = '' current['date-reported'] = () current['short-description'] = '' - - print "Processing: %d" % current['number'] - file = open(filename, "r") - create_date = os.fstat(file.fileno()) - msg = mimetools.Message(file) + print "Processing: %d" % current['number'] - msgtype = msg.gettype() + mfile = open(filename, "r") + create_date = os.fstat(mfile.fileno()) + msg = email.message_from_file(mfile) - add_notes(current) - current['date-reported'] = email.utils.parsedate_tz(msg['Date']) + current['date-reported'] = time.gmtime(email.utils.mktime_tz(email.utils.parsedate_tz(msg['Date']))) if current['date-reported'] is None: current['date-reported'] = time.gmtime(create_date[stat.ST_MTIME]) if current['date-reported'][0] < 1900: current['date-reported'] = time.gmtime(create_date[stat.ST_MTIME]) - if msg.getparam('Subject') is not None: + if msg.has_key('Subject') is not False: current['short-description'] = msg['Subject'] else: current['short-description'] = "Unknown" - if msgtype[:5] == 'text/': + msgtype = msg.get_content_maintype() + if msgtype == 'text': process_text_plain(msg, current) - elif msgtype[:5] == 'text': - process_text_plain(msg, current) - elif msgtype[:10] == "multipart/": - process_multi_part(file, msg, current) + elif msgtype == "multipart": + process_multi_part(msg, current) else: # Huh? This should never happen. print "Unknown content-type: %s" % msgtype sys.exit(1) + add_notes(current) + # At this point we have processed the message: we have all of the notes and # attachments stored, so it's time to add things to the database. # The schema for JitterBug 2.14 can be found at: @@ -219,7 +228,9 @@ def process_jitterbug(filename): try: cursor.execute( "INSERT INTO bugs SET " \ "bug_id=%s," \ + "priority='---'," \ "bug_severity='normal'," \ + "op_sys='All'," \ "bug_status=%s," \ "creation_ts=%s," \ "delta_ts=%s," \ @@ -242,7 +253,7 @@ def process_jitterbug(filename): version, component, resolution] ) - + # This is the initial long description associated with the bug report cursor.execute( "INSERT INTO longdescs SET " \ "bug_id=%s," \ @@ -253,7 +264,7 @@ def process_jitterbug(filename): reporter, time.strftime("%Y-%m-%d %H:%M:%S", current['date-reported'][:9]), current['description'] ] ) - + # Add whatever notes are associated with this defect for n in current['notes']: cursor.execute( "INSERT INTO longdescs SET " \ @@ -265,15 +276,15 @@ def process_jitterbug(filename): reporter, time.strftime("%Y-%m-%d %H:%M:%S", n['timestamp'][:9]), n['text']]) - + # add attachments associated with this defect for a in current['attachments']: cursor.execute( "INSERT INTO attachments SET " \ - "bug_id=%s, creation_ts=%s, description='', mimetype=%s," \ + "bug_id=%s, creation_ts=%s, description=%s, mimetype=%s," \ "filename=%s, submitter_id=%s", [ current['number'], time.strftime("%Y-%m-%d %H:%M:%S", current['date-reported'][:9]), - a[1], a[0], reporter ]) + a[0], a[1], a[0], reporter ]) cursor.execute( "INSERT INTO attach_data SET " \ "id=LAST_INSERT_ID(), thedata=%s", [ a[2] ]) diff --git a/editclassifications.cgi b/editclassifications.cgi index ea4b139da..640b8b8cd 100755 --- a/editclassifications.cgi +++ b/editclassifications.cgi @@ -27,7 +27,6 @@ local our $vars = {}; sub LoadTemplate { my $action = shift; - my $cgi = Bugzilla->cgi; my $template = Bugzilla->template; $vars->{'classifications'} = [Bugzilla::Classification->get_all] @@ -38,7 +37,6 @@ sub LoadTemplate { $action =~ /(\w+)/; $action = $1; - print $cgi->header(); $template->process("admin/classifications/$action.html.tmpl", $vars) || ThrowTemplateError($template->error()); exit; diff --git a/editflagtypes.cgi b/editflagtypes.cgi index d848d250a..71f7cb655 100755 --- a/editflagtypes.cgi +++ b/editflagtypes.cgi @@ -436,17 +436,30 @@ sub get_products_and_components { my @products; if ($user->in_group('editcomponents')) { - @products = Bugzilla::Product->get_all; + if (Bugzilla->params->{useclassification}) { + # We want products grouped by classifications. + @products = map { @{ $_->products } } Bugzilla::Classification->get_all; + } + else { + @products = Bugzilla::Product->get_all; + } } else { @products = @{$user->get_products_by_permission('editcomponents')}; + + if (Bugzilla->params->{useclassification}) { + my %class; + push(@{$class{$_->classification_id}}, $_) foreach @products; + + # Let's sort the list by classifications. + @products = (); + push(@products, @{$class{$_->id} || []}) foreach Bugzilla::Classification->get_all; + } } - # We require all unique component names. + my %components; foreach my $product (@products) { - foreach my $component (@{$product->components}) { - $components{$component->name} = 1; - } + $components{$_->name} = 1 foreach @{$product->components}; } $vars->{'products'} = \@products; $vars->{'components'} = [sort(keys %components)]; diff --git a/editgroups.cgi b/editgroups.cgi index 35989b954..f2c915556 100755 --- a/editgroups.cgi +++ b/editgroups.cgi @@ -135,8 +135,7 @@ sub get_current_and_available { unless ($action) { my @groups = Bugzilla::Group->get_all; $vars->{'groups'} = \@groups; - - print $cgi->header(); + $template->process("admin/groups/list.html.tmpl", $vars) || ThrowTemplateError($template->error()); exit; @@ -155,12 +154,10 @@ if ($action eq 'changeform') { get_current_and_available($group, $vars); $vars->{'group'} = $group; - $vars->{'token'} = issue_session_token('edit_group'); + $vars->{'token'} = issue_session_token('edit_group'); - print $cgi->header(); $template->process("admin/groups/edit.html.tmpl", $vars) || ThrowTemplateError($template->error()); - exit; } @@ -172,10 +169,9 @@ if ($action eq 'changeform') { if ($action eq 'add') { $vars->{'token'} = issue_session_token('add_group'); - print $cgi->header(); + $template->process("admin/groups/create.html.tmpl", $vars) || ThrowTemplateError($template->error()); - exit; } @@ -204,7 +200,6 @@ if ($action eq 'new') { get_current_and_available($group, $vars); $vars->{'token'} = issue_session_token('edit_group'); - print $cgi->header(); $template->process("admin/groups/edit.html.tmpl", $vars) || ThrowTemplateError($template->error()); exit; @@ -228,10 +223,8 @@ if ($action eq 'del') { $vars->{'group'} = $group; $vars->{'token'} = issue_session_token('delete_group'); - print $cgi->header(); $template->process("admin/groups/delete.html.tmpl", $vars) || ThrowTemplateError($template->error()); - exit; } @@ -255,7 +248,6 @@ if ($action eq 'delete') { $vars->{'message'} = 'group_deleted'; $vars->{'groups'} = [Bugzilla::Group->get_all]; - print $cgi->header(); $template->process("admin/groups/list.html.tmpl", $vars) || ThrowTemplateError($template->error()); exit; @@ -277,7 +269,6 @@ if ($action eq 'postchanges') { $vars->{'changes'} = $changes; $vars->{'token'} = issue_session_token('edit_group'); - print $cgi->header(); $template->process("admin/groups/edit.html.tmpl", $vars) || ThrowTemplateError($template->error()); exit; @@ -288,6 +279,7 @@ if ($action eq 'confirm_remove') { $vars->{'group'} = $group; $vars->{'regexp'} = CheckGroupRegexp($cgi->param('regexp')); $vars->{'token'} = issue_session_token('remove_group_members'); + $template->process('admin/groups/confirm-remove.html.tmpl', $vars) || ThrowTemplateError($template->error()); exit; @@ -326,10 +318,8 @@ if ($action eq 'remove_regexp') { $vars->{'group'} = $group->name; $vars->{'groups'} = [Bugzilla::Group->get_all]; - print $cgi->header(); $template->process("admin/groups/list.html.tmpl", $vars) || ThrowTemplateError($template->error()); - exit; } diff --git a/editkeywords.cgi b/editkeywords.cgi index 41496f362..01f30dbed 100755 --- a/editkeywords.cgi +++ b/editkeywords.cgi @@ -24,10 +24,6 @@ my $dbh = Bugzilla->dbh; my $template = Bugzilla->template; my $vars = {}; -# -# Preliminary checks: -# - my $user = Bugzilla->login(LOGIN_REQUIRED); print $cgi->header(); @@ -47,22 +43,16 @@ $vars->{'action'} = $action; if ($action eq "") { $vars->{'keywords'} = Bugzilla::Keyword->get_all_with_bug_count(); - print $cgi->header(); $template->process("admin/keywords/list.html.tmpl", $vars) || ThrowTemplateError($template->error()); - exit; } - if ($action eq 'add') { $vars->{'token'} = issue_session_token('add_keyword'); - print $cgi->header(); - $template->process("admin/keywords/create.html.tmpl", $vars) || ThrowTemplateError($template->error()); - exit; } @@ -79,8 +69,6 @@ if ($action eq 'new') { delete_token($token); - print $cgi->header(); - $vars->{'message'} = 'keyword_created'; $vars->{'name'} = $keyword->name; $vars->{'keywords'} = Bugzilla::Keyword->get_all_with_bug_count(); @@ -104,7 +92,6 @@ if ($action eq 'edit') { $vars->{'keyword'} = $keyword; $vars->{'token'} = issue_session_token('edit_keyword'); - print $cgi->header(); $template->process("admin/keywords/edit.html.tmpl", $vars) || ThrowTemplateError($template->error()); exit; @@ -128,8 +115,6 @@ if ($action eq 'update') { delete_token($token); - print $cgi->header(); - $vars->{'message'} = 'keyword_updated'; $vars->{'keyword'} = $keyword; $vars->{'changes'} = $changes; @@ -147,7 +132,6 @@ if ($action eq 'del') { $vars->{'keyword'} = $keyword; $vars->{'token'} = issue_session_token('delete_keyword'); - print $cgi->header(); $template->process("admin/keywords/confirm-delete.html.tmpl", $vars) || ThrowTemplateError($template->error()); exit; @@ -162,8 +146,6 @@ if ($action eq 'delete') { delete_token($token); - print $cgi->header(); - $vars->{'message'} = 'keyword_deleted'; $vars->{'keyword'} = $keyword; $vars->{'keywords'} = Bugzilla::Keyword->get_all_with_bug_count(); diff --git a/js/productform.js b/js/productform.js index d5e24d306..0f669aa26 100644 --- a/js/productform.js +++ b/js/productform.js @@ -10,16 +10,10 @@ // collection of javascript arrays containing strings. /** - * Reads the selected products and updates component, version and milestone - * lists accordingly. + * Reads the selected products and updates the component list accordingly. * * @param product Select element that contains products. - * @param component Select element that contains components. Can be null if - * there is no such element to update. - * @param version Select element that contains versions. Can be null if - * there is no such element to update. - * @param milestone Select element that contains milestones. Can be null if - * there is no such element to update. + * @param component Select element that contains components. * @param anyval Value to use for a special "Any" list item. Can be null * to not use any. If used must and will be first item in * the select element. @@ -27,21 +21,15 @@ * @global cpts Array of arrays, indexed by product name. The subarrays * contain a list of components to be fed to the respective * select element. - * @global vers Array of arrays, indexed by product name. The subarrays - * contain a list of versions to be fed to the respective - * select element. - * @global tms Array of arrays, indexed by product name. The subarrays - * contain a list of milestones to be fed to the respective - * select element. * @global first_load Boolean; true if this is the first time this page loads * or false if not. * @global last_sel Array that contains last list of products so we know what * has changed, and optimize for additions. */ -function selectProduct(product, component, version, milestone, anyval) { +function selectProduct(product, component, anyval) { // This is to avoid handling events that occur before the form // itself is ready, which could happen in buggy browsers. - if (!product) + if (!product || !component) return; // Do nothing if no products are defined. This is to avoid the @@ -78,15 +66,8 @@ function selectProduct(product, component, version, milestone, anyval) { var findall = (product.selectedIndex == -1 || (anyval != null && product.options[0].selected)); - if (useclassification) { - // Update index based on the complete product array. - sel = get_selection(product, findall, true, anyval); - for (var i=0; iparam('id')) { my %is_private; foreach my $field (grep(/^defined_isprivate/, $cgi->param())) { - $field =~ /(\d+)$/; - my $comment_id = $1; - $is_private{$comment_id} = $cgi->param("isprivate_$comment_id"); + if ($field =~ /(\d+)$/) { + my $comment_id = $1; + $is_private{$comment_id} = $cgi->param("isprivate_$comment_id"); + } } $set_all_fields{comment_is_private} = \%is_private; diff --git a/report.cgi b/report.cgi index 2a8317d7a..d5f471ef0 100755 --- a/report.cgi +++ b/report.cgi @@ -359,19 +359,22 @@ my $format = $template->get_format("reports/report", $formatparam, # If we get a template or CGI error, it comes out as HTML, which isn't valid # PNG data, and the browser just displays a "corrupt PNG" message. So, you can # set debug=1 to always get an HTML content-type, and view the error. -$format->{'ctype'} = "text/html" if $cgi->param('debug'); +if (exists $vars->{'debug'}) { + # Bug 1439260 - if we're using debug mode, always use the HTML template + # which has proper filters in it. Debug forces an HTML content type + # anyway, and can cause XSS if we're not filtering the output. + $format = $template->get_format("reports/report", $formatparam, "html"); +} $cgi->set_dated_content_disp("inline", "report", $format->{extension}); print $cgi->header($format->{'ctype'}); # Problems with this CGI are often due to malformed data. Setting debug=1 # prints out both data structures. -if ($cgi->param('debug')) { +if (exists $vars->{'debug'}) { require Data::Dumper; - say "
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/request.cgi b/request.cgi index 566781a0c..347cb7a44 100755 --- a/request.cgi +++ b/request.cgi @@ -305,13 +305,22 @@ sub queue { $vars->{'requests'} = \@requests; $vars->{'types'} = \@types; - my %components; - foreach my $prod (@{$user->get_selectable_products}) { - foreach my $comp (@{$prod->components}) { - $components{$comp->name} = 1; + # This code is needed to populate the Product and Component select fields. + my ($products, %components); + if (Bugzilla->params->{useclassification}) { + foreach my $class (@{$user->get_selectable_classifications}) { + push @$products, @{$user->get_selectable_products($class->id)}; } } - $vars->{'components'} = [ sort { $a cmp $b } keys %components ]; + else { + $products = $user->get_selectable_products; + } + + foreach my $product (@$products) { + $components{$_->name} = 1 foreach @{$product->components}; + } + $vars->{'products'} = $products; + $vars->{'components'} = [ sort keys %components ]; $vars->{'urlquerypart'} = $cgi->canonicalise_query('ctype'); diff --git a/showdependencygraph.cgi b/showdependencygraph.cgi index 476df1e8e..7b2d2f55d 100755 --- a/showdependencygraph.cgi +++ b/showdependencygraph.cgi @@ -55,13 +55,19 @@ sub CreateImagemap { $default = qq{\n}; } - if ($line =~ /^rectangle \((.*),(.*)\) \((.*),(.*)\) (http[^ ]*) (\d+)(\\n.*)?$/) { + if ($line =~ /^rectangle \((\d+),(\d+)\) \((\d+),(\d+)\) (http[^ ]*) (\d+)(?:\\n.*)?$/) { my ($leftx, $rightx, $topy, $bottomy, $url, $bugid) = ($1, $3, $2, $4, $5, $6); # Pick up bugid from the mapdata label field. Getting the title from # bugtitle hash instead of mapdata allows us to get the summary even # when showsummary is off, and also gives us status and resolution. + # This text is safe; it has already been escaped. my $bugtitle = $bugtitles{$bugid}; + + # The URL is supposed to be safe, because it's built manually. + # But in case someone manages to inject code, it's safer to escape it. + $url = html_quote($url); + $map .= qq{bug $bugid\n}; @@ -198,6 +204,9 @@ foreach my $k (@bug_ids) { utf8::encode($summary) if utf8::is_utf8($summary); } $summary =~ s/([\\\"])/\\$1/g; + # Newlines must be escaped too, to not break the .map file + # and to prevent code injection. + $summary =~ s/\n/\\n/g; push(@params, qq{label="$k\\n$summary"}); } diff --git a/t/002goodperl.t b/t/002goodperl.t index d1858361f..8fcd1a343 100644 --- a/t/002goodperl.t +++ b/t/002goodperl.t @@ -14,7 +14,7 @@ use 5.10.1; use strict; use warnings; -use lib 't'; +use lib qw(. lib t); use Support::Files; diff --git a/t/003safesys.t b/t/003safesys.t index 443f96415..e1d04bed8 100644 --- a/t/003safesys.t +++ b/t/003safesys.t @@ -14,7 +14,7 @@ use 5.10.1; use strict; use warnings; -use lib 't'; +use lib qw(. lib t); use Support::Files; diff --git a/t/004template.t b/t/004template.t index 0a6f0e0aa..f336441ac 100644 --- a/t/004template.t +++ b/t/004template.t @@ -13,7 +13,7 @@ use 5.10.1; use strict; use warnings; -use lib 't'; +use lib qw(. lib t); use Support::Templates; @@ -114,10 +114,10 @@ foreach my $include_path (@include_paths) { # Forbid single quotes to delimit URLs, see bug 926085. if ($data =~ /href=\\?'/) { - ok(0, "$path contains blacklisted constructs: href='...'"); + ok(0, "$path contains blocklisted constructs: href='...'"); } else { - ok(1, "$path contains no blacklisted constructs"); + ok(1, "$path contains no blocklisted constructs"); } } } diff --git a/t/005whitespace.t b/t/005whitespace.t index b6de8cee3..b7f10cae2 100644 --- a/t/005whitespace.t +++ b/t/005whitespace.t @@ -13,7 +13,7 @@ use 5.10.1; use strict; use warnings; -use lib 't'; +use lib qw(. lib t); use Support::Files; use Support::Templates; diff --git a/t/006spellcheck.t b/t/006spellcheck.t index 24e00242d..ea298c5f6 100644 --- a/t/006spellcheck.t +++ b/t/006spellcheck.t @@ -14,7 +14,7 @@ use 5.10.1; use strict; use warnings; -use lib 't'; +use lib qw(. lib t); use Support::Files; # -1 because 006spellcheck.t must not be checked. diff --git a/t/007util.t b/t/007util.t index 66c2df032..6f5ea8b72 100644 --- a/t/007util.t +++ b/t/007util.t @@ -13,7 +13,7 @@ use 5.10.1; use strict; use warnings; -use lib 't'; +use lib qw(. lib t); use Support::Files; use Test::More tests => 17; use DateTime; diff --git a/t/009bugwords.t b/t/009bugwords.t index e36651edb..5b0bf1f94 100644 --- a/t/009bugwords.t +++ b/t/009bugwords.t @@ -19,7 +19,7 @@ use 5.10.1; use strict; use warnings; -use lib 't'; +use lib qw(. t lib); use Support::Files; use Support::Templates; diff --git a/t/010dependencies.t b/t/010dependencies.t index afd29a652..e0e990172 100644 --- a/t/010dependencies.t +++ b/t/010dependencies.t @@ -69,7 +69,7 @@ foreach my $module (keys %mods) { $used =~ s#/#::#g; $used =~ s#\.pm$##; $used =~ s#\$module#[^:]+#; - $used =~ s#\${[^}]+}#[^:]+#; + $used =~ s#\$\{[^}]+\}#[^:]+#; $used =~ s#[" ]##g; push(@use, grep(/^\Q$used\E$/, keys %mods)); } diff --git a/t/011pod.t b/t/011pod.t index 8a7f374ce..fd37faa7c 100644 --- a/t/011pod.t +++ b/t/011pod.t @@ -14,7 +14,7 @@ use 5.10.1; use strict; use warnings; -use lib 't'; +use lib qw(. lib t); use Support::Files; use Pod::Checker; diff --git a/taskgraph.json b/taskgraph.json index f292653be..ba1d1f3e0 100644 --- a/taskgraph.json +++ b/taskgraph.json @@ -10,14 +10,15 @@ "reruns": 3, "maxRunTime": 3000, "task": { + "expires": "2018-02-18T17:33:38.806Z", "metadata": { "name": "Basic Sanity Tests" }, "provisionerId": "aws-provisioner-v1", "workerType": "b2gtest", "payload": { - "image": "dklawren/docker-bugzilla", - "command": ["/runtests.sh"], + "image": "bugzilla/bugzilla-ci", + "command": ["runtests.sh"], "env": { "TEST_SUITE": "sanity" }, @@ -25,13 +26,19 @@ "public/runtests_log": { "type": "file", "path": "/runtests.log", - "expires": "2016-02-17T17:33:38.806Z" + "expires": "2018-02-17T17:33:38.806Z" } } }, "extra": { "treeherder": { - "symbol": "San" + "symbol": "San", + "machine": { + "platform": "linux64" + }, + "build": { + "platform": "linux64" + } } } } @@ -40,14 +47,15 @@ "reruns": 3, "maxRunTime": 3000, "task": { + "expires": "2018-02-18T17:33:38.806Z", "metadata": { "name": "Documentation Build Test" }, "provisionerId": "aws-provisioner-v1", "workerType": "b2gtest", "payload": { - "image": "dklawren/docker-bugzilla", - "command": ["/runtests.sh"], + "image": "bugzilla/bugzilla-ci", + "command": ["runtests.sh"], "env": { "TEST_SUITE": "docs" }, @@ -55,13 +63,19 @@ "public/runtests_log": { "type": "file", "path": "/runtests.log", - "expires": "2016-02-17T17:33:38.806Z" + "expires": "2018-02-17T17:33:38.806Z" } } }, "extra": { "treeherder": { - "symbol": "Doc" + "symbol": "Doc", + "machine": { + "platform": "linux64" + }, + "build": { + "platform": "linux64" + } } } } @@ -70,14 +84,15 @@ "reruns": 3, "maxRunTime": 7200, "task": { + "expires": "2018-02-18T17:33:38.806Z", "metadata": { "name": "WebService API Tests (MySQL)" }, "provisionerId": "aws-provisioner-v1", "workerType": "b2gtest", "payload": { - "image": "dklawren/docker-bugzilla", - "command": ["/runtests.sh"], + "image": "bugzilla/bugzilla-ci", + "command": ["runtests.sh"], "env": { "TEST_SUITE": "webservices" }, @@ -85,18 +100,24 @@ "public/runtests_log": { "type": "file", "path": "/runtests.log", - "expires": "2016-02-17T17:33:38.806Z" + "expires": "2018-02-17T17:33:38.806Z" }, "public/httpd_error_log": { "type": "file", "path": "/var/log/httpd/error_log", - "expires": "2016-02-17T17:33:38.806Z" + "expires": "2018-02-17T17:33:38.806Z" } } }, "extra": { "treeherder": { - "symbol": "API" + "symbol": "API", + "machine": { + "platform": "linux64" + }, + "build": { + "platform": "linux64" + } } } } @@ -105,38 +126,45 @@ "reruns": 3, "maxRunTime": 7200, "task": { + "expires": "2018-02-18T17:33:38.806Z", "metadata": { "name": "Selenium Tests (MySQL)" }, "provisionerId": "aws-provisioner-v1", "workerType": "b2gtest", "payload": { - "image": "dklawren/docker-bugzilla", - "command": ["/runtests.sh"], + "image": "bugzilla/bugzilla-ci", + "command": ["runtests.sh"], "env": { "TEST_SUITE": "selenium" }, "artifacts": { "public/runtests_log": { "type": "file", - "path": "/runtests.log", - "expires": "2016-02-17T17:33:38.806Z" + "path": "/tmp/runtests.log", + "expires": "2018-02-17T17:33:38.806Z" }, "public/httpd_error_log": { "type": "file", "path": "/var/log/httpd/error_log", - "expires": "2016-02-17T17:33:38.806Z" + "expires": "2018-02-17T17:33:38.806Z" }, "public/selenium_log": { "type": "file", - "path": "/selenium.log", - "expires": "2016-02-17T17:33:38.806Z" + "path": "/tmp/selenium.log", + "expires": "2018-02-17T17:33:38.806Z" } } }, "extra": { "treeherder": { - "symbol": "Sel" + "symbol": "Sel", + "machine": { + "platform": "linux64" + }, + "build": { + "platform": "linux64" + } } } } @@ -145,33 +173,41 @@ "reruns": 3, "maxRunTime": 7200, "task": { + "expires": "2018-02-18T17:33:38.806Z", "metadata": { "name": "WebService API Tests (Pg)" }, "provisionerId": "aws-provisioner-v1", "workerType": "b2gtest", "payload": { - "image": "dklawren/docker-bugzilla:pgsql", - "command": ["/runtests.sh"], + "image": "bugzilla/bugzilla-ci", + "command": ["runtests.sh"], "env": { + "BUGS_DB_DRIVER": "pg", "TEST_SUITE": "webservices" }, "artifacts": { "public/runtests_log": { "type": "file", - "path": "/runtests.log", - "expires": "2016-02-17T17:33:38.806Z" + "path": "/tmp/runtests.log", + "expires": "2018-02-17T17:33:38.806Z" }, "public/httpd_error_log": { "type": "file", "path": "/var/log/httpd/error_log", - "expires": "2016-02-17T17:33:38.806Z" + "expires": "2018-02-17T17:33:38.806Z" } } }, "extra": { "treeherder": { - "symbol": "API-Pg" + "symbol": "API-Pg", + "machine": { + "platform": "linux64" + }, + "build": { + "platform": "linux64" + } } } } @@ -180,38 +216,46 @@ "reruns": 3, "maxRunTime": 7200, "task": { + "expires": "2018-02-18T17:33:38.806Z", "metadata": { "name": "Selenium Tests (Pg)" }, "provisionerId": "aws-provisioner-v1", "workerType": "b2gtest", "payload": { - "image": "dklawren/docker-bugzilla:pgsql", - "command": ["/runtests.sh"], + "image": "bugzilla/bugzilla-ci", + "command": ["runtests.sh"], "env": { + "BUGS_DB_DRIVER": "pg", "TEST_SUITE": "selenium" }, "artifacts": { "public/runtests_log": { "type": "file", - "path": "/runtests.log", - "expires": "2016-02-17T17:33:38.806Z" + "path": "/tmp/runtests.log", + "expires": "2018-02-17T17:33:38.806Z" }, "public/httpd_error_log": { "type": "file", "path": "/var/log/httpd/error_log", - "expires": "2016-02-17T17:33:38.806Z" + "expires": "2018-02-17T17:33:38.806Z" }, "public/selenium_log": { "type": "file", - "path": "/selenium.log", - "expires": "2016-02-17T17:33:38.806Z" + "path": "/tmp/selenium.log", + "expires": "2018-02-17T17:33:38.806Z" } } }, "extra": { "treeherder": { - "symbol": "Sel-Pg" + "symbol": "Sel-Pg", + "machine": { + "platform": "linux64" + }, + "build": { + "platform": "linux64" + } } } } diff --git a/template/en/default/account/auth/login-small.html.tmpl b/template/en/default/account/auth/login-small.html.tmpl index 65aa861a1..a1a074372 100644 --- a/template/en/default/account/auth/login-small.html.tmpl +++ b/template/en/default/account/auth/login-small.html.tmpl @@ -27,8 +27,6 @@ Log In - [% Hook.process('additional_methods') %] - -
  • - | - Forgot Password -
    - - - - - - [x] -
    -
  • + +[% Hook.process('additional_methods') %] + +[% IF user.authorizer.can_change_password %] +
  • + | + Forgot Password +
    + + + + + + [x] +
    +
  • +[% END %] diff --git a/template/en/default/admin/flag-type/edit.html.tmpl b/template/en/default/admin/flag-type/edit.html.tmpl index 6c0e0cb78..7505ebd8c 100644 --- a/template/en/default/admin/flag-type/edit.html.tmpl +++ b/template/en/default/admin/flag-type/edit.html.tmpl @@ -23,7 +23,7 @@ title = title style_urls = ['skins/standard/admin.css'] onload="var f = document.forms['flagtype_properties']; - selectProduct(f.product, f.component, null, null, '__Any__');" + selectProduct(f.product, f.component, '__Any__');" javascript_urls=["js/productform.js"] doc_section = "administering/flags.html" %] @@ -92,7 +92,7 @@ id => "product" name => "product" add => "__Any__" - onchange => "selectProduct(this, this.form.component, null, null, '__Any__');" + onchange => "selectProduct(this, this.form.component, '__Any__');" products => products %]