From 7f3a749d7bd78a3e4aee163f562d7e95b0954b44 Mon Sep 17 00:00:00 2001 From: Perl Tidy Date: Wed, 30 Jan 2019 20:00:43 -0500 Subject: no bug - reformat all the code using the new perltidy rules --- .perltidyrc | 17 + Bugzilla.pm | 900 +-- Bugzilla/Attachment.pm | 997 +-- Bugzilla/Attachment/PatchReader.pm | 499 +- Bugzilla/Auth.pm | 349 +- Bugzilla/Auth/Login.pm | 18 +- Bugzilla/Auth/Login/APIKey.pm | 35 +- Bugzilla/Auth/Login/CGI.pm | 118 +- Bugzilla/Auth/Login/Cookie.pm | 185 +- Bugzilla/Auth/Login/Env.pm | 27 +- Bugzilla/Auth/Login/Stack.pm | 126 +- Bugzilla/Auth/Persist/Cookie.pm | 267 +- Bugzilla/Auth/Verify.pm | 200 +- Bugzilla/Auth/Verify/DB.pm | 146 +- Bugzilla/Auth/Verify/LDAP.pm | 253 +- Bugzilla/Auth/Verify/RADIUS.pm | 58 +- Bugzilla/Auth/Verify/Stack.pm | 107 +- Bugzilla/Bug.pm | 7353 ++++++++++---------- Bugzilla/BugMail.pm | 996 +-- Bugzilla/BugUrl.pm | 228 +- Bugzilla/BugUrl/Bugzilla.pm | 52 +- Bugzilla/BugUrl/Bugzilla/Local.pm | 110 +- Bugzilla/BugUrl/Debian.pm | 34 +- Bugzilla/BugUrl/GitHub.pm | 26 +- Bugzilla/BugUrl/Google.pm | 30 +- Bugzilla/BugUrl/JIRA.pm | 25 +- Bugzilla/BugUrl/Launchpad.pm | 31 +- Bugzilla/BugUrl/MantisBT.pm | 18 +- Bugzilla/BugUrl/SourceForge.pm | 30 +- Bugzilla/BugUrl/Trac.pm | 25 +- Bugzilla/BugUserLastVisit.pm | 22 +- Bugzilla/CGI.pm | 1069 +-- Bugzilla/Chart.pm | 697 +- Bugzilla/Classification.pm | 198 +- Bugzilla/Comment.pm | 648 +- Bugzilla/Comment/TagWeights.pm | 10 +- Bugzilla/Component.pm | 540 +- Bugzilla/Config.pm | 527 +- Bugzilla/Config/Admin.pm | 39 +- Bugzilla/Config/Advanced.pm | 29 +- Bugzilla/Config/Attachment.pm | 75 +- Bugzilla/Config/Auth.pm | 182 +- Bugzilla/Config/BugChange.pm | 70 +- Bugzilla/Config/BugFields.pm | 111 +- Bugzilla/Config/Common.pm | 610 +- Bugzilla/Config/Core.pm | 32 +- Bugzilla/Config/DependencyGraph.pm | 22 +- Bugzilla/Config/General.pm | 43 +- Bugzilla/Config/GroupSecurity.pm | 133 +- Bugzilla/Config/LDAP.pm | 45 +- Bugzilla/Config/MTA.pm | 97 +- Bugzilla/Config/Memcached.pm | 12 +- Bugzilla/Config/Query.pm | 78 +- Bugzilla/Config/RADIUS.pm | 32 +- Bugzilla/Config/ShadowDB.pm | 44 +- Bugzilla/Config/UserMatch.pm | 39 +- Bugzilla/Constants.pm | 791 ++- Bugzilla/DB.pm | 1889 ++--- Bugzilla/DB/Mysql.pm | 1603 ++--- Bugzilla/DB/Oracle.pm | 1019 +-- Bugzilla/DB/Pg.pm | 611 +- Bugzilla/DB/Schema.pm | 4123 ++++++----- Bugzilla/DB/Schema/Mysql.pm | 571 +- Bugzilla/DB/Schema/Oracle.pm | 782 ++- Bugzilla/DB/Schema/Pg.pm | 286 +- Bugzilla/DB/Schema/Sqlite.pm | 420 +- Bugzilla/DB/Sqlite.pm | 324 +- Bugzilla/Error.pm | 303 +- Bugzilla/Extension.pm | 300 +- Bugzilla/Field.pm | 1357 ++-- Bugzilla/Field/Choice.pm | 309 +- Bugzilla/Field/ChoiceInterface.pm | 227 +- Bugzilla/Flag.pm | 1689 ++--- Bugzilla/FlagType.pm | 791 ++- Bugzilla/Group.pm | 642 +- Bugzilla/Hook.pm | 28 +- Bugzilla/Install.pm | 692 +- Bugzilla/Install/CPAN.pm | 426 +- Bugzilla/Install/DB.pm | 6711 +++++++++--------- Bugzilla/Install/Filesystem.pm | 1347 ++-- Bugzilla/Install/Localconfig.pm | 398 +- Bugzilla/Install/Requirements.pm | 1182 ++-- Bugzilla/Install/Util.pm | 1080 +-- Bugzilla/Job/BugMail.pm | 24 +- Bugzilla/Job/Mailer.pm | 34 +- Bugzilla/JobQueue.pm | 210 +- Bugzilla/JobQueue/Runner.pm | 263 +- Bugzilla/Keyword.pm | 120 +- Bugzilla/MIME.pm | 158 +- Bugzilla/Mailer.pm | 362 +- Bugzilla/Memcached.pm | 417 +- Bugzilla/Migrate.pm | 1232 ++-- Bugzilla/Migrate/Gnats.pm | 1034 +-- Bugzilla/Milestone.pm | 296 +- Bugzilla/Object.pm | 1348 ++-- Bugzilla/Product.pm | 1230 ++-- Bugzilla/RNG.pm | 247 +- Bugzilla/Report.pm | 74 +- Bugzilla/Search.pm | 5170 +++++++------- Bugzilla/Search/Clause.pm | 170 +- Bugzilla/Search/ClauseGroup.pm | 131 +- Bugzilla/Search/Condition.pm | 70 +- Bugzilla/Search/Quicksearch.pm | 1103 +-- Bugzilla/Search/Recent.pm | 116 +- Bugzilla/Search/Saved.pm | 371 +- Bugzilla/Sender/Transport/Sendmail.pm | 145 +- Bugzilla/Series.pm | 421 +- Bugzilla/Status.pm | 241 +- Bugzilla/Template.pm | 2109 +++--- Bugzilla/Template/Context.pm | 126 +- Bugzilla/Template/Plugin/Bugzilla.pm | 14 +- Bugzilla/Template/Plugin/Hook.pm | 111 +- Bugzilla/Token.pm | 646 +- Bugzilla/Update.pm | 274 +- Bugzilla/User.pm | 3627 +++++----- Bugzilla/User/APIKey.pm | 68 +- Bugzilla/User/Setting.pm | 408 +- Bugzilla/User/Setting/Lang.pm | 6 +- Bugzilla/User/Setting/Skin.pm | 28 +- Bugzilla/User/Setting/Timezone.pm | 22 +- Bugzilla/UserAgent.pm | 352 +- Bugzilla/Util.pm | 1384 ++-- Bugzilla/Version.pm | 295 +- Bugzilla/WebService.pm | 9 +- Bugzilla/WebService/Bug.pm | 2296 +++--- Bugzilla/WebService/BugUserLastVisit.pm | 113 +- Bugzilla/WebService/Bugzilla.pm | 237 +- Bugzilla/WebService/Classification.pm | 89 +- Bugzilla/WebService/Component.pm | 39 +- Bugzilla/WebService/Constants.pm | 494 +- Bugzilla/WebService/FlagType.pm | 514 +- Bugzilla/WebService/Group.pm | 325 +- Bugzilla/WebService/Product.pm | 514 +- Bugzilla/WebService/Server.pm | 115 +- Bugzilla/WebService/Server/JSONRPC.pm | 696 +- Bugzilla/WebService/Server/REST.pm | 778 ++- Bugzilla/WebService/Server/REST/Resources/Bug.pm | 274 +- .../Server/REST/Resources/BugUserLastVisit.pm | 35 +- .../WebService/Server/REST/Resources/Bugzilla.pm | 46 +- .../Server/REST/Resources/Classification.pm | 27 +- .../WebService/Server/REST/Resources/Component.pm | 18 +- .../WebService/Server/REST/Resources/FlagType.pm | 67 +- Bugzilla/WebService/Server/REST/Resources/Group.pm | 41 +- .../WebService/Server/REST/Resources/Product.pm | 78 +- Bugzilla/WebService/Server/REST/Resources/User.pm | 76 +- Bugzilla/WebService/Server/XMLRPC.pm | 491 +- Bugzilla/WebService/User.pm | 593 +- Bugzilla/WebService/Util.pm | 442 +- Bugzilla/Whine.pm | 22 +- Bugzilla/Whine/Query.pm | 20 +- Bugzilla/Whine/Schedule.pm | 78 +- admin.cgi | 7 +- attachment.cgi | 1190 ++-- buglist.cgi | 1326 ++-- chart.cgi | 410 +- checksetup.pl | 38 +- clean-bug-user-last-visit.pl | 6 +- colchange.cgi | 232 +- collectstats.pl | 658 +- config.cgi | 155 +- contrib/Bugzilla.pm | 2 +- contrib/bz_webservice_demo.pl | 239 +- contrib/bzdbcopy.pl | 306 +- contrib/console.pl | 157 +- contrib/convert-workflow.pl | 178 +- contrib/extension-convert.pl | 304 +- contrib/merge-users.pl | 204 +- contrib/mysqld-watcher.pl | 93 +- contrib/recode.pl | 315 +- contrib/sendbugmail.pl | 35 +- contrib/sendunsentbugmail.pl | 38 +- contrib/syncLDAP.pl | 378 +- createaccount.cgi | 25 +- describecomponents.cgi | 66 +- describekeywords.cgi | 6 +- docs/lib/Pod/Simple/HTML/Bugzilla.pm | 48 +- docs/lib/Pod/Simple/HTMLBatch/Bugzilla.pm | 128 +- docs/makedocs.pl | 132 +- duplicates.cgi | 268 +- editclassifications.cgi | 212 +- editcomponents.cgi | 262 +- editfields.cgi | 246 +- editflagtypes.cgi | 860 +-- editgroups.cgi | 582 +- editkeywords.cgi | 142 +- editmilestones.cgi | 209 +- editparams.cgi | 201 +- editproducts.cgi | 545 +- editsettings.cgi | 49 +- editusers.cgi | 1168 ++-- editvalues.cgi | 133 +- editversions.cgi | 188 +- editwhines.cgi | 550 +- editworkflow.cgi | 193 +- email_in.pl | 766 +- enter_bug.cgi | 385 +- extensions/BmpConvert/Config.pm | 9 +- extensions/BmpConvert/Extension.pm | 49 +- extensions/Example/Config.pm | 21 +- extensions/Example/Extension.pm | 1512 ++-- extensions/Example/lib/Auth/Login.pm | 2 +- extensions/Example/lib/Auth/Verify.pm | 2 +- extensions/Example/lib/Config.pm | 13 +- extensions/Example/lib/WebService.pm | 4 +- .../template/en/default/setup/strings.txt.pl | 4 +- extensions/MoreBugUrl/Config.pm | 6 +- extensions/MoreBugUrl/Extension.pm | 51 +- extensions/MoreBugUrl/lib/BitBucket.pm | 20 +- extensions/MoreBugUrl/lib/GetSatisfaction.pm | 22 +- extensions/MoreBugUrl/lib/PHP.pm | 26 +- extensions/MoreBugUrl/lib/RT.pm | 25 +- extensions/MoreBugUrl/lib/Redmine.pm | 23 +- extensions/MoreBugUrl/lib/ReviewBoard.pm | 31 +- extensions/MoreBugUrl/lib/Rietveld.pm | 48 +- extensions/MoreBugUrl/lib/Savane.pm | 20 +- extensions/OldBugMove/Extension.pm | 248 +- extensions/OldBugMove/lib/Params.pm | 22 +- extensions/Voting/Config.pm | 6 +- extensions/Voting/Extension.pm | 1354 ++-- extensions/create.pl | 45 +- importxml.pl | 2066 +++--- index.cgi | 58 +- install-module.pl | 72 +- jobqueue.pl | 7 +- jsonrpc.cgi | 7 +- migrate.pl | 14 +- mod_perl.pl | 97 +- page.cgi | 79 +- post_bug.cgi | 190 +- process_bug.cgi | 518 +- query.cgi | 317 +- quips.cgi | 197 +- relogin.cgi | 329 +- report.cgi | 515 +- reports.cgi | 313 +- request.cgi | 477 +- rest.cgi | 9 +- runtests.pl | 12 +- sanitycheck.cgi | 1083 +-- sanitycheck.pl | 34 +- search_plugin.cgi | 12 +- show_activity.cgi | 11 +- show_bug.cgi | 116 +- showdependencygraph.cgi | 419 +- showdependencytree.cgi | 118 +- summarize_time.cgi | 503 +- t/001compile.t | 116 +- t/002goodperl.t | 269 +- t/003safesys.t | 61 +- t/004template.t | 180 +- t/005whitespace.t | 66 +- t/006spellcheck.t | 104 +- t/007util.t | 68 +- t/008filter.t | 310 +- t/009bugwords.t | 84 +- t/010dependencies.t | 76 +- t/011pod.t | 154 +- t/012throwables.t | 280 +- t/013dbschema.t | 80 +- t/Support/Files.pm | 40 +- t/Support/Templates.pm | 91 +- template/en/default/filterexceptions.pl | 645 +- template/en/default/setup/strings.txt.pl | 217 +- testserver.pl | 360 +- token.cgi | 415 +- userprefs.cgi | 976 +-- votes.cgi | 16 +- whine.pl | 901 +-- whineatnews.pl | 71 +- xmlrpc.cgi | 16 +- xt/lib/Bugzilla/Test/Search.pm | 1502 ++-- xt/lib/Bugzilla/Test/Search/AndTest.pm | 30 +- xt/lib/Bugzilla/Test/Search/Constants.pm | 1834 ++--- xt/lib/Bugzilla/Test/Search/CustomTest.pm | 81 +- xt/lib/Bugzilla/Test/Search/FieldTest.pm | 832 +-- xt/lib/Bugzilla/Test/Search/FieldTestNormal.pm | 129 +- xt/lib/Bugzilla/Test/Search/InjectionTest.pm | 80 +- xt/lib/Bugzilla/Test/Search/NotTest.pm | 35 +- xt/lib/Bugzilla/Test/Search/OperatorTest.pm | 103 +- xt/lib/Bugzilla/Test/Search/OrTest.pm | 139 +- xt/search.t | 3 +- 281 files changed, 59679 insertions(+), 57005 deletions(-) create mode 100644 .perltidyrc diff --git a/.perltidyrc b/.perltidyrc new file mode 100644 index 000000000..95c897374 --- /dev/null +++ b/.perltidyrc @@ -0,0 +1,17 @@ +-pbp # Start with Perl Best Practices +-w # Show all warnings +-iob # Ignore old breakpoints +-l=80 # 80 characters per line +-vmll +-ibc +-iscl +-hsc +-mbl=2 # No more than 2 blank lines +-i=2 # Indentation is 2 columns +-ci=2 # Continuation indentation is 2 columns +-vt=0 # Less vertical tightness +-pt=2 # High parenthesis tightness +-bt=2 # High brace tightness +-sbt=2 # High square bracket tightness +-wn # Weld nested containers +-isbc # Don't indent comments without leading space diff --git a/Bugzilla.pm b/Bugzilla.pm index e4772e08b..deffa259e 100644 --- a/Bugzilla.pm +++ b/Bugzilla.pm @@ -13,11 +13,11 @@ use warnings; # We want any compile errors to get to the browser, if possible. BEGIN { - # This makes sure we're in a CGI. - if ($ENV{SERVER_SOFTWARE} && !$ENV{MOD_PERL}) { - require CGI::Carp; - CGI::Carp->import('fatalsToBrowser'); - } + # This makes sure we're in a CGI. + if ($ENV{SERVER_SOFTWARE} && !$ENV{MOD_PERL}) { + require CGI::Carp; + CGI::Carp->import('fatalsToBrowser'); + } } use Bugzilla::Auth; @@ -51,15 +51,15 @@ use Safe; # Scripts that are not stopped by shutdownhtml being in effect. use constant SHUTDOWNHTML_EXEMPT => qw( - editparams.cgi - checksetup.pl - migrate.pl - recode.pl + editparams.cgi + checksetup.pl + migrate.pl + recode.pl ); # Non-cgi scripts that should silently exit. use constant SHUTDOWNHTML_EXIT_SILENTLY => qw( - whine.pl + whine.pl ); # shutdownhtml pages are sent as an HTTP 503. After how many seconds @@ -74,119 +74,127 @@ use constant SHUTDOWNHTML_RETRY_AFTER => 3600; # Note that this is a raw subroutine, not a method, so $class isn't available. sub init_page { - if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) { - init_console(); - } - elsif (Bugzilla->params->{'utf8'}) { - binmode STDOUT, ':utf8'; - } + if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) { + init_console(); + } + elsif (Bugzilla->params->{'utf8'}) { + binmode STDOUT, ':utf8'; + } - if (${^TAINT}) { - my $path = ''; - if (ON_WINDOWS) { - # On Windows, these paths are tainted, preventing - # File::Spec::Win32->tmpdir from using them. But we need - # a place to temporary store attachments which are uploaded. - foreach my $temp (qw(TMPDIR TMP TEMP WINDIR)) { - trick_taint($ENV{$temp}) if $ENV{$temp}; - } - # Some DLLs used by Strawberry Perl are also in c\bin, - # see https://rt.cpan.org/Public/Bug/Display.html?id=99104 - if (!ON_ACTIVESTATE) { - my $c_path = $path = dirname($^X); - $c_path =~ s/\bperl\b(?=\\bin)/c/; - $path .= ";$c_path"; - trick_taint($path); - } - } - # Some environment variables are not taint safe - delete @::ENV{'PATH', 'IFS', 'CDPATH', 'ENV', 'BASH_ENV'}; - # Some modules throw undefined errors (notably File::Spec::Win32) if - # PATH is undefined. - $ENV{'PATH'} = $path; + if (${^TAINT}) { + my $path = ''; + if (ON_WINDOWS) { + + # On Windows, these paths are tainted, preventing + # File::Spec::Win32->tmpdir from using them. But we need + # a place to temporary store attachments which are uploaded. + foreach my $temp (qw(TMPDIR TMP TEMP WINDIR)) { + trick_taint($ENV{$temp}) if $ENV{$temp}; + } + + # Some DLLs used by Strawberry Perl are also in c\bin, + # see https://rt.cpan.org/Public/Bug/Display.html?id=99104 + if (!ON_ACTIVESTATE) { + my $c_path = $path = dirname($^X); + $c_path =~ s/\bperl\b(?=\\bin)/c/; + $path .= ";$c_path"; + trick_taint($path); + } } - # Because this function is run live from perl "use" commands of - # other scripts, we're skipping the rest of this function if we get here - # during a perl syntax check (perl -c, like we do during the - # 001compile.t test). - return if $^C; - - # IIS prints out warnings to the webpage, so ignore them, or log them - # to a file if the file exists. - if ($ENV{SERVER_SOFTWARE} && $ENV{SERVER_SOFTWARE} =~ /microsoft-iis/i) { - $SIG{__WARN__} = sub { - my ($msg) = @_; - my $datadir = bz_locations()->{'datadir'}; - if (-w "$datadir/errorlog") { - my $warning_log = new IO::File(">>$datadir/errorlog"); - print $warning_log $msg; - $warning_log->close(); - } - }; - } + # Some environment variables are not taint safe + delete @::ENV{'PATH', 'IFS', 'CDPATH', 'ENV', 'BASH_ENV'}; + + # Some modules throw undefined errors (notably File::Spec::Win32) if + # PATH is undefined. + $ENV{'PATH'} = $path; + } + + # Because this function is run live from perl "use" commands of + # other scripts, we're skipping the rest of this function if we get here + # during a perl syntax check (perl -c, like we do during the + # 001compile.t test). + return if $^C; + + # IIS prints out warnings to the webpage, so ignore them, or log them + # to a file if the file exists. + if ($ENV{SERVER_SOFTWARE} && $ENV{SERVER_SOFTWARE} =~ /microsoft-iis/i) { + $SIG{__WARN__} = sub { + my ($msg) = @_; + my $datadir = bz_locations()->{'datadir'}; + if (-w "$datadir/errorlog") { + my $warning_log = new IO::File(">>$datadir/errorlog"); + print $warning_log $msg; + $warning_log->close(); + } + }; + } + + my $script = basename($0); - my $script = basename($0); + # Because of attachment_base, attachment.cgi handles this itself. + if ($script ne 'attachment.cgi') { + do_ssl_redirect_if_required(); + } - # Because of attachment_base, attachment.cgi handles this itself. - if ($script ne 'attachment.cgi') { - do_ssl_redirect_if_required(); + # If Bugzilla is shut down, do not allow anything to run, just display a + # message to the user about the downtime and log out. Scripts listed in + # SHUTDOWNHTML_EXEMPT are exempt from this message. + # + # This code must go here. It cannot go anywhere in Bugzilla::CGI, because + # it uses Template, and that causes various dependency loops. + if (!grep { $_ eq $script } SHUTDOWNHTML_EXEMPT + and Bugzilla->params->{'shutdownhtml'}) + { + # Allow non-cgi scripts to exit silently (without displaying any + # message), if desired. At this point, no DBI call has been made + # yet, and no error will be returned if the DB is inaccessible. + if (!i_am_cgi() && grep { $_ eq $script } SHUTDOWNHTML_EXIT_SILENTLY) { + exit; } - # If Bugzilla is shut down, do not allow anything to run, just display a - # message to the user about the downtime and log out. Scripts listed in - # SHUTDOWNHTML_EXEMPT are exempt from this message. - # - # This code must go here. It cannot go anywhere in Bugzilla::CGI, because - # it uses Template, and that causes various dependency loops. - if (!grep { $_ eq $script } SHUTDOWNHTML_EXEMPT - and Bugzilla->params->{'shutdownhtml'}) + # For security reasons, log out users when Bugzilla is down. + # Bugzilla->login() is required to catch the logincookie, if any. + my $user; + eval { $user = Bugzilla->login(LOGIN_OPTIONAL); }; + if ($@) { + + # The DB is not accessible. Use the default user object. + $user = Bugzilla->user; + $user->{settings} = {}; + } + my $userid = $user->id; + Bugzilla->logout(); + + my $template = Bugzilla->template; + my $vars = {}; + $vars->{'message'} = 'shutdown'; + $vars->{'userid'} = $userid; + + # Generate and return a message about the downtime, appropriately + # for if we're a command-line script or a CGI script. + my $extension; + if (i_am_cgi() + && (!Bugzilla->cgi->param('ctype') || Bugzilla->cgi->param('ctype') eq 'html')) { - # Allow non-cgi scripts to exit silently (without displaying any - # message), if desired. At this point, no DBI call has been made - # yet, and no error will be returned if the DB is inaccessible. - if (!i_am_cgi() - && grep { $_ eq $script } SHUTDOWNHTML_EXIT_SILENTLY) - { - exit; - } - - # For security reasons, log out users when Bugzilla is down. - # Bugzilla->login() is required to catch the logincookie, if any. - my $user; - eval { $user = Bugzilla->login(LOGIN_OPTIONAL); }; - if ($@) { - # The DB is not accessible. Use the default user object. - $user = Bugzilla->user; - $user->{settings} = {}; - } - my $userid = $user->id; - Bugzilla->logout(); - - my $template = Bugzilla->template; - my $vars = {}; - $vars->{'message'} = 'shutdown'; - $vars->{'userid'} = $userid; - # Generate and return a message about the downtime, appropriately - # for if we're a command-line script or a CGI script. - my $extension; - if (i_am_cgi() && (!Bugzilla->cgi->param('ctype') - || Bugzilla->cgi->param('ctype') eq 'html')) { - $extension = 'html'; - } - else { - $extension = 'txt'; - } - if (i_am_cgi()) { - # Set the HTTP status to 503 when Bugzilla is down to avoid pages - # being indexed by search engines. - print Bugzilla->cgi->header(-status => 503, - -retry_after => SHUTDOWNHTML_RETRY_AFTER); - } - $template->process("global/message.$extension.tmpl", $vars) - || ThrowTemplateError($template->error); - exit; + $extension = 'html'; + } + else { + $extension = 'txt'; + } + if (i_am_cgi()) { + + # Set the HTTP status to 503 when Bugzilla is down to avoid pages + # being indexed by search engines. + print Bugzilla->cgi->header( + -status => 503, + -retry_after => SHUTDOWNHTML_RETRY_AFTER + ); } + $template->process("global/message.$extension.tmpl", $vars) + || ThrowTemplateError($template->error); + exit; + } } ##################################################################### @@ -194,434 +202,446 @@ sub init_page { ##################################################################### sub template { - return $_[0]->request_cache->{template} ||= Bugzilla::Template->create(); + return $_[0]->request_cache->{template} ||= Bugzilla::Template->create(); } sub template_inner { - my ($class, $lang) = @_; - my $cache = $class->request_cache; - my $current_lang = $cache->{template_current_lang}->[0]; - $lang ||= $current_lang || ''; - return $cache->{"template_inner_$lang"} ||= Bugzilla::Template->create(language => $lang); + my ($class, $lang) = @_; + my $cache = $class->request_cache; + my $current_lang = $cache->{template_current_lang}->[0]; + $lang ||= $current_lang || ''; + return $cache->{"template_inner_$lang"} + ||= Bugzilla::Template->create(language => $lang); } our $extension_packages; + sub extensions { - my ($class) = @_; - my $cache = $class->request_cache; - if (!$cache->{extensions}) { - # Under mod_perl, mod_perl.pl populates $extension_packages for us. - if (!$extension_packages) { - $extension_packages = Bugzilla::Extension->load_all(); - } - my @extensions; - foreach my $package (@$extension_packages) { - my $extension = $package->new(); - if ($extension->enabled) { - push(@extensions, $extension); - } - } - $cache->{extensions} = \@extensions; + my ($class) = @_; + my $cache = $class->request_cache; + if (!$cache->{extensions}) { + + # Under mod_perl, mod_perl.pl populates $extension_packages for us. + if (!$extension_packages) { + $extension_packages = Bugzilla::Extension->load_all(); } - return $cache->{extensions}; + my @extensions; + foreach my $package (@$extension_packages) { + my $extension = $package->new(); + if ($extension->enabled) { + push(@extensions, $extension); + } + } + $cache->{extensions} = \@extensions; + } + return $cache->{extensions}; } sub feature { - my ($class, $feature) = @_; - my $cache = $class->request_cache; - return $cache->{feature}->{$feature} - if exists $cache->{feature}->{$feature}; - - my $feature_map = $cache->{feature_map}; - if (!$feature_map) { - foreach my $package (@{ OPTIONAL_MODULES() }) { - foreach my $f (@{ $package->{feature} }) { - $feature_map->{$f} ||= []; - push(@{ $feature_map->{$f} }, $package); - } - } - $cache->{feature_map} = $feature_map; + my ($class, $feature) = @_; + my $cache = $class->request_cache; + return $cache->{feature}->{$feature} if exists $cache->{feature}->{$feature}; + + my $feature_map = $cache->{feature_map}; + if (!$feature_map) { + foreach my $package (@{OPTIONAL_MODULES()}) { + foreach my $f (@{$package->{feature}}) { + $feature_map->{$f} ||= []; + push(@{$feature_map->{$f}}, $package); + } } + $cache->{feature_map} = $feature_map; + } - if (!$feature_map->{$feature}) { - ThrowCodeError('invalid_feature', { feature => $feature }); - } + if (!$feature_map->{$feature}) { + ThrowCodeError('invalid_feature', {feature => $feature}); + } - my $success = 1; - foreach my $package (@{ $feature_map->{$feature} }) { - have_vers($package) or $success = 0; - } - $cache->{feature}->{$feature} = $success; - return $success; + my $success = 1; + foreach my $package (@{$feature_map->{$feature}}) { + have_vers($package) or $success = 0; + } + $cache->{feature}->{$feature} = $success; + return $success; } sub cgi { - return $_[0]->request_cache->{cgi} ||= new Bugzilla::CGI(); + return $_[0]->request_cache->{cgi} ||= new Bugzilla::CGI(); } sub input_params { - my ($class, $params) = @_; - my $cache = $class->request_cache; - # This is how the WebService and other places set input_params. - if (defined $params) { - $cache->{input_params} = $params; - } - return $cache->{input_params} if defined $cache->{input_params}; + my ($class, $params) = @_; + my $cache = $class->request_cache; + + # This is how the WebService and other places set input_params. + if (defined $params) { + $cache->{input_params} = $params; + } + return $cache->{input_params} if defined $cache->{input_params}; - # Making this scalar makes it a tied hash to the internals of $cgi, - # so if a variable is changed, then it actually changes the $cgi object - # as well. - $cache->{input_params} = $class->cgi->Vars; - return $cache->{input_params}; + # Making this scalar makes it a tied hash to the internals of $cgi, + # so if a variable is changed, then it actually changes the $cgi object + # as well. + $cache->{input_params} = $class->cgi->Vars; + return $cache->{input_params}; } sub localconfig { - return $_[0]->process_cache->{localconfig} ||= read_localconfig(); + return $_[0]->process_cache->{localconfig} ||= read_localconfig(); } sub params { - return $_[0]->request_cache->{params} ||= Bugzilla::Config::read_param_file(); + return $_[0]->request_cache->{params} ||= Bugzilla::Config::read_param_file(); } sub user { - return $_[0]->request_cache->{user} ||= new Bugzilla::User; + return $_[0]->request_cache->{user} ||= new Bugzilla::User; } sub set_user { - my ($class, $user) = @_; - $class->request_cache->{user} = $user; + my ($class, $user) = @_; + $class->request_cache->{user} = $user; } sub sudoer { - return $_[0]->request_cache->{sudoer}; + return $_[0]->request_cache->{sudoer}; } sub sudo_request { - my ($class, $new_user, $new_sudoer) = @_; - $class->request_cache->{user} = $new_user; - $class->request_cache->{sudoer} = $new_sudoer; - # NOTE: If you want to log the start of an sudo session, do it here. + my ($class, $new_user, $new_sudoer) = @_; + $class->request_cache->{user} = $new_user; + $class->request_cache->{sudoer} = $new_sudoer; + + # NOTE: If you want to log the start of an sudo session, do it here. } sub page_requires_login { - return $_[0]->request_cache->{page_requires_login}; + return $_[0]->request_cache->{page_requires_login}; } sub login { - my ($class, $type) = @_; + my ($class, $type) = @_; - return $class->user if $class->user->id; + return $class->user if $class->user->id; - my $authorizer = new Bugzilla::Auth(); - $type = LOGIN_REQUIRED if $class->cgi->param('GoAheadAndLogIn'); + my $authorizer = new Bugzilla::Auth(); + $type = LOGIN_REQUIRED if $class->cgi->param('GoAheadAndLogIn'); - if (!defined $type || $type == LOGIN_NORMAL) { - $type = $class->params->{'requirelogin'} ? LOGIN_REQUIRED : LOGIN_NORMAL; - } + if (!defined $type || $type == LOGIN_NORMAL) { + $type = $class->params->{'requirelogin'} ? LOGIN_REQUIRED : LOGIN_NORMAL; + } + + # Allow templates to know that we're in a page that always requires + # login. + if ($type == LOGIN_REQUIRED) { + $class->request_cache->{page_requires_login} = 1; + } - # Allow templates to know that we're in a page that always requires - # login. - if ($type == LOGIN_REQUIRED) { - $class->request_cache->{page_requires_login} = 1; + my $authenticated_user = $authorizer->login($type); + + # At this point, we now know if a real person is logged in. + # We must now check to see if an sudo session is in progress. + # For a session to be in progress, the following must be true: + # 1: There must be a logged in user + # 2: That user must be in the 'bz_sudoer' group + # 3: There must be a valid value in the 'sudo' cookie + # 4: A Bugzilla::User object must exist for the given cookie value + # 5: That user must NOT be in the 'bz_sudo_protect' group + my $token = $class->cgi->cookie('sudo'); + if (defined $authenticated_user && $token) { + my ($user_id, $date, $sudo_target_id) = Bugzilla::Token::GetTokenData($token); + if (!$user_id + || $user_id != $authenticated_user->id + || !detaint_natural($sudo_target_id) + || (time() - str2time($date) > MAX_SUDO_TOKEN_AGE)) + { + $class->cgi->remove_cookie('sudo'); + ThrowUserError('sudo_invalid_cookie'); } - my $authenticated_user = $authorizer->login($type); - - # At this point, we now know if a real person is logged in. - # We must now check to see if an sudo session is in progress. - # For a session to be in progress, the following must be true: - # 1: There must be a logged in user - # 2: That user must be in the 'bz_sudoer' group - # 3: There must be a valid value in the 'sudo' cookie - # 4: A Bugzilla::User object must exist for the given cookie value - # 5: That user must NOT be in the 'bz_sudo_protect' group - my $token = $class->cgi->cookie('sudo'); - if (defined $authenticated_user && $token) { - my ($user_id, $date, $sudo_target_id) = Bugzilla::Token::GetTokenData($token); - if (!$user_id - || $user_id != $authenticated_user->id - || !detaint_natural($sudo_target_id) - || (time() - str2time($date) > MAX_SUDO_TOKEN_AGE)) - { - $class->cgi->remove_cookie('sudo'); - ThrowUserError('sudo_invalid_cookie'); - } - - my $sudo_target = new Bugzilla::User($sudo_target_id); - if ($authenticated_user->in_group('bz_sudoers') - && defined $sudo_target - && !$sudo_target->in_group('bz_sudo_protect')) - { - $class->set_user($sudo_target); - $class->request_cache->{sudoer} = $authenticated_user; - # And make sure that both users have the same Auth object, - # since we never call Auth::login for the sudo target. - $sudo_target->set_authorizer($authenticated_user->authorizer); - - # NOTE: If you want to do any special logging, do it here. - } - else { - delete_token($token); - $class->cgi->remove_cookie('sudo'); - ThrowUserError('sudo_illegal_action', { sudoer => $authenticated_user, - target_user => $sudo_target }); - } + my $sudo_target = new Bugzilla::User($sudo_target_id); + if ( $authenticated_user->in_group('bz_sudoers') + && defined $sudo_target + && !$sudo_target->in_group('bz_sudo_protect')) + { + $class->set_user($sudo_target); + $class->request_cache->{sudoer} = $authenticated_user; + + # And make sure that both users have the same Auth object, + # since we never call Auth::login for the sudo target. + $sudo_target->set_authorizer($authenticated_user->authorizer); + + # NOTE: If you want to do any special logging, do it here. } else { - $class->set_user($authenticated_user); + delete_token($token); + $class->cgi->remove_cookie('sudo'); + ThrowUserError('sudo_illegal_action', + {sudoer => $authenticated_user, target_user => $sudo_target}); } + } + else { + $class->set_user($authenticated_user); + } - if ($class->sudoer) { - $class->sudoer->update_last_seen_date(); - } else { - $class->user->update_last_seen_date(); - } + if ($class->sudoer) { + $class->sudoer->update_last_seen_date(); + } + else { + $class->user->update_last_seen_date(); + } - return $class->user; + return $class->user; } sub logout { - my ($class, $option) = @_; + my ($class, $option) = @_; - # If we're not logged in, go away - return unless $class->user->id; + # If we're not logged in, go away + return unless $class->user->id; - $option = LOGOUT_CURRENT unless defined $option; - Bugzilla::Auth::Persist::Cookie->logout({type => $option}); - $class->logout_request() unless $option eq LOGOUT_KEEP_CURRENT; + $option = LOGOUT_CURRENT unless defined $option; + Bugzilla::Auth::Persist::Cookie->logout({type => $option}); + $class->logout_request() unless $option eq LOGOUT_KEEP_CURRENT; } sub logout_user { - my ($class, $user) = @_; - # When we're logging out another user we leave cookies alone, and - # therefore avoid calling Bugzilla->logout() directly. - Bugzilla::Auth::Persist::Cookie->logout({user => $user}); + my ($class, $user) = @_; + + # When we're logging out another user we leave cookies alone, and + # therefore avoid calling Bugzilla->logout() directly. + Bugzilla::Auth::Persist::Cookie->logout({user => $user}); } # just a compatibility front-end to logout_user that gets a user by id sub logout_user_by_id { - my ($class, $id) = @_; - my $user = new Bugzilla::User($id); - $class->logout_user($user); + my ($class, $id) = @_; + my $user = new Bugzilla::User($id); + $class->logout_user($user); } # hack that invalidates credentials for a single request sub logout_request { - my $class = shift; - delete $class->request_cache->{user}; - delete $class->request_cache->{sudoer}; - # We can't delete from $cgi->cookie, so logincookie data will remain - # there. Don't rely on it: use Bugzilla->user->login instead! + my $class = shift; + delete $class->request_cache->{user}; + delete $class->request_cache->{sudoer}; + + # We can't delete from $cgi->cookie, so logincookie data will remain + # there. Don't rely on it: use Bugzilla->user->login instead! } sub job_queue { - require Bugzilla::JobQueue; - return $_[0]->request_cache->{job_queue} ||= Bugzilla::JobQueue->new(); + require Bugzilla::JobQueue; + return $_[0]->request_cache->{job_queue} ||= Bugzilla::JobQueue->new(); } sub dbh { - # If we're not connected, then we must want the main db - return $_[0]->request_cache->{dbh} ||= $_[0]->dbh_main; + + # If we're not connected, then we must want the main db + return $_[0]->request_cache->{dbh} ||= $_[0]->dbh_main; } sub dbh_main { - return $_[0]->request_cache->{dbh_main} ||= Bugzilla::DB::connect_main(); + return $_[0]->request_cache->{dbh_main} ||= Bugzilla::DB::connect_main(); } sub languages { - return Bugzilla::Install::Util::supported_languages(); + return Bugzilla::Install::Util::supported_languages(); } sub current_language { - return $_[0]->request_cache->{current_language} ||= (include_languages())[0]; + return $_[0]->request_cache->{current_language} ||= (include_languages())[0]; } sub error_mode { - my ($class, $newval) = @_; - if (defined $newval) { - $class->request_cache->{error_mode} = $newval; - } + my ($class, $newval) = @_; + if (defined $newval) { + $class->request_cache->{error_mode} = $newval; + } - # XXX - Once we require Perl 5.10.1, this test can be replaced by //. - if (exists $class->request_cache->{error_mode}) { - return $class->request_cache->{error_mode}; - } - else { - return (i_am_cgi() ? ERROR_MODE_WEBPAGE : ERROR_MODE_DIE); - } + # XXX - Once we require Perl 5.10.1, this test can be replaced by //. + if (exists $class->request_cache->{error_mode}) { + return $class->request_cache->{error_mode}; + } + else { + return (i_am_cgi() ? ERROR_MODE_WEBPAGE : ERROR_MODE_DIE); + } } # This is used only by Bugzilla::Error to throw errors. sub _json_server { - my ($class, $newval) = @_; - if (defined $newval) { - $class->request_cache->{_json_server} = $newval; - } - return $class->request_cache->{_json_server}; + my ($class, $newval) = @_; + if (defined $newval) { + $class->request_cache->{_json_server} = $newval; + } + return $class->request_cache->{_json_server}; } sub usage_mode { - my ($class, $newval) = @_; - if (defined $newval) { - if ($newval == USAGE_MODE_BROWSER) { - $class->error_mode(ERROR_MODE_WEBPAGE); - } - elsif ($newval == USAGE_MODE_CMDLINE) { - $class->error_mode(ERROR_MODE_DIE); - } - elsif ($newval == USAGE_MODE_XMLRPC) { - $class->error_mode(ERROR_MODE_DIE_SOAP_FAULT); - } - elsif ($newval == USAGE_MODE_JSON) { - $class->error_mode(ERROR_MODE_JSON_RPC); - } - elsif ($newval == USAGE_MODE_EMAIL) { - $class->error_mode(ERROR_MODE_DIE); - } - elsif ($newval == USAGE_MODE_TEST) { - $class->error_mode(ERROR_MODE_TEST); - } - elsif ($newval == USAGE_MODE_REST) { - $class->error_mode(ERROR_MODE_REST); - } - else { - ThrowCodeError('usage_mode_invalid', - {'invalid_usage_mode', $newval}); - } - $class->request_cache->{usage_mode} = $newval; + my ($class, $newval) = @_; + if (defined $newval) { + if ($newval == USAGE_MODE_BROWSER) { + $class->error_mode(ERROR_MODE_WEBPAGE); } - - # XXX - Once we require Perl 5.10.1, this test can be replaced by //. - if (exists $class->request_cache->{usage_mode}) { - return $class->request_cache->{usage_mode}; + elsif ($newval == USAGE_MODE_CMDLINE) { + $class->error_mode(ERROR_MODE_DIE); + } + elsif ($newval == USAGE_MODE_XMLRPC) { + $class->error_mode(ERROR_MODE_DIE_SOAP_FAULT); + } + elsif ($newval == USAGE_MODE_JSON) { + $class->error_mode(ERROR_MODE_JSON_RPC); + } + elsif ($newval == USAGE_MODE_EMAIL) { + $class->error_mode(ERROR_MODE_DIE); + } + elsif ($newval == USAGE_MODE_TEST) { + $class->error_mode(ERROR_MODE_TEST); + } + elsif ($newval == USAGE_MODE_REST) { + $class->error_mode(ERROR_MODE_REST); } else { - return (i_am_cgi()? USAGE_MODE_BROWSER : USAGE_MODE_CMDLINE); + ThrowCodeError('usage_mode_invalid', {'invalid_usage_mode', $newval}); } + $class->request_cache->{usage_mode} = $newval; + } + + # XXX - Once we require Perl 5.10.1, this test can be replaced by //. + if (exists $class->request_cache->{usage_mode}) { + return $class->request_cache->{usage_mode}; + } + else { + return (i_am_cgi() ? USAGE_MODE_BROWSER : USAGE_MODE_CMDLINE); + } } sub installation_mode { - my ($class, $newval) = @_; - ($class->request_cache->{installation_mode} = $newval) if defined $newval; - return $class->request_cache->{installation_mode} - || INSTALLATION_MODE_INTERACTIVE; + my ($class, $newval) = @_; + ($class->request_cache->{installation_mode} = $newval) if defined $newval; + return $class->request_cache->{installation_mode} + || INSTALLATION_MODE_INTERACTIVE; } sub installation_answers { - my ($class, $filename) = @_; - if ($filename) { - my $s = new Safe; - $s->rdo($filename); + my ($class, $filename) = @_; + if ($filename) { + my $s = new Safe; + $s->rdo($filename); - die "Error reading $filename: $!" if $!; - die "Error evaluating $filename: $@" if $@; + die "Error reading $filename: $!" if $!; + die "Error evaluating $filename: $@" if $@; - # Now read the param back out from the sandbox - $class->request_cache->{installation_answers} = $s->varglob('answer'); - } - return $class->request_cache->{installation_answers} || {}; + # Now read the param back out from the sandbox + $class->request_cache->{installation_answers} = $s->varglob('answer'); + } + return $class->request_cache->{installation_answers} || {}; } sub switch_to_shadow_db { - my $class = shift; - - if (!$class->request_cache->{dbh_shadow}) { - if ($class->params->{'shadowdb'}) { - $class->request_cache->{dbh_shadow} = Bugzilla::DB::connect_shadow(); - } else { - $class->request_cache->{dbh_shadow} = $class->dbh_main; - } + my $class = shift; + + if (!$class->request_cache->{dbh_shadow}) { + if ($class->params->{'shadowdb'}) { + $class->request_cache->{dbh_shadow} = Bugzilla::DB::connect_shadow(); } + else { + $class->request_cache->{dbh_shadow} = $class->dbh_main; + } + } - $class->request_cache->{dbh} = $class->request_cache->{dbh_shadow}; - # we have to return $class->dbh instead of {dbh} as - # {dbh_shadow} may be undefined if no shadow DB is used - # and no connection to the main DB has been established yet. - return $class->dbh; + $class->request_cache->{dbh} = $class->request_cache->{dbh_shadow}; + + # we have to return $class->dbh instead of {dbh} as + # {dbh_shadow} may be undefined if no shadow DB is used + # and no connection to the main DB has been established yet. + return $class->dbh; } sub switch_to_main_db { - my $class = shift; + my $class = shift; - $class->request_cache->{dbh} = $class->dbh_main; - return $class->dbh_main; + $class->request_cache->{dbh} = $class->dbh_main; + return $class->dbh_main; } sub is_shadow_db { - my $class = shift; - return $class->request_cache->{dbh} != $class->dbh_main; + my $class = shift; + return $class->request_cache->{dbh} != $class->dbh_main; } sub fields { - my ($class, $criteria) = @_; - $criteria ||= {}; - my $cache = $class->request_cache; - - # We create an advanced cache for fields by type, so that we - # can avoid going back to the database for every fields() call. - # (And most of our fields() calls are for getting fields by type.) - # - # We also cache fields by name, because calling $field->name a few - # million times can be slow in calling code, but if we just do it - # once here, that makes things a lot faster for callers. - if (!defined $cache->{fields}) { - my @all_fields = Bugzilla::Field->get_all; - my (%by_name, %by_type); - foreach my $field (@all_fields) { - my $name = $field->name; - $by_type{$field->type}->{$name} = $field; - $by_name{$name} = $field; - } - $cache->{fields} = { by_type => \%by_type, by_name => \%by_name }; + my ($class, $criteria) = @_; + $criteria ||= {}; + my $cache = $class->request_cache; + + # We create an advanced cache for fields by type, so that we + # can avoid going back to the database for every fields() call. + # (And most of our fields() calls are for getting fields by type.) + # + # We also cache fields by name, because calling $field->name a few + # million times can be slow in calling code, but if we just do it + # once here, that makes things a lot faster for callers. + if (!defined $cache->{fields}) { + my @all_fields = Bugzilla::Field->get_all; + my (%by_name, %by_type); + foreach my $field (@all_fields) { + my $name = $field->name; + $by_type{$field->type}->{$name} = $field; + $by_name{$name} = $field; } + $cache->{fields} = {by_type => \%by_type, by_name => \%by_name}; + } - my $fields = $cache->{fields}; - my %requested; - if (my $types = delete $criteria->{type}) { - $types = ref($types) ? $types : [$types]; - %requested = map { %{ $fields->{by_type}->{$_} || {} } } @$types; - } - else { - %requested = %{ $fields->{by_name} }; - } + my $fields = $cache->{fields}; + my %requested; + if (my $types = delete $criteria->{type}) { + $types = ref($types) ? $types : [$types]; + %requested = map { %{$fields->{by_type}->{$_} || {}} } @$types; + } + else { + %requested = %{$fields->{by_name}}; + } - my $do_by_name = delete $criteria->{by_name}; + my $do_by_name = delete $criteria->{by_name}; - # Filtering before returning the fields based on - # the criterias. - foreach my $filter (keys %$criteria) { - foreach my $field (keys %requested) { - if ($requested{$field}->$filter != $criteria->{$filter}) { - delete $requested{$field}; - } - } + # Filtering before returning the fields based on + # the criterias. + foreach my $filter (keys %$criteria) { + foreach my $field (keys %requested) { + if ($requested{$field}->$filter != $criteria->{$filter}) { + delete $requested{$field}; + } } + } - return $do_by_name ? \%requested - : [sort { $a->sortkey <=> $b->sortkey || $a->name cmp $b->name } values %requested]; + return $do_by_name + ? \%requested + : [sort { $a->sortkey <=> $b->sortkey || $a->name cmp $b->name } + values %requested]; } sub active_custom_fields { - my $class = shift; - if (!exists $class->request_cache->{active_custom_fields}) { - $class->request_cache->{active_custom_fields} = - Bugzilla::Field->match({ custom => 1, obsolete => 0 }); - } - return @{$class->request_cache->{active_custom_fields}}; + my $class = shift; + if (!exists $class->request_cache->{active_custom_fields}) { + $class->request_cache->{active_custom_fields} + = Bugzilla::Field->match({custom => 1, obsolete => 0}); + } + return @{$class->request_cache->{active_custom_fields}}; } sub has_flags { - my $class = shift; + my $class = shift; - if (!defined $class->request_cache->{has_flags}) { - $class->request_cache->{has_flags} = Bugzilla::Flag->any_exist; - } - return $class->request_cache->{has_flags}; + if (!defined $class->request_cache->{has_flags}) { + $class->request_cache->{has_flags} = Bugzilla::Flag->any_exist; + } + return $class->request_cache->{has_flags}; } sub local_timezone { - return $_[0]->process_cache->{local_timezone} - ||= DateTime::TimeZone->new(name => 'local'); + return $_[0]->process_cache->{local_timezone} + ||= DateTime::TimeZone->new(name => 'local'); } # This creates the request cache for non-mod_perl installations. @@ -631,27 +651,28 @@ sub local_timezone { our $_request_cache = $Bugzilla::Install::Util::_cache; sub request_cache { - if ($ENV{MOD_PERL}) { - require Apache2::RequestUtil; - # Sometimes (for example, during mod_perl.pl), the request - # object isn't available, and we should use $_request_cache instead. - my $request = eval { Apache2::RequestUtil->request }; - return $_request_cache if !$request; - return $request->pnotes(); - } - return $_request_cache; + if ($ENV{MOD_PERL}) { + require Apache2::RequestUtil; + + # Sometimes (for example, during mod_perl.pl), the request + # object isn't available, and we should use $_request_cache instead. + my $request = eval { Apache2::RequestUtil->request }; + return $_request_cache if !$request; + return $request->pnotes(); + } + return $_request_cache; } sub clear_request_cache { - $_request_cache = {}; - if ($ENV{MOD_PERL}) { - require Apache2::RequestUtil; - my $request = eval { Apache2::RequestUtil->request }; - if ($request) { - my $pnotes = $request->pnotes; - delete @$pnotes{(keys %$pnotes)}; - } + $_request_cache = {}; + if ($ENV{MOD_PERL}) { + require Apache2::RequestUtil; + my $request = eval { Apache2::RequestUtil->request }; + if ($request) { + my $pnotes = $request->pnotes; + delete @$pnotes{(keys %$pnotes)}; } + } } # This is a per-process cache. Under mod_cgi it's identical to the @@ -660,13 +681,13 @@ sub clear_request_cache { our $_process_cache = {}; sub process_cache { - return $_process_cache; + return $_process_cache; } # This is a memcached wrapper, which provides cross-process and cross-system # caching. sub memcached { - return $_[0]->process_cache->{memcached} ||= Bugzilla::Memcached->_new(); + return $_[0]->process_cache->{memcached} ||= Bugzilla::Memcached->_new(); } # Private methods @@ -674,28 +695,29 @@ sub memcached { # Per-process cleanup. Note that this is a plain subroutine, not a method, # so we don't have $class available. sub _cleanup { - my $cache = Bugzilla->request_cache; - my $main = $cache->{dbh_main}; - my $shadow = $cache->{dbh_shadow}; - foreach my $dbh ($main, $shadow) { - next if !$dbh; - $dbh->bz_rollback_transaction() if $dbh->bz_in_transaction; - $dbh->disconnect; - } - my $smtp = $cache->{smtp}; - $smtp->disconnect if $smtp; - clear_request_cache(); - - # These are both set by CGI.pm but need to be undone so that - # Apache can actually shut down its children if it needs to. - foreach my $signal (qw(TERM PIPE)) { - $SIG{$signal} = 'DEFAULT' if $SIG{$signal} && $SIG{$signal} eq 'IGNORE'; - } + my $cache = Bugzilla->request_cache; + my $main = $cache->{dbh_main}; + my $shadow = $cache->{dbh_shadow}; + foreach my $dbh ($main, $shadow) { + next if !$dbh; + $dbh->bz_rollback_transaction() if $dbh->bz_in_transaction; + $dbh->disconnect; + } + my $smtp = $cache->{smtp}; + $smtp->disconnect if $smtp; + clear_request_cache(); + + # These are both set by CGI.pm but need to be undone so that + # Apache can actually shut down its children if it needs to. + foreach my $signal (qw(TERM PIPE)) { + $SIG{$signal} = 'DEFAULT' if $SIG{$signal} && $SIG{$signal} eq 'IGNORE'; + } } sub END { - # Bugzilla.pm cannot compile in mod_perl.pl if this runs. - _cleanup() unless $ENV{MOD_PERL}; + + # Bugzilla.pm cannot compile in mod_perl.pl if this runs. + _cleanup() unless $ENV{MOD_PERL}; } init_page() if !$ENV{MOD_PERL}; diff --git a/Bugzilla/Attachment.pm b/Bugzilla/Attachment.pm index 33183797b..326534dd8 100644 --- a/Bugzilla/Attachment.pm +++ b/Bugzilla/Attachment.pm @@ -57,55 +57,51 @@ use parent qw(Bugzilla::Object); use constant DB_TABLE => 'attachments'; use constant ID_FIELD => 'attach_id'; use constant LIST_ORDER => ID_FIELD; + # Attachments are tracked in bugs_activity. use constant AUDIT_CREATES => 0; use constant AUDIT_UPDATES => 0; use constant DB_COLUMNS => qw( - attach_id - bug_id - creation_ts - description - filename - isobsolete - ispatch - isprivate - mimetype - modification_time - submitter_id + attach_id + bug_id + creation_ts + description + filename + isobsolete + ispatch + isprivate + mimetype + modification_time + submitter_id ); -use constant REQUIRED_FIELD_MAP => { - bug_id => 'bug', -}; +use constant REQUIRED_FIELD_MAP => {bug_id => 'bug',}; use constant EXTRA_REQUIRED_FIELDS => qw(data); use constant UPDATE_COLUMNS => qw( - description - filename - isobsolete - ispatch - isprivate - mimetype + description + filename + isobsolete + ispatch + isprivate + mimetype ); use constant VALIDATORS => { - bug => \&_check_bug, - description => \&_check_description, - filename => \&_check_filename, - ispatch => \&Bugzilla::Object::check_boolean, - isprivate => \&_check_is_private, - mimetype => \&_check_content_type, + bug => \&_check_bug, + description => \&_check_description, + filename => \&_check_filename, + ispatch => \&Bugzilla::Object::check_boolean, + isprivate => \&_check_is_private, + mimetype => \&_check_content_type, }; -use constant VALIDATOR_DEPENDENCIES => { - content_type => ['ispatch'], - mimetype => ['ispatch'], -}; +use constant VALIDATOR_DEPENDENCIES => + {content_type => ['ispatch'], mimetype => ['ispatch'],}; -use constant UPDATE_VALIDATORS => { - isobsolete => \&Bugzilla::Object::check_boolean, -}; +use constant UPDATE_VALIDATORS => + {isobsolete => \&Bugzilla::Object::check_boolean,}; ############################### #### Accessors ###### @@ -126,7 +122,7 @@ the ID of the bug to which the attachment is attached =cut sub bug_id { - return $_[0]->{bug_id}; + return $_[0]->{bug_id}; } =over @@ -140,8 +136,8 @@ the bug object to which the attachment is attached =cut sub bug { - require Bugzilla::Bug; - return $_[0]->{bug} //= Bugzilla::Bug->new({ id => $_[0]->bug_id, cache => 1 }); + require Bugzilla::Bug; + return $_[0]->{bug} //= Bugzilla::Bug->new({id => $_[0]->bug_id, cache => 1}); } =over @@ -155,7 +151,7 @@ user-provided text describing the attachment =cut sub description { - return $_[0]->{description}; + return $_[0]->{description}; } =over @@ -169,7 +165,7 @@ the attachment's MIME media type =cut sub contenttype { - return $_[0]->{mimetype}; + return $_[0]->{mimetype}; } =over @@ -183,8 +179,8 @@ the user who attached the attachment =cut sub attacher { - return $_[0]->{attacher} - //= new Bugzilla::User({ id => $_[0]->{submitter_id}, cache => 1 }); + return $_[0]->{attacher} + //= new Bugzilla::User({id => $_[0]->{submitter_id}, cache => 1}); } =over @@ -198,7 +194,7 @@ the date and time on which the attacher attached the attachment =cut sub attached { - return $_[0]->{creation_ts}; + return $_[0]->{creation_ts}; } =over @@ -212,7 +208,7 @@ the date and time on which the attachment was last modified. =cut sub modification_time { - return $_[0]->{modification_time}; + return $_[0]->{modification_time}; } =over @@ -226,7 +222,7 @@ the name of the file the attacher attached =cut sub filename { - return $_[0]->{filename}; + return $_[0]->{filename}; } =over @@ -240,7 +236,7 @@ whether or not the attachment is a patch =cut sub ispatch { - return $_[0]->{ispatch}; + return $_[0]->{ispatch}; } =over @@ -254,7 +250,7 @@ whether or not the attachment is obsolete =cut sub isobsolete { - return $_[0]->{isobsolete}; + return $_[0]->{isobsolete}; } =over @@ -268,7 +264,7 @@ whether or not the attachment is private =cut sub isprivate { - return $_[0]->{isprivate}; + return $_[0]->{isprivate}; } =over @@ -285,23 +281,24 @@ matches, because this will return a value even if it's matched by the generic =cut sub is_viewable { - my $contenttype = $_[0]->contenttype; - my $cgi = Bugzilla->cgi; + my $contenttype = $_[0]->contenttype; + my $cgi = Bugzilla->cgi; - # We assume we can view all text and image types. - return 1 if ($contenttype =~ /^(text|image)\//); + # We assume we can view all text and image types. + return 1 if ($contenttype =~ /^(text|image)\//); - # Mozilla can view XUL. Note the trailing slash on the Gecko detection to - # avoid sending XUL to Safari. - return 1 if (($contenttype =~ /^application\/vnd\.mozilla\./) - && ($cgi->user_agent() =~ /Gecko\//)); + # Mozilla can view XUL. Note the trailing slash on the Gecko detection to + # avoid sending XUL to Safari. + return 1 + if (($contenttype =~ /^application\/vnd\.mozilla\./) + && ($cgi->user_agent() =~ /Gecko\//)); - # If it's not one of the above types, we check the Accept: header for any - # types mentioned explicitly. - my $accept = join(",", $cgi->Accept()); - return 1 if ($accept =~ /^(.*,)?\Q$contenttype\E(,.*)?$/); + # If it's not one of the above types, we check the Accept: header for any + # types mentioned explicitly. + my $accept = join(",", $cgi->Accept()); + return 1 if ($accept =~ /^(.*,)?\Q$contenttype\E(,.*)?$/); - return 0; + return 0; } =over @@ -315,28 +312,29 @@ the content of the attachment =cut sub data { - my $self = shift; - return $self->{data} if exists $self->{data}; + my $self = shift; + return $self->{data} if exists $self->{data}; - # First try to get the attachment data from the database. - ($self->{data}) = Bugzilla->dbh->selectrow_array("SELECT thedata + # First try to get the attachment data from the database. + ($self->{data}) = Bugzilla->dbh->selectrow_array( + "SELECT thedata FROM attach_data - WHERE id = ?", - undef, - $self->id); - - # 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())) { - local $/; - binmode AH; - $self->{data} = ; - close(AH); - } + WHERE id = ?", undef, + $self->id + ); + + # 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())) { + local $/; + binmode AH; + $self->{data} = ; + close(AH); } + } - return $self->{data}; + return $self->{data}; } =over @@ -358,37 +356,37 @@ the length (in bytes) of the attachment content # LENGTH() function or stat()ing the file instead. I've left it in for now. sub datasize { - my $self = shift; - return $self->{datasize} if defined $self->{datasize}; + my $self = shift; + return $self->{datasize} if defined $self->{datasize}; - # If we have already retrieved the data, return its size. - return length($self->{data}) if exists $self->{data}; + # If we have already retrieved the data, return its size. + return length($self->{data}) if exists $self->{data}; - $self->{datasize} = - Bugzilla->dbh->selectrow_array("SELECT LENGTH(thedata) + $self->{datasize} = Bugzilla->dbh->selectrow_array( + "SELECT LENGTH(thedata) FROM attach_data - WHERE id = ?", - undef, $self->id) || 0; - - # If there's no attachment data in the database, either the attachment - # 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())) { - binmode AH; - $self->{datasize} = (stat(AH))[7]; - close(AH); - } + WHERE id = ?", undef, $self->id + ) || 0; + + # If there's no attachment data in the database, either the attachment + # 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())) { + binmode AH; + $self->{datasize} = (stat(AH))[7]; + close(AH); } + } - return $self->{datasize}; + return $self->{datasize}; } sub _get_local_filename { - my $self = shift; - my $hash = ($self->id % 100) + 100; - $hash =~ s/.*(\d\d)$/group.$1/; - return bz_locations()->{'attachdir'} . "/$hash/attachment." . $self->id; + my $self = shift; + my $hash = ($self->id % 100) + 100; + $hash =~ s/.*(\d\d)$/group.$1/; + return bz_locations()->{'attachdir'} . "/$hash/attachment." . $self->id; } =over @@ -402,8 +400,9 @@ flags that have been set on the attachment =cut sub flags { - # Don't cache it as it must be in sync with ->flag_types. - return $_[0]->{flags} = [map { @{$_->{flags}} } @{$_[0]->flag_types}]; + + # Don't cache it as it must be in sync with ->flag_types. + return $_[0]->{flags} = [map { @{$_->{flags}} } @{$_[0]->flag_types}]; } =over @@ -418,202 +417,216 @@ already set, grouped by flag type. =cut sub flag_types { - my $self = shift; - return $self->{flag_types} if exists $self->{flag_types}; + my $self = shift; + return $self->{flag_types} if exists $self->{flag_types}; - my $vars = { target_type => 'attachment', - product_id => $self->bug->product_id, - component_id => $self->bug->component_id, - attach_id => $self->id }; + my $vars = { + target_type => 'attachment', + product_id => $self->bug->product_id, + component_id => $self->bug->component_id, + attach_id => $self->id + }; - return $self->{flag_types} = Bugzilla::Flag->_flag_types($vars); + return $self->{flag_types} = Bugzilla::Flag->_flag_types($vars); } ############################### #### Validators ###### ############################### -sub set_content_type { $_[0]->set('mimetype', $_[1]); } +sub set_content_type { $_[0]->set('mimetype', $_[1]); } sub set_description { $_[0]->set('description', $_[1]); } -sub set_filename { $_[0]->set('filename', $_[1]); } -sub set_is_patch { $_[0]->set('ispatch', $_[1]); } -sub set_is_private { $_[0]->set('isprivate', $_[1]); } - -sub set_is_obsolete { - my ($self, $obsolete) = @_; - - my $old = $self->isobsolete; - $self->set('isobsolete', $obsolete); - my $new = $self->isobsolete; - - # If the attachment is being marked as obsolete, cancel pending requests. - if ($new && $old != $new) { - my @requests = grep { $_->status eq '?' } @{$self->flags}; - return unless scalar @requests; - - my %flag_ids = map { $_->id => 1 } @requests; - foreach my $flagtype (@{$self->flag_types}) { - @{$flagtype->{flags}} = grep { !$flag_ids{$_->id} } @{$flagtype->{flags}}; - } +sub set_filename { $_[0]->set('filename', $_[1]); } +sub set_is_patch { $_[0]->set('ispatch', $_[1]); } +sub set_is_private { $_[0]->set('isprivate', $_[1]); } + +sub set_is_obsolete { + my ($self, $obsolete) = @_; + + my $old = $self->isobsolete; + $self->set('isobsolete', $obsolete); + my $new = $self->isobsolete; + + # If the attachment is being marked as obsolete, cancel pending requests. + if ($new && $old != $new) { + my @requests = grep { $_->status eq '?' } @{$self->flags}; + return unless scalar @requests; + + my %flag_ids = map { $_->id => 1 } @requests; + foreach my $flagtype (@{$self->flag_types}) { + @{$flagtype->{flags}} = grep { !$flag_ids{$_->id} } @{$flagtype->{flags}}; } + } } sub set_flags { - my ($self, $flags, $new_flags) = @_; + my ($self, $flags, $new_flags) = @_; - Bugzilla::Flag->set_flag($self, $_) foreach (@$flags, @$new_flags); + Bugzilla::Flag->set_flag($self, $_) foreach (@$flags, @$new_flags); } sub _check_bug { - my ($invocant, $bug) = @_; - my $user = Bugzilla->user; + my ($invocant, $bug) = @_; + my $user = Bugzilla->user; - $bug = ref $invocant ? $invocant->bug : $bug; + $bug = ref $invocant ? $invocant->bug : $bug; - $bug || ThrowCodeError('param_required', - { function => "$invocant->create", param => 'bug' }); + $bug + || ThrowCodeError('param_required', + {function => "$invocant->create", param => 'bug'}); - ($user->can_see_bug($bug->id) && $user->can_edit_product($bug->product_id)) - || ThrowUserError("illegal_attachment_edit_bug", { bug_id => $bug->id }); + ($user->can_see_bug($bug->id) && $user->can_edit_product($bug->product_id)) + || ThrowUserError("illegal_attachment_edit_bug", {bug_id => $bug->id}); - return $bug; + return $bug; } sub _check_content_type { - my ($invocant, $content_type, undef, $params) = @_; - - my $is_patch = ref($invocant) ? $invocant->ispatch : $params->{ispatch}; - $content_type = 'text/plain' if $is_patch; - $content_type = clean_text($content_type); - # The subsets below cover all existing MIME types and charsets registered by IANA. - # (MIME type: RFC 2045 section 5.1; charset: RFC 2278 section 3.3) - my $legal_types = join('|', LEGAL_CONTENT_TYPES); - if (!$content_type - || $content_type !~ /^($legal_types)\/[a-z0-9_\-\+\.]+(;\s*charset=[a-z0-9_\-\+]+)?$/i) - { - ThrowUserError("invalid_content_type", { contenttype => $content_type }); + my ($invocant, $content_type, undef, $params) = @_; + + my $is_patch = ref($invocant) ? $invocant->ispatch : $params->{ispatch}; + $content_type = 'text/plain' if $is_patch; + $content_type = clean_text($content_type); + +# The subsets below cover all existing MIME types and charsets registered by IANA. +# (MIME type: RFC 2045 section 5.1; charset: RFC 2278 section 3.3) + my $legal_types = join('|', LEGAL_CONTENT_TYPES); + if (!$content_type + || $content_type + !~ /^($legal_types)\/[a-z0-9_\-\+\.]+(;\s*charset=[a-z0-9_\-\+]+)?$/i) + { + ThrowUserError("invalid_content_type", {contenttype => $content_type}); + } + trick_taint($content_type); + + # $ENV{HOME} must be defined when using File::MimeInfo::Magic, + # see https://rt.cpan.org/Public/Bug/Display.html?id=41744. + local $ENV{HOME} = $ENV{HOME} || File::Spec->rootdir(); + + # If we have autodetected application/octet-stream from the Content-Type + # header, let's have a better go using a sniffer if available. + if ( defined Bugzilla->input_params->{contenttypemethod} + && Bugzilla->input_params->{contenttypemethod} eq 'autodetect' + && $content_type eq 'application/octet-stream' + && Bugzilla->feature('typesniffer')) + { + import File::MimeInfo::Magic qw(mimetype); + require IO::Scalar; + + # data is either a filehandle, or the data itself. + my $fh = $params->{data}; + if (!ref($fh)) { + $fh = new IO::Scalar \$fh; } - trick_taint($content_type); - - # $ENV{HOME} must be defined when using File::MimeInfo::Magic, - # see https://rt.cpan.org/Public/Bug/Display.html?id=41744. - local $ENV{HOME} = $ENV{HOME} || File::Spec->rootdir(); - - # If we have autodetected application/octet-stream from the Content-Type - # header, let's have a better go using a sniffer if available. - if (defined Bugzilla->input_params->{contenttypemethod} - && Bugzilla->input_params->{contenttypemethod} eq 'autodetect' - && $content_type eq 'application/octet-stream' - && Bugzilla->feature('typesniffer')) - { - import File::MimeInfo::Magic qw(mimetype); - require IO::Scalar; - - # data is either a filehandle, or the data itself. - my $fh = $params->{data}; - if (!ref($fh)) { - $fh = new IO::Scalar \$fh; - } - elsif (!$fh->isa('IO::Handle')) { - # CGI.pm sends us an Fh that isn't actually an IO::Handle, but - # has a method for getting an actual handle out of it. - $fh = $fh->handle; - # ->handle returns an literal IO::Handle, even though the - # underlying object is a file. So we rebless it to be a proper - # IO::File object so that we can call ->seek on it and so on. - # Just in case CGI.pm fixes this some day, we check ->isa first. - if (!$fh->isa('IO::File')) { - bless $fh, 'IO::File'; - } - } - - my $mimetype = mimetype($fh); - $fh->seek(0, 0); - $content_type = $mimetype if $mimetype; + elsif (!$fh->isa('IO::Handle')) { + + # CGI.pm sends us an Fh that isn't actually an IO::Handle, but + # has a method for getting an actual handle out of it. + $fh = $fh->handle; + + # ->handle returns an literal IO::Handle, even though the + # underlying object is a file. So we rebless it to be a proper + # IO::File object so that we can call ->seek on it and so on. + # Just in case CGI.pm fixes this some day, we check ->isa first. + if (!$fh->isa('IO::File')) { + bless $fh, 'IO::File'; + } } - # Make sure patches are viewable in the browser - if (!ref($invocant) - && defined Bugzilla->input_params->{contenttypemethod} - && Bugzilla->input_params->{contenttypemethod} eq 'autodetect' - && $content_type =~ m{text/x-(?:diff|patch)}) - { - $params->{ispatch} = 1; - $content_type = 'text/plain'; - } - - return $content_type; + my $mimetype = mimetype($fh); + $fh->seek(0, 0); + $content_type = $mimetype if $mimetype; + } + + # Make sure patches are viewable in the browser + if (!ref($invocant) + && defined Bugzilla->input_params->{contenttypemethod} + && Bugzilla->input_params->{contenttypemethod} eq 'autodetect' + && $content_type =~ m{text/x-(?:diff|patch)}) + { + $params->{ispatch} = 1; + $content_type = 'text/plain'; + } + + return $content_type; } sub _check_data { - my ($invocant, $params) = @_; + my ($invocant, $params) = @_; - my $data = $params->{data}; - $params->{filesize} = ref $data ? -s $data : length($data); + my $data = $params->{data}; + $params->{filesize} = ref $data ? -s $data : length($data); - Bugzilla::Hook::process('attachment_process_data', { data => \$data, - attributes => $params }); + Bugzilla::Hook::process('attachment_process_data', + {data => \$data, attributes => $params}); - $params->{filesize} || ThrowUserError('zero_length_file'); - # Make sure the attachment does not exceed the maximum permitted size. - my $max_size = max(Bugzilla->params->{'maxlocalattachment'} * 1048576, - Bugzilla->params->{'maxattachmentsize'} * 1024); + $params->{filesize} || ThrowUserError('zero_length_file'); - if ($params->{filesize} > $max_size) { - my $vars = { filesize => sprintf("%.0f", $params->{filesize}/1024) }; - ThrowUserError('file_too_large', $vars); - } - return $data; + # Make sure the attachment does not exceed the maximum permitted size. + my $max_size = max( + Bugzilla->params->{'maxlocalattachment'} * 1048576, + Bugzilla->params->{'maxattachmentsize'} * 1024 + ); + + if ($params->{filesize} > $max_size) { + my $vars = {filesize => sprintf("%.0f", $params->{filesize} / 1024)}; + ThrowUserError('file_too_large', $vars); + } + return $data; } sub _check_description { - my ($invocant, $description) = @_; + my ($invocant, $description) = @_; - $description = trim($description); - $description || ThrowUserError('missing_attachment_description'); - return $description; + $description = trim($description); + $description || ThrowUserError('missing_attachment_description'); + return $description; } sub _check_filename { - my ($invocant, $filename) = @_; - - $filename = clean_text($filename); - if (!$filename) { - if (ref $invocant) { - ThrowUserError('filename_not_specified'); - } - else { - ThrowUserError('file_not_specified'); - } - } + my ($invocant, $filename) = @_; - # Remove path info (if any) from the file name. The browser should do this - # for us, but some are buggy. This may not work on Mac file names and could - # mess up file names with slashes in them, but them's the breaks. We only - # use this as a hint to users downloading attachments anyway, so it's not - # a big deal if it munges incorrectly occasionally. - $filename =~ s/^.*[\/\\]//; - - # Truncate the filename to MAX_ATTACH_FILENAME_LENGTH characters, counting - # from the end of the string to make sure we keep the filename extension. - $filename = substr($filename, - -&MAX_ATTACH_FILENAME_LENGTH, - MAX_ATTACH_FILENAME_LENGTH); - trick_taint($filename); - - return $filename; + $filename = clean_text($filename); + if (!$filename) { + if (ref $invocant) { + ThrowUserError('filename_not_specified'); + } + else { + ThrowUserError('file_not_specified'); + } + } + + # Remove path info (if any) from the file name. The browser should do this + # for us, but some are buggy. This may not work on Mac file names and could + # mess up file names with slashes in them, but them's the breaks. We only + # use this as a hint to users downloading attachments anyway, so it's not + # a big deal if it munges incorrectly occasionally. + $filename =~ s/^.*[\/\\]//; + + # Truncate the filename to MAX_ATTACH_FILENAME_LENGTH characters, counting + # from the end of the string to make sure we keep the filename extension. + $filename + = substr($filename, -&MAX_ATTACH_FILENAME_LENGTH, MAX_ATTACH_FILENAME_LENGTH); + trick_taint($filename); + + return $filename; } sub _check_is_private { - my ($invocant, $is_private) = @_; - - $is_private = $is_private ? 1 : 0; - if (((!ref $invocant && $is_private) - || (ref $invocant && $invocant->isprivate != $is_private)) - && !Bugzilla->user->is_insider) { - ThrowUserError('user_not_insider'); - } - return $is_private; + my ($invocant, $is_private) = @_; + + $is_private = $is_private ? 1 : 0; + if ( + ( + (!ref $invocant && $is_private) + || (ref $invocant && $invocant->isprivate != $is_private) + ) + && !Bugzilla->user->is_insider + ) + { + ThrowUserError('user_not_insider'); + } + return $is_private; } =pod @@ -635,69 +648,74 @@ Returns: a reference to an array of attachment objects. =cut sub get_attachments_by_bug { - my ($class, $bug, $vars) = @_; - my $user = Bugzilla->user; - my $dbh = Bugzilla->dbh; - - # By default, private attachments are not accessible, unless the user - # is in the insider group or submitted the attachment. - my $and_restriction = ''; - my @values = ($bug->id); - - unless ($user->is_insider) { - $and_restriction = 'AND (isprivate = 0 OR submitter_id = ?)'; - push(@values, $user->id); + my ($class, $bug, $vars) = @_; + my $user = Bugzilla->user; + my $dbh = Bugzilla->dbh; + + # By default, private attachments are not accessible, unless the user + # is in the insider group or submitted the attachment. + my $and_restriction = ''; + my @values = ($bug->id); + + unless ($user->is_insider) { + $and_restriction = 'AND (isprivate = 0 OR submitter_id = ?)'; + push(@values, $user->id); + } + + my $attach_ids = $dbh->selectcol_arrayref( + "SELECT attach_id FROM attachments + WHERE bug_id = ? $and_restriction", + undef, @values + ); + + my $attachments = Bugzilla::Attachment->new_from_list($attach_ids); + $_->{bug} = $bug foreach @$attachments; + + # To avoid $attachment->flags and $attachment->flag_types running SQL queries + # themselves for each attachment listed here, we collect all the data at once and + # populate $attachment->{flag_types} ourselves. We also load all attachers and + # datasizes at once for the same reason. + if ($vars->{preload}) { + + # Preload flag types and flags + my $vars = { + target_type => 'attachment', + product_id => $bug->product_id, + component_id => $bug->component_id, + attach_id => $attach_ids + }; + my $flag_types = Bugzilla::Flag->_flag_types($vars); + + foreach my $attachment (@$attachments) { + $attachment->{flag_types} = []; + my $new_types = dclone($flag_types); + foreach my $new_type (@$new_types) { + $new_type->{flags} + = [grep($_->attach_id == $attachment->id, @{$new_type->{flags}})]; + push(@{$attachment->{flag_types}}, $new_type); + } } - my $attach_ids = $dbh->selectcol_arrayref("SELECT attach_id FROM attachments - WHERE bug_id = ? $and_restriction", - undef, @values); - - my $attachments = Bugzilla::Attachment->new_from_list($attach_ids); - $_->{bug} = $bug foreach @$attachments; - - # To avoid $attachment->flags and $attachment->flag_types running SQL queries - # themselves for each attachment listed here, we collect all the data at once and - # populate $attachment->{flag_types} ourselves. We also load all attachers and - # datasizes at once for the same reason. - if ($vars->{preload}) { - # Preload flag types and flags - my $vars = { target_type => 'attachment', - product_id => $bug->product_id, - component_id => $bug->component_id, - attach_id => $attach_ids }; - my $flag_types = Bugzilla::Flag->_flag_types($vars); - - foreach my $attachment (@$attachments) { - $attachment->{flag_types} = []; - my $new_types = dclone($flag_types); - foreach my $new_type (@$new_types) { - $new_type->{flags} = [ grep($_->attach_id == $attachment->id, - @{ $new_type->{flags} }) ]; - push(@{ $attachment->{flag_types} }, $new_type); - } - } - - # Preload attachers. - my %user_ids = map { $_->{submitter_id} => 1 } @$attachments; - my $users = Bugzilla::User->new_from_list([keys %user_ids]); - my %user_map = map { $_->id => $_ } @$users; - foreach my $attachment (@$attachments) { - $attachment->{attacher} = $user_map{$attachment->{submitter_id}}; - } - - # Preload datasizes. - my $sizes = - $dbh->selectall_hashref('SELECT attach_id, LENGTH(thedata) AS datasize + # Preload attachers. + my %user_ids = map { $_->{submitter_id} => 1 } @$attachments; + my $users = Bugzilla::User->new_from_list([keys %user_ids]); + my %user_map = map { $_->id => $_ } @$users; + foreach my $attachment (@$attachments) { + $attachment->{attacher} = $user_map{$attachment->{submitter_id}}; + } + + # Preload datasizes. + my $sizes = $dbh->selectall_hashref( + 'SELECT attach_id, LENGTH(thedata) AS datasize FROM attachments LEFT JOIN attach_data ON attach_id = id - WHERE bug_id = ?', - 'attach_id', undef, $bug->id); + WHERE bug_id = ?', 'attach_id', undef, $bug->id + ); - # Force the size of attachments not in the DB to be recalculated. - $_->{datasize} = $sizes->{$_->id}->{datasize} || undef foreach @$attachments; - } + # Force the size of attachments not in the DB to be recalculated. + $_->{datasize} = $sizes->{$_->id}->{datasize} || undef foreach @$attachments; + } - return $attachments; + return $attachments; } =pod @@ -716,13 +734,15 @@ Returns: 1 on success, 0 otherwise. =cut sub validate_can_edit { - my $attachment = shift; - my $user = Bugzilla->user; - - # The submitter can edit their attachments. - return ($attachment->attacher->id == $user->id - || ((!$attachment->isprivate || $user->is_insider) - && $user->in_group('editbugs', $attachment->bug->product_id))) ? 1 : 0; + my $attachment = shift; + my $user = Bugzilla->user; + + # The submitter can edit their attachments. + return ( + $attachment->attacher->id == $user->id + || ((!$attachment->isprivate || $user->is_insider) + && $user->in_group('editbugs', $attachment->bug->product_id)) + ) ? 1 : 0; } =item C @@ -741,37 +761,36 @@ Returns: The list of attachment objects to mark as obsolete. =cut sub validate_obsolete { - my ($class, $bug, $list) = @_; + my ($class, $bug, $list) = @_; - # Make sure the attachment id is valid and the user has permissions to view - # the bug to which it is attached. Make sure also that the user can view - # the attachment itself. - my @obsolete_attachments; - foreach my $attachid (@$list) { - my $vars = {}; - $vars->{'attach_id'} = $attachid; + # Make sure the attachment id is valid and the user has permissions to view + # the bug to which it is attached. Make sure also that the user can view + # the attachment itself. + my @obsolete_attachments; + foreach my $attachid (@$list) { + my $vars = {}; + $vars->{'attach_id'} = $attachid; - detaint_natural($attachid) - || ThrowUserError('invalid_attach_id', $vars); + detaint_natural($attachid) || ThrowUserError('invalid_attach_id', $vars); - # Make sure the attachment exists in the database. - my $attachment = new Bugzilla::Attachment($attachid) - || ThrowUserError('invalid_attach_id', $vars); + # Make sure the attachment exists in the database. + my $attachment = new Bugzilla::Attachment($attachid) + || ThrowUserError('invalid_attach_id', $vars); - # Check that the user can view and edit this attachment. - $attachment->validate_can_edit - || ThrowUserError('illegal_attachment_edit', { attach_id => $attachment->id }); + # Check that the user can view and edit this attachment. + $attachment->validate_can_edit + || ThrowUserError('illegal_attachment_edit', {attach_id => $attachment->id}); - if ($attachment->bug_id != $bug->bug_id) { - $vars->{'my_bug_id'} = $bug->bug_id; - ThrowUserError('mismatched_bug_ids_on_obsolete', $vars); - } + if ($attachment->bug_id != $bug->bug_id) { + $vars->{'my_bug_id'} = $bug->bug_id; + ThrowUserError('mismatched_bug_ids_on_obsolete', $vars); + } - next if $attachment->isobsolete; + next if $attachment->isobsolete; - push(@obsolete_attachments, $attachment); - } - return @obsolete_attachments; + push(@obsolete_attachments, $attachment); + } + return @obsolete_attachments; } ############################### @@ -806,112 +825,119 @@ Returns: The new attachment object. =cut sub create { - my $class = shift; - my $dbh = Bugzilla->dbh; - - $class->check_required_create_fields(@_); - my $params = $class->run_create_validators(@_); - - # Extract everything which is not a valid column name. - my $bug = delete $params->{bug}; - $params->{bug_id} = $bug->id; - my $data = delete $params->{data}; - my $size = delete $params->{filesize}; - - my $attachment = $class->insert_create_data($params); - my $attachid = $attachment->id; - - # The file is too large to be stored in the DB, so we store it locally. - if ($size > Bugzilla->params->{'maxattachmentsize'} * 1024) { - my $attachdir = bz_locations()->{'attachdir'}; - my $hash = ($attachid % 100) + 100; - $hash =~ s/.*(\d\d)$/group.$1/; - mkdir "$attachdir/$hash", 0770; - chmod 0770, "$attachdir/$hash"; - if (ref $data) { - copy($data, "$attachdir/$hash/attachment.$attachid"); - close $data; - } - else { - open(AH, '>', "$attachdir/$hash/attachment.$attachid"); - binmode AH; - print AH $data; - close AH; - } - $data = ''; # Will be stored in the DB. - } - # If we have a filehandle, we need its content to store it in the DB. - elsif (ref $data) { - local $/; - # Store the content in a temp variable while we close the FH. - my $tmp = <$data>; - close $data; - $data = $tmp; - } + my $class = shift; + my $dbh = Bugzilla->dbh; - my $sth = $dbh->prepare("INSERT INTO attach_data - (id, thedata) VALUES ($attachid, ?)"); + $class->check_required_create_fields(@_); + my $params = $class->run_create_validators(@_); - trick_taint($data); - $sth->bind_param(1, $data, $dbh->BLOB_TYPE); - $sth->execute(); + # Extract everything which is not a valid column name. + my $bug = delete $params->{bug}; + $params->{bug_id} = $bug->id; + my $data = delete $params->{data}; + my $size = delete $params->{filesize}; - $attachment->{bug} = $bug; + my $attachment = $class->insert_create_data($params); + my $attachid = $attachment->id; - # Return the new attachment object. - return $attachment; -} + # The file is too large to be stored in the DB, so we store it locally. + if ($size > Bugzilla->params->{'maxattachmentsize'} * 1024) { + my $attachdir = bz_locations()->{'attachdir'}; + my $hash = ($attachid % 100) + 100; + $hash =~ s/.*(\d\d)$/group.$1/; + mkdir "$attachdir/$hash", 0770; + chmod 0770, "$attachdir/$hash"; + if (ref $data) { + copy($data, "$attachdir/$hash/attachment.$attachid"); + close $data; + } + else { + open(AH, '>', "$attachdir/$hash/attachment.$attachid"); + binmode AH; + print AH $data; + close AH; + } + $data = ''; # Will be stored in the DB. + } -sub run_create_validators { - my ($class, $params) = @_; + # If we have a filehandle, we need its content to store it in the DB. + elsif (ref $data) { + local $/; + + # Store the content in a temp variable while we close the FH. + my $tmp = <$data>; + close $data; + $data = $tmp; + } - $params->{submitter_id} = Bugzilla->user->id || ThrowUserError('invalid_user'); + my $sth = $dbh->prepare( + "INSERT INTO attach_data + (id, thedata) VALUES ($attachid, ?)" + ); - # Let's validate the attachment content first as it may - # alter some other attachment attributes. - $params->{data} = $class->_check_data($params); - $params = $class->SUPER::run_create_validators($params); + trick_taint($data); + $sth->bind_param(1, $data, $dbh->BLOB_TYPE); + $sth->execute(); - $params->{creation_ts} ||= Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); - $params->{modification_time} = $params->{creation_ts}; + $attachment->{bug} = $bug; - return $params; + # Return the new attachment object. + return $attachment; } -sub update { - my $self = shift; - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; - my $timestamp = shift || $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); +sub run_create_validators { + my ($class, $params) = @_; - my ($changes, $old_self) = $self->SUPER::update(@_); + $params->{submitter_id} = Bugzilla->user->id || ThrowUserError('invalid_user'); - my ($removed, $added) = Bugzilla::Flag->update_flags($self, $old_self, $timestamp); - if ($removed || $added) { - $changes->{'flagtypes.name'} = [$removed, $added]; - } + # Let's validate the attachment content first as it may + # alter some other attachment attributes. + $params->{data} = $class->_check_data($params); + $params = $class->SUPER::run_create_validators($params); - # Record changes in the activity table. - require Bugzilla::Bug; - foreach my $field (keys %$changes) { - my $change = $changes->{$field}; - $field = "attachments.$field" unless $field eq "flagtypes.name"; - Bugzilla::Bug::LogActivityEntry($self->bug_id, $field, $change->[0], - $change->[1], $user->id, $timestamp, undef, $self->id); - } + $params->{creation_ts} + ||= Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); + $params->{modification_time} = $params->{creation_ts}; - if (scalar(keys %$changes)) { - $dbh->do('UPDATE attachments SET modification_time = ? WHERE attach_id = ?', - undef, ($timestamp, $self->id)); - $dbh->do('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?', - undef, ($timestamp, $self->bug_id)); - $self->{modification_time} = $timestamp; - # because we updated the attachments table after SUPER::update(), we - # need to ensure the cache is flushed. - Bugzilla->memcached->clear({ table => 'attachments', id => $self->id }); - } + return $params; +} - return $changes; +sub update { + my $self = shift; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + my $timestamp = shift || $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); + + my ($changes, $old_self) = $self->SUPER::update(@_); + + my ($removed, $added) + = Bugzilla::Flag->update_flags($self, $old_self, $timestamp); + if ($removed || $added) { + $changes->{'flagtypes.name'} = [$removed, $added]; + } + + # Record changes in the activity table. + require Bugzilla::Bug; + foreach my $field (keys %$changes) { + my $change = $changes->{$field}; + $field = "attachments.$field" unless $field eq "flagtypes.name"; + Bugzilla::Bug::LogActivityEntry($self->bug_id, $field, $change->[0], + $change->[1], $user->id, $timestamp, undef, $self->id); + } + + if (scalar(keys %$changes)) { + $dbh->do('UPDATE attachments SET modification_time = ? WHERE attach_id = ?', + undef, ($timestamp, $self->id)); + $dbh->do('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?', + undef, ($timestamp, $self->bug_id)); + $self->{modification_time} = $timestamp; + + # because we updated the attachments table after SUPER::update(), we + # need to ensure the cache is flushed. + Bugzilla->memcached->clear({table => 'attachments', id => $self->id}); + } + + return $changes; } =pod @@ -929,30 +955,33 @@ Returns: nothing =cut sub remove_from_db { - my $self = shift; - my $dbh = Bugzilla->dbh; - - $dbh->bz_start_transaction(); - my $flag_ids = $dbh->selectcol_arrayref( - 'SELECT id FROM flags WHERE attach_id = ?', undef, $self->id); - $dbh->do('DELETE FROM flags WHERE ' . $dbh->sql_in('id', $flag_ids)) - if @$flag_ids; - $dbh->do('DELETE FROM attach_data WHERE id = ?', undef, $self->id); - $dbh->do('UPDATE attachments SET mimetype = ?, ispatch = ?, isobsolete = ? - WHERE attach_id = ?', undef, ('text/plain', 0, 1, $self->id)); - $dbh->bz_commit_transaction(); - - my $filename = $self->_get_local_filename; - if (-e $filename) { - unlink $filename or warn "Couldn't unlink $filename: $!"; - } - - # As we don't call SUPER->remove_from_db we need to manually clear - # memcached here. - Bugzilla->memcached->clear({ table => 'attachments', id => $self->id }); - foreach my $flag_id (@$flag_ids) { - Bugzilla->memcached->clear({ table => 'flags', id => $flag_id }); - } + my $self = shift; + my $dbh = Bugzilla->dbh; + + $dbh->bz_start_transaction(); + my $flag_ids + = $dbh->selectcol_arrayref('SELECT id FROM flags WHERE attach_id = ?', + undef, $self->id); + $dbh->do('DELETE FROM flags WHERE ' . $dbh->sql_in('id', $flag_ids)) + if @$flag_ids; + $dbh->do('DELETE FROM attach_data WHERE id = ?', undef, $self->id); + $dbh->do( + 'UPDATE attachments SET mimetype = ?, ispatch = ?, isobsolete = ? + WHERE attach_id = ?', undef, ('text/plain', 0, 1, $self->id) + ); + $dbh->bz_commit_transaction(); + + my $filename = $self->_get_local_filename; + if (-e $filename) { + unlink $filename or warn "Couldn't unlink $filename: $!"; + } + + # As we don't call SUPER->remove_from_db we need to manually clear + # memcached here. + Bugzilla->memcached->clear({table => 'attachments', id => $self->id}); + foreach my $flag_id (@$flag_ids) { + Bugzilla->memcached->clear({table => 'flags', id => $flag_id}); + } } ############################### @@ -961,37 +990,39 @@ sub remove_from_db { # Extract the content type from the attachment form. sub get_content_type { - my $cgi = Bugzilla->cgi; + my $cgi = Bugzilla->cgi; - return 'text/plain' if ($cgi->param('ispatch') || $cgi->param('attach_text')); + return 'text/plain' if ($cgi->param('ispatch') || $cgi->param('attach_text')); - my $content_type; - my $method = $cgi->param('contenttypemethod') || ''; + my $content_type; + my $method = $cgi->param('contenttypemethod') || ''; - if ($method eq 'list') { - # The user selected a content type from the list, so use their - # selection. - $content_type = $cgi->param('contenttypeselection'); - } - elsif ($method eq 'manual') { - # The user entered a content type manually, so use their entry. - $content_type = $cgi->param('contenttypeentry'); - } - else { - defined $cgi->upload('data') || ThrowUserError('file_not_specified'); - # The user asked us to auto-detect the content type, so use the type - # specified in the HTTP request headers. - $content_type = - $cgi->uploadInfo($cgi->param('data'))->{'Content-Type'}; - $content_type || ThrowUserError("missing_content_type"); - - # Internet Explorer sends image/x-png for PNG images, - # so convert that to image/png to match other browsers. - if ($content_type eq 'image/x-png') { - $content_type = 'image/png'; - } + if ($method eq 'list') { + + # The user selected a content type from the list, so use their + # selection. + $content_type = $cgi->param('contenttypeselection'); + } + elsif ($method eq 'manual') { + + # The user entered a content type manually, so use their entry. + $content_type = $cgi->param('contenttypeentry'); + } + else { + defined $cgi->upload('data') || ThrowUserError('file_not_specified'); + + # The user asked us to auto-detect the content type, so use the type + # specified in the HTTP request headers. + $content_type = $cgi->uploadInfo($cgi->param('data'))->{'Content-Type'}; + $content_type || ThrowUserError("missing_content_type"); + + # Internet Explorer sends image/x-png for PNG images, + # so convert that to image/png to match other browsers. + if ($content_type eq 'image/x-png') { + $content_type = 'image/png'; } - return $content_type; + } + return $content_type; } diff --git a/Bugzilla/Attachment/PatchReader.pm b/Bugzilla/Attachment/PatchReader.pm index d0e221220..01ed51518 100644 --- a/Bugzilla/Attachment/PatchReader.pm +++ b/Bugzilla/Attachment/PatchReader.pm @@ -23,184 +23,199 @@ use Bugzilla::Util; use constant PERLIO_IS_ENABLED => $Config{useperlio}; sub process_diff { - my ($attachment, $format) = @_; - my $dbh = Bugzilla->dbh; - my $cgi = Bugzilla->cgi; - my $lc = Bugzilla->localconfig; - my $vars = {}; - - require PatchReader::Raw; - my $reader = new PatchReader::Raw; - - if ($format eq 'raw') { - require PatchReader::DiffPrinter::raw; - $reader->sends_data_to(new PatchReader::DiffPrinter::raw()); - # Actually print out the patch. - print $cgi->header(-type => 'text/plain'); - disable_utf8(); - $reader->iterate_string('Attachment ' . $attachment->id, $attachment->data); - } - else { - my @other_patches = (); - if ($lc->{interdiffbin} && $lc->{diffpath}) { - # Get the list of attachments that the user can view in this bug. - my @attachments = - @{Bugzilla::Attachment->get_attachments_by_bug($attachment->bug)}; - # Extract patches only. - @attachments = grep {$_->ispatch == 1} @attachments; - # We want them sorted from newer to older. - @attachments = sort { $b->id <=> $a->id } @attachments; - - # Ignore the current patch, but select the one right before it - # chronologically. - my $select_next_patch = 0; - foreach my $attach (@attachments) { - if ($attach->id == $attachment->id) { - $select_next_patch = 1; - } - else { - push(@other_patches, { 'id' => $attach->id, - 'desc' => $attach->description, - 'selected' => $select_next_patch }); - $select_next_patch = 0; - } - } - } + my ($attachment, $format) = @_; + my $dbh = Bugzilla->dbh; + my $cgi = Bugzilla->cgi; + my $lc = Bugzilla->localconfig; + my $vars = {}; - $vars->{'bugid'} = $attachment->bug_id; - $vars->{'attachid'} = $attachment->id; - $vars->{'description'} = $attachment->description; - $vars->{'other_patches'} = \@other_patches; - - setup_template_patch_reader($reader, $vars); - # The patch is going to be displayed in a HTML page and if the utf8 - # param is enabled, we have to encode attachment data as utf8. - if (Bugzilla->params->{'utf8'}) { - $attachment->data; # Populate ->{data} - utf8::decode($attachment->{data}); - } - $reader->iterate_string('Attachment ' . $attachment->id, $attachment->data); - } -} - -sub process_interdiff { - my ($old_attachment, $new_attachment, $format) = @_; - my $cgi = Bugzilla->cgi; - my $lc = Bugzilla->localconfig; - my $vars = {}; - - require PatchReader::Raw; - - # Encode attachment data as utf8 if it's going to be displayed in a HTML - # page using the UTF-8 encoding. - if ($format ne 'raw' && Bugzilla->params->{'utf8'}) { - $old_attachment->data; # Populate ->{data} - utf8::decode($old_attachment->{data}); - $new_attachment->data; # Populate ->{data} - utf8::decode($new_attachment->{data}); - } - - # Get old patch data. - my ($old_filename, $old_file_list) = get_unified_diff($old_attachment, $format); - # Get new patch data. - my ($new_filename, $new_file_list) = get_unified_diff($new_attachment, $format); - - my $warning = warn_if_interdiff_might_fail($old_file_list, $new_file_list); - - # Send through interdiff, send output directly to template. - # Must hack path so that interdiff will work. - local $ENV{'PATH'} = $lc->{diffpath}; - - # Open the interdiff pipe, reading from both STDOUT and STDERR - # To avoid deadlocks, we have to read the entire output from all handles - my ($stdout, $stderr) = ('', ''); - my ($pid, $interdiff_stdout, $interdiff_stderr, $use_select); - if ($ENV{MOD_PERL}) { - require Apache2::RequestUtil; - require Apache2::SubProcess; - my $request = Apache2::RequestUtil->request; - (undef, $interdiff_stdout, $interdiff_stderr) = $request->spawn_proc_prog( - $lc->{interdiffbin}, [$old_filename, $new_filename] - ); - $use_select = !PERLIO_IS_ENABLED; - } else { - $interdiff_stderr = gensym; - $pid = open3(gensym, $interdiff_stdout, $interdiff_stderr, - $lc->{interdiffbin}, $old_filename, $new_filename); - $use_select = 1; - } + require PatchReader::Raw; + my $reader = new PatchReader::Raw; - if ($format ne 'raw' && Bugzilla->params->{'utf8'}) { - binmode $interdiff_stdout, ':utf8'; - binmode $interdiff_stderr, ':utf8'; - } else { - binmode $interdiff_stdout; - binmode $interdiff_stderr; - } - - if ($use_select) { - my $select = IO::Select->new(); - $select->add($interdiff_stdout, $interdiff_stderr); - while (my @handles = $select->can_read) { - foreach my $handle (@handles) { - my $line = <$handle>; - if (!defined $line) { - $select->remove($handle); - next; - } - if ($handle == $interdiff_stdout) { - $stdout .= $line; - } else { - $stderr .= $line; - } + if ($format eq 'raw') { + require PatchReader::DiffPrinter::raw; + $reader->sends_data_to(new PatchReader::DiffPrinter::raw()); + + # Actually print out the patch. + print $cgi->header(-type => 'text/plain'); + disable_utf8(); + $reader->iterate_string('Attachment ' . $attachment->id, $attachment->data); + } + else { + my @other_patches = (); + if ($lc->{interdiffbin} && $lc->{diffpath}) { + + # Get the list of attachments that the user can view in this bug. + my @attachments + = @{Bugzilla::Attachment->get_attachments_by_bug($attachment->bug)}; + + # Extract patches only. + @attachments = grep { $_->ispatch == 1 } @attachments; + + # We want them sorted from newer to older. + @attachments = sort { $b->id <=> $a->id } @attachments; + + # Ignore the current patch, but select the one right before it + # chronologically. + my $select_next_patch = 0; + foreach my $attach (@attachments) { + if ($attach->id == $attachment->id) { + $select_next_patch = 1; + } + else { + push( + @other_patches, + { + 'id' => $attach->id, + 'desc' => $attach->description, + 'selected' => $select_next_patch } + ); + $select_next_patch = 0; } - waitpid($pid, 0) if $pid; - - } else { - local $/ = undef; - $stdout = <$interdiff_stdout>; - $stdout //= ''; - $stderr = <$interdiff_stderr>; - $stderr //= ''; + } } - close($interdiff_stdout), - close($interdiff_stderr); + $vars->{'bugid'} = $attachment->bug_id; + $vars->{'attachid'} = $attachment->id; + $vars->{'description'} = $attachment->description; + $vars->{'other_patches'} = \@other_patches; - # Tidy up - unlink($old_filename) or warn "Could not unlink $old_filename: $!"; - unlink($new_filename) or warn "Could not unlink $new_filename: $!"; + setup_template_patch_reader($reader, $vars); - # Any output on STDERR means interdiff failed to full process the patches. - # Interdiff's error messages are generic and not useful to end users, so we - # show a generic failure message. - if ($stderr) { - warn($stderr); - $warning = 'interdiff3'; + # The patch is going to be displayed in a HTML page and if the utf8 + # param is enabled, we have to encode attachment data as utf8. + if (Bugzilla->params->{'utf8'}) { + $attachment->data; # Populate ->{data} + utf8::decode($attachment->{data}); } + $reader->iterate_string('Attachment ' . $attachment->id, $attachment->data); + } +} - my $reader = new PatchReader::Raw; - - if ($format eq 'raw') { - require PatchReader::DiffPrinter::raw; - $reader->sends_data_to(new PatchReader::DiffPrinter::raw()); - # Actually print out the patch. - print $cgi->header(-type => 'text/plain'); - disable_utf8(); - } - else { - $vars->{'warning'} = $warning if $warning; - $vars->{'bugid'} = $new_attachment->bug_id; - $vars->{'oldid'} = $old_attachment->id; - $vars->{'old_desc'} = $old_attachment->description; - $vars->{'newid'} = $new_attachment->id; - $vars->{'new_desc'} = $new_attachment->description; - - setup_template_patch_reader($reader, $vars); +sub process_interdiff { + my ($old_attachment, $new_attachment, $format) = @_; + my $cgi = Bugzilla->cgi; + my $lc = Bugzilla->localconfig; + my $vars = {}; + + require PatchReader::Raw; + + # Encode attachment data as utf8 if it's going to be displayed in a HTML + # page using the UTF-8 encoding. + if ($format ne 'raw' && Bugzilla->params->{'utf8'}) { + $old_attachment->data; # Populate ->{data} + utf8::decode($old_attachment->{data}); + $new_attachment->data; # Populate ->{data} + utf8::decode($new_attachment->{data}); + } + + # Get old patch data. + my ($old_filename, $old_file_list) = get_unified_diff($old_attachment, $format); + + # Get new patch data. + my ($new_filename, $new_file_list) = get_unified_diff($new_attachment, $format); + + my $warning = warn_if_interdiff_might_fail($old_file_list, $new_file_list); + + # Send through interdiff, send output directly to template. + # Must hack path so that interdiff will work. + local $ENV{'PATH'} = $lc->{diffpath}; + + # Open the interdiff pipe, reading from both STDOUT and STDERR + # To avoid deadlocks, we have to read the entire output from all handles + my ($stdout, $stderr) = ('', ''); + my ($pid, $interdiff_stdout, $interdiff_stderr, $use_select); + if ($ENV{MOD_PERL}) { + require Apache2::RequestUtil; + require Apache2::SubProcess; + my $request = Apache2::RequestUtil->request; + (undef, $interdiff_stdout, $interdiff_stderr) + = $request->spawn_proc_prog($lc->{interdiffbin}, + [$old_filename, $new_filename]); + $use_select = !PERLIO_IS_ENABLED; + } + else { + $interdiff_stderr = gensym; + $pid = open3(gensym, $interdiff_stdout, $interdiff_stderr, $lc->{interdiffbin}, + $old_filename, $new_filename); + $use_select = 1; + } + + if ($format ne 'raw' && Bugzilla->params->{'utf8'}) { + binmode $interdiff_stdout, ':utf8'; + binmode $interdiff_stderr, ':utf8'; + } + else { + binmode $interdiff_stdout; + binmode $interdiff_stderr; + } + + if ($use_select) { + my $select = IO::Select->new(); + $select->add($interdiff_stdout, $interdiff_stderr); + while (my @handles = $select->can_read) { + foreach my $handle (@handles) { + my $line = <$handle>; + if (!defined $line) { + $select->remove($handle); + next; + } + if ($handle == $interdiff_stdout) { + $stdout .= $line; + } + else { + $stderr .= $line; + } + } } - $reader->iterate_string('interdiff #' . $old_attachment->id . - ' #' . $new_attachment->id, $stdout); + waitpid($pid, 0) if $pid; + + } + else { + local $/ = undef; + $stdout = <$interdiff_stdout>; + $stdout //= ''; + $stderr = <$interdiff_stderr>; + $stderr //= ''; + } + + close($interdiff_stdout), close($interdiff_stderr); + + # Tidy up + unlink($old_filename) or warn "Could not unlink $old_filename: $!"; + unlink($new_filename) or warn "Could not unlink $new_filename: $!"; + + # Any output on STDERR means interdiff failed to full process the patches. + # Interdiff's error messages are generic and not useful to end users, so we + # show a generic failure message. + if ($stderr) { + warn($stderr); + $warning = 'interdiff3'; + } + + my $reader = new PatchReader::Raw; + + if ($format eq 'raw') { + require PatchReader::DiffPrinter::raw; + $reader->sends_data_to(new PatchReader::DiffPrinter::raw()); + + # Actually print out the patch. + print $cgi->header(-type => 'text/plain'); + disable_utf8(); + } + else { + $vars->{'warning'} = $warning if $warning; + $vars->{'bugid'} = $new_attachment->bug_id; + $vars->{'oldid'} = $old_attachment->id; + $vars->{'old_desc'} = $old_attachment->description; + $vars->{'newid'} = $new_attachment->id; + $vars->{'new_desc'} = $new_attachment->description; + + setup_template_patch_reader($reader, $vars); + } + $reader->iterate_string( + 'interdiff #' . $old_attachment->id . ' #' . $new_attachment->id, $stdout); } ###################### @@ -208,92 +223,92 @@ sub process_interdiff { ###################### sub get_unified_diff { - my ($attachment, $format) = @_; + my ($attachment, $format) = @_; - # Bring in the modules we need. - require PatchReader::Raw; - require PatchReader::DiffPrinter::raw; - require PatchReader::PatchInfoGrabber; - require File::Temp; - - $attachment->ispatch - || ThrowCodeError('must_be_patch', { 'attach_id' => $attachment->id }); - - # Reads in the patch, converting to unified diff in a temp file. - my $reader = new PatchReader::Raw; - my $last_reader = $reader; - - # Grabs the patch file info. - my $patch_info_grabber = new PatchReader::PatchInfoGrabber(); - $last_reader->sends_data_to($patch_info_grabber); - $last_reader = $patch_info_grabber; - - # Prints out to temporary file. - my ($fh, $filename) = File::Temp::tempfile(); - if ($format ne 'raw' && Bugzilla->params->{'utf8'}) { - # The HTML page will be displayed with the UTF-8 encoding. - binmode $fh, ':utf8'; - } - my $raw_printer = new PatchReader::DiffPrinter::raw($fh); - $last_reader->sends_data_to($raw_printer); - $last_reader = $raw_printer; + # Bring in the modules we need. + require PatchReader::Raw; + require PatchReader::DiffPrinter::raw; + require PatchReader::PatchInfoGrabber; + require File::Temp; - # Iterate! - $reader->iterate_string($attachment->id, $attachment->data); + $attachment->ispatch + || ThrowCodeError('must_be_patch', {'attach_id' => $attachment->id}); - return ($filename, $patch_info_grabber->patch_info()->{files}); -} + # Reads in the patch, converting to unified diff in a temp file. + my $reader = new PatchReader::Raw; + my $last_reader = $reader; -sub warn_if_interdiff_might_fail { - my ($old_file_list, $new_file_list) = @_; + # Grabs the patch file info. + my $patch_info_grabber = new PatchReader::PatchInfoGrabber(); + $last_reader->sends_data_to($patch_info_grabber); + $last_reader = $patch_info_grabber; - # Verify that the list of files diffed is the same. - my @old_files = sort keys %{$old_file_list}; - my @new_files = sort keys %{$new_file_list}; - if (@old_files != @new_files - || join(' ', @old_files) ne join(' ', @new_files)) - { - return 'interdiff1'; - } + # Prints out to temporary file. + my ($fh, $filename) = File::Temp::tempfile(); + if ($format ne 'raw' && Bugzilla->params->{'utf8'}) { - # Verify that the revisions in the files are the same. - foreach my $file (keys %{$old_file_list}) { - if (exists $old_file_list->{$file}{old_revision} - && exists $new_file_list->{$file}{old_revision} - && $old_file_list->{$file}{old_revision} ne - $new_file_list->{$file}{old_revision}) - { - return 'interdiff2'; - } - } - return undef; -} + # The HTML page will be displayed with the UTF-8 encoding. + binmode $fh, ':utf8'; + } + my $raw_printer = new PatchReader::DiffPrinter::raw($fh); + $last_reader->sends_data_to($raw_printer); + $last_reader = $raw_printer; -sub setup_template_patch_reader { - my ($last_reader, $vars) = @_; - my $cgi = Bugzilla->cgi; - my $template = Bugzilla->template; + # Iterate! + $reader->iterate_string($attachment->id, $attachment->data); - require PatchReader::DiffPrinter::template; + return ($filename, $patch_info_grabber->patch_info()->{files}); +} - # Define the vars for templates. - if (defined $cgi->param('headers')) { - $vars->{'headers'} = $cgi->param('headers'); - } - else { - $vars->{'headers'} = 1; +sub warn_if_interdiff_might_fail { + my ($old_file_list, $new_file_list) = @_; + + # Verify that the list of files diffed is the same. + my @old_files = sort keys %{$old_file_list}; + my @new_files = sort keys %{$new_file_list}; + if (@old_files != @new_files || join(' ', @old_files) ne join(' ', @new_files)) + { + return 'interdiff1'; + } + + # Verify that the revisions in the files are the same. + foreach my $file (keys %{$old_file_list}) { + if ( exists $old_file_list->{$file}{old_revision} + && exists $new_file_list->{$file}{old_revision} + && $old_file_list->{$file}{old_revision} ne + $new_file_list->{$file}{old_revision}) + { + return 'interdiff2'; } + } + return undef; +} - $vars->{'collapsed'} = $cgi->param('collapsed'); - - # Print everything out. - print $cgi->header(-type => 'text/html'); - - $last_reader->sends_data_to(new PatchReader::DiffPrinter::template($template, - 'attachment/diff-header.html.tmpl', - 'attachment/diff-file.html.tmpl', - 'attachment/diff-footer.html.tmpl', - $vars)); +sub setup_template_patch_reader { + my ($last_reader, $vars) = @_; + my $cgi = Bugzilla->cgi; + my $template = Bugzilla->template; + + require PatchReader::DiffPrinter::template; + + # Define the vars for templates. + if (defined $cgi->param('headers')) { + $vars->{'headers'} = $cgi->param('headers'); + } + else { + $vars->{'headers'} = 1; + } + + $vars->{'collapsed'} = $cgi->param('collapsed'); + + # Print everything out. + print $cgi->header(-type => 'text/html'); + + $last_reader->sends_data_to(new PatchReader::DiffPrinter::template( + $template, 'attachment/diff-header.html.tmpl', + 'attachment/diff-file.html.tmpl', 'attachment/diff-footer.html.tmpl', + $vars + )); } 1; diff --git a/Bugzilla/Auth.pm b/Bugzilla/Auth.pm index c830f0506..23de9b4bd 100644 --- a/Bugzilla/Auth.pm +++ b/Bugzilla/Auth.pm @@ -12,9 +12,9 @@ use strict; use warnings; use fields qw( - _info_getter - _verifier - _persister + _info_getter + _verifier + _persister ); use Bugzilla::Constants; @@ -28,218 +28,223 @@ use Bugzilla::Auth::Persist::Cookie; use Socket; sub new { - my ($class, $params) = @_; - my $self = fields::new($class); + my ($class, $params) = @_; + my $self = fields::new($class); - $params ||= {}; - $params->{Login} ||= Bugzilla->params->{'user_info_class'} . ',Cookie,APIKey'; - $params->{Verify} ||= Bugzilla->params->{'user_verify_class'}; + $params ||= {}; + $params->{Login} ||= Bugzilla->params->{'user_info_class'} . ',Cookie,APIKey'; + $params->{Verify} ||= Bugzilla->params->{'user_verify_class'}; - $self->{_info_getter} = new Bugzilla::Auth::Login::Stack($params->{Login}); - $self->{_verifier} = new Bugzilla::Auth::Verify::Stack($params->{Verify}); - # If we ever have any other login persistence methods besides cookies, - # this could become more configurable. - $self->{_persister} = new Bugzilla::Auth::Persist::Cookie(); + $self->{_info_getter} = new Bugzilla::Auth::Login::Stack($params->{Login}); + $self->{_verifier} = new Bugzilla::Auth::Verify::Stack($params->{Verify}); - return $self; + # If we ever have any other login persistence methods besides cookies, + # this could become more configurable. + $self->{_persister} = new Bugzilla::Auth::Persist::Cookie(); + + return $self; } sub login { - my ($self, $type) = @_; - - # Get login info from the cookie, form, environment variables, etc. - my $login_info = $self->{_info_getter}->get_login_info(); + my ($self, $type) = @_; - if ($login_info->{failure}) { - return $self->_handle_login_result($login_info, $type); - } + # Get login info from the cookie, form, environment variables, etc. + my $login_info = $self->{_info_getter}->get_login_info(); - # Now verify their username and password against the DB, LDAP, etc. - if ($self->{_info_getter}->{successful}->requires_verification) { - $login_info = $self->{_verifier}->check_credentials($login_info); - if ($login_info->{failure}) { - return $self->_handle_login_result($login_info, $type); - } - $login_info = - $self->{_verifier}->{successful}->create_or_update_user($login_info); - } - else { - $login_info = $self->{_verifier}->create_or_update_user($login_info); - } + if ($login_info->{failure}) { + return $self->_handle_login_result($login_info, $type); + } + # Now verify their username and password against the DB, LDAP, etc. + if ($self->{_info_getter}->{successful}->requires_verification) { + $login_info = $self->{_verifier}->check_credentials($login_info); if ($login_info->{failure}) { - return $self->_handle_login_result($login_info, $type); + return $self->_handle_login_result($login_info, $type); } + $login_info + = $self->{_verifier}->{successful}->create_or_update_user($login_info); + } + else { + $login_info = $self->{_verifier}->create_or_update_user($login_info); + } + + if ($login_info->{failure}) { + return $self->_handle_login_result($login_info, $type); + } - # Make sure the user isn't disabled. - my $user = $login_info->{user}; - if (!$user->is_enabled) { - return $self->_handle_login_result({ failure => AUTH_DISABLED, - user => $user }, $type); - } - $user->set_authorizer($self); + # Make sure the user isn't disabled. + my $user = $login_info->{user}; + if (!$user->is_enabled) { + return $self->_handle_login_result({failure => AUTH_DISABLED, user => $user}, + $type); + } + $user->set_authorizer($self); - return $self->_handle_login_result($login_info, $type); + return $self->_handle_login_result($login_info, $type); } sub can_change_password { - my ($self) = @_; - my $verifier = $self->{_verifier}->{successful}; - $verifier ||= $self->{_verifier}; - my $getter = $self->{_info_getter}->{successful}; - $getter = $self->{_info_getter} - if (!$getter || $getter->isa('Bugzilla::Auth::Login::Cookie')); - return $verifier->can_change_password && - $getter->user_can_create_account; + my ($self) = @_; + my $verifier = $self->{_verifier}->{successful}; + $verifier ||= $self->{_verifier}; + my $getter = $self->{_info_getter}->{successful}; + $getter = $self->{_info_getter} + if (!$getter || $getter->isa('Bugzilla::Auth::Login::Cookie')); + return $verifier->can_change_password && $getter->user_can_create_account; } sub can_login { - my ($self) = @_; - my $getter = $self->{_info_getter}->{successful}; - $getter = $self->{_info_getter} - if (!$getter || $getter->isa('Bugzilla::Auth::Login::Cookie')); - return $getter->can_login; + my ($self) = @_; + my $getter = $self->{_info_getter}->{successful}; + $getter = $self->{_info_getter} + if (!$getter || $getter->isa('Bugzilla::Auth::Login::Cookie')); + return $getter->can_login; } sub can_logout { - my ($self) = @_; - my $getter = $self->{_info_getter}->{successful}; - # If there's no successful getter, we're not logged in, so of - # course we can't log out! - return 0 unless $getter; - return $getter->can_logout; + my ($self) = @_; + my $getter = $self->{_info_getter}->{successful}; + + # If there's no successful getter, we're not logged in, so of + # course we can't log out! + return 0 unless $getter; + 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; + 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}; - $verifier ||= $self->{_verifier}; - my $getter = $self->{_info_getter}->{successful}; - $getter = $self->{_info_getter} - if (!$getter || $getter->isa('Bugzilla::Auth::Login::Cookie')); - return $verifier->user_can_create_account - && $getter->user_can_create_account; + my ($self) = @_; + my $verifier = $self->{_verifier}->{successful}; + $verifier ||= $self->{_verifier}; + my $getter = $self->{_info_getter}->{successful}; + $getter = $self->{_info_getter} + if (!$getter || $getter->isa('Bugzilla::Auth::Login::Cookie')); + return $verifier->user_can_create_account && $getter->user_can_create_account; } sub extern_id_used { - my ($self) = @_; - return $self->{_info_getter}->extern_id_used - || $self->{_verifier}->extern_id_used; + my ($self) = @_; + return $self->{_info_getter}->extern_id_used + || $self->{_verifier}->extern_id_used; } sub can_change_email { - return $_[0]->user_can_create_account; + return $_[0]->user_can_create_account; } sub _handle_login_result { - my ($self, $result, $login_type) = @_; - my $dbh = Bugzilla->dbh; - - my $user = $result->{user}; - my $fail_code = $result->{failure}; - - if (!$fail_code) { - # We don't persist logins over GET requests in the WebService, - # because the persistance information can't be re-used again. - # (See Bugzilla::WebService::Server::JSONRPC for more info.) - if ($self->{_info_getter}->{successful}->requires_persistence - and !Bugzilla->request_cache->{auth_no_automatic_login}) - { - $user->{_login_token} = $self->{_persister}->persist_login($user); - } - } - elsif ($fail_code == AUTH_ERROR) { - if ($result->{user_error}) { - ThrowUserError($result->{user_error}, $result->{details}); - } - else { - ThrowCodeError($result->{error}, $result->{details}); - } - } - elsif ($fail_code == AUTH_NODATA) { - $self->{_info_getter}->fail_nodata($self) - if $login_type == LOGIN_REQUIRED; + my ($self, $result, $login_type) = @_; + my $dbh = Bugzilla->dbh; - # If we're not LOGIN_REQUIRED, we just return the default user. - $user = Bugzilla->user; - } - # The username/password may be wrong - # Don't let the user know whether the username exists or whether - # the password was just wrong. (This makes it harder for a cracker - # to find account names by brute force) - elsif ($fail_code == AUTH_LOGINFAILED or $fail_code == AUTH_NO_SUCH_USER) { - my $remaining_attempts = MAX_LOGIN_ATTEMPTS - - ($result->{failure_count} || 0); - ThrowUserError("invalid_login_or_password", - { remaining => $remaining_attempts }); - } - # The account may be disabled - elsif ($fail_code == AUTH_DISABLED) { - $self->{_persister}->logout(); - # XXX This is NOT a good way to do this, architecturally. - $self->{_persister}->clear_browser_cookies(); - # and throw a user error - ThrowUserError("account_disabled", - {'disabled_reason' => $result->{user}->disabledtext}); + my $user = $result->{user}; + my $fail_code = $result->{failure}; + + if (!$fail_code) { + + # We don't persist logins over GET requests in the WebService, + # because the persistance information can't be re-used again. + # (See Bugzilla::WebService::Server::JSONRPC for more info.) + if ($self->{_info_getter}->{successful}->requires_persistence + and !Bugzilla->request_cache->{auth_no_automatic_login}) + { + $user->{_login_token} = $self->{_persister}->persist_login($user); } - elsif ($fail_code == AUTH_LOCKOUT) { - my $attempts = $user->account_ip_login_failures; - - # We want to know when the account will be unlocked. This is - # determined by the 5th-from-last login failure (or more/less than - # 5th, if MAX_LOGIN_ATTEMPTS is not 5). - my $determiner = $attempts->[scalar(@$attempts) - MAX_LOGIN_ATTEMPTS]; - my $unlock_at = datetime_from($determiner->{login_time}, - Bugzilla->local_timezone); - $unlock_at->add(minutes => LOGIN_LOCKOUT_INTERVAL); - - # If we were *just* locked out, notify the maintainer about the - # lockout. - if ($result->{just_locked_out}) { - # We're sending to the maintainer, who may be not a Bugzilla - # account, but just an email address. So we use the - # installation's default language for sending the email. - my $default_settings = Bugzilla::User::Setting::get_defaults(); - my $template = Bugzilla->template_inner( - $default_settings->{lang}->{default_value}); - my $address = $attempts->[0]->{ip_addr}; - # Note: inet_aton will only resolve IPv4 addresses. - # For IPv6 we'll need to use inet_pton which requires Perl 5.12. - my $n = inet_aton($address); - if ($n) { - $address = gethostbyaddr($n, AF_INET) . " ($address)" - } - my $vars = { - locked_user => $user, - attempts => $attempts, - unlock_at => $unlock_at, - address => $address, - }; - my $message; - $template->process('email/lockout.txt.tmpl', $vars, \$message) - || ThrowTemplateError($template->error); - MessageToMTA($message); - } - - $unlock_at->set_time_zone($user->timezone); - ThrowUserError('account_locked', - { ip_addr => $determiner->{ip_addr}, unlock_at => $unlock_at }); + } + elsif ($fail_code == AUTH_ERROR) { + if ($result->{user_error}) { + ThrowUserError($result->{user_error}, $result->{details}); } - # If we get here, then we've run out of options, which shouldn't happen. else { - ThrowCodeError("authres_unhandled", { value => $fail_code }); + ThrowCodeError($result->{error}, $result->{details}); + } + } + elsif ($fail_code == AUTH_NODATA) { + $self->{_info_getter}->fail_nodata($self) if $login_type == LOGIN_REQUIRED; + + # If we're not LOGIN_REQUIRED, we just return the default user. + $user = Bugzilla->user; + } + + # The username/password may be wrong + # Don't let the user know whether the username exists or whether + # the password was just wrong. (This makes it harder for a cracker + # to find account names by brute force) + elsif ($fail_code == AUTH_LOGINFAILED or $fail_code == AUTH_NO_SUCH_USER) { + my $remaining_attempts = MAX_LOGIN_ATTEMPTS - ($result->{failure_count} || 0); + ThrowUserError("invalid_login_or_password", {remaining => $remaining_attempts}); + } + + # The account may be disabled + elsif ($fail_code == AUTH_DISABLED) { + $self->{_persister}->logout(); + + # XXX This is NOT a good way to do this, architecturally. + $self->{_persister}->clear_browser_cookies(); + + # and throw a user error + ThrowUserError("account_disabled", + {'disabled_reason' => $result->{user}->disabledtext}); + } + elsif ($fail_code == AUTH_LOCKOUT) { + my $attempts = $user->account_ip_login_failures; + + # We want to know when the account will be unlocked. This is + # determined by the 5th-from-last login failure (or more/less than + # 5th, if MAX_LOGIN_ATTEMPTS is not 5). + my $determiner = $attempts->[scalar(@$attempts) - MAX_LOGIN_ATTEMPTS]; + my $unlock_at + = datetime_from($determiner->{login_time}, Bugzilla->local_timezone); + $unlock_at->add(minutes => LOGIN_LOCKOUT_INTERVAL); + + # If we were *just* locked out, notify the maintainer about the + # lockout. + if ($result->{just_locked_out}) { + + # We're sending to the maintainer, who may be not a Bugzilla + # account, but just an email address. So we use the + # installation's default language for sending the email. + my $default_settings = Bugzilla::User::Setting::get_defaults(); + my $template + = Bugzilla->template_inner($default_settings->{lang}->{default_value}); + my $address = $attempts->[0]->{ip_addr}; + + # Note: inet_aton will only resolve IPv4 addresses. + # For IPv6 we'll need to use inet_pton which requires Perl 5.12. + my $n = inet_aton($address); + if ($n) { + $address = gethostbyaddr($n, AF_INET) . " ($address)"; + } + my $vars = { + locked_user => $user, + attempts => $attempts, + unlock_at => $unlock_at, + address => $address, + }; + my $message; + $template->process('email/lockout.txt.tmpl', $vars, \$message) + || ThrowTemplateError($template->error); + MessageToMTA($message); } - return $user; + $unlock_at->set_time_zone($user->timezone); + ThrowUserError('account_locked', + {ip_addr => $determiner->{ip_addr}, unlock_at => $unlock_at}); + } + + # If we get here, then we've run out of options, which shouldn't happen. + else { + ThrowCodeError("authres_unhandled", {value => $fail_code}); + } + + return $user; } 1; diff --git a/Bugzilla/Auth/Login.pm b/Bugzilla/Auth/Login.pm index a5f089777..68c7464f2 100644 --- a/Bugzilla/Auth/Login.pm +++ b/Bugzilla/Auth/Login.pm @@ -16,18 +16,18 @@ use fields qw(); # Determines whether or not a user can logout. It's really a subroutine, # but we implement it here as a constant. Override it in subclasses if # that particular type of login method cannot log out. -use constant can_logout => 1; -use constant can_login => 1; -use constant requires_persistence => 1; -use constant requires_verification => 1; +use constant can_logout => 1; +use constant can_login => 1; +use constant requires_persistence => 1; +use constant requires_verification => 1; use constant user_can_create_account => 0; -use constant is_automatic => 0; -use constant extern_id_used => 0; +use constant is_automatic => 0; +use constant extern_id_used => 0; sub new { - my ($class) = @_; - my $self = fields::new($class); - return $self; + my ($class) = @_; + my $self = fields::new($class); + return $self; } 1; diff --git a/Bugzilla/Auth/Login/APIKey.pm b/Bugzilla/Auth/Login/APIKey.pm index 63e35578a..79c16825e 100644 --- a/Bugzilla/Auth/Login/APIKey.pm +++ b/Bugzilla/Auth/Login/APIKey.pm @@ -26,28 +26,29 @@ use constant can_logout => 0; # This method is only available to web services. An API key can never # be used to authenticate a Web request. sub get_login_info { - my ($self) = @_; - my $params = Bugzilla->input_params; - my ($user_id, $login_cookie); + my ($self) = @_; + my $params = Bugzilla->input_params; + my ($user_id, $login_cookie); - my $api_key_text = trim(delete $params->{'Bugzilla_api_key'}); - if (!i_am_webservice() || !$api_key_text) { - return { failure => AUTH_NODATA }; - } + my $api_key_text = trim(delete $params->{'Bugzilla_api_key'}); + if (!i_am_webservice() || !$api_key_text) { + return {failure => AUTH_NODATA}; + } - my $api_key = Bugzilla::User::APIKey->new({ name => $api_key_text }); + my $api_key = Bugzilla::User::APIKey->new({name => $api_key_text}); - if (!$api_key or $api_key->api_key ne $api_key_text) { - # The second part checks the correct capitalisation. Silly MySQL - ThrowUserError("api_key_not_valid"); - } - elsif ($api_key->revoked) { - ThrowUserError('api_key_revoked'); - } + if (!$api_key or $api_key->api_key ne $api_key_text) { - $api_key->update_last_used(); + # The second part checks the correct capitalisation. Silly MySQL + ThrowUserError("api_key_not_valid"); + } + elsif ($api_key->revoked) { + ThrowUserError('api_key_revoked'); + } - return { user_id => $api_key->user_id }; + $api_key->update_last_used(); + + return {user_id => $api_key->user_id}; } 1; diff --git a/Bugzilla/Auth/Login/CGI.pm b/Bugzilla/Auth/Login/CGI.pm index 6003d62a5..0824f1ebd 100644 --- a/Bugzilla/Auth/Login/CGI.pm +++ b/Bugzilla/Auth/Login/CGI.pm @@ -21,65 +21,71 @@ 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 $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($login) || !defined($password) || !$valid) { - return { failure => AUTH_NODATA }; - } - - return { username => $login, password => $password }; + 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 $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($login) || !defined($password) || !$valid) { + return {failure => AUTH_NODATA}; + } + + return {username => $login, password => $password}; } sub fail_nodata { - my ($self) = @_; - my $cgi = Bugzilla->cgi; - my $template = Bugzilla->template; - - if (Bugzilla->usage_mode != USAGE_MODE_BROWSER) { - ThrowUserError('login_required'); - } - - print $cgi->header(); - $template->process("account/auth/login.html.tmpl", - { 'target' => $cgi->url(-relative=>1) }) - || ThrowTemplateError($template->error()); - exit; + my ($self) = @_; + my $cgi = Bugzilla->cgi; + my $template = Bugzilla->template; + + if (Bugzilla->usage_mode != USAGE_MODE_BROWSER) { + ThrowUserError('login_required'); + } + + print $cgi->header(); + $template->process("account/auth/login.html.tmpl", + {'target' => $cgi->url(-relative => 1)}) + || ThrowTemplateError($template->error()); + exit; } 1; diff --git a/Bugzilla/Auth/Login/Cookie.pm b/Bugzilla/Auth/Login/Cookie.pm index c09f08d24..1983dbd4c 100644 --- a/Bugzilla/Auth/Login/Cookie.pm +++ b/Bugzilla/Auth/Login/Cookie.pm @@ -23,121 +23,124 @@ use List::Util qw(first); use constant requires_persistence => 0; use constant requires_verification => 0; -use constant can_login => 0; +use constant can_login => 0; 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. sub get_login_info { - my ($self) = @_; - my $cgi = Bugzilla->cgi; - my $dbh = Bugzilla->dbh; - my ($user_id, $login_cookie); - - 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; - } - unless ($user_id) { - my $cookie = first {$_->name eq 'Bugzilla_login'} - @{$cgi->{'Bugzilla_cookie_list'}}; - $user_id = $cookie->value if $cookie; - } - - # If the call is for a web service, and an api token is provided, check - # it is valid. - if (i_am_webservice() && Bugzilla->input_params->{Bugzilla_api_token}) { - my $api_token = Bugzilla->input_params->{Bugzilla_api_token}; - my ($token_user_id, undef, undef, $token_type) - = Bugzilla::Token::GetTokenData($api_token); - if (!defined $token_type - || $token_type ne 'api_token' - || $user_id != $token_user_id) - { - ThrowUserError('auth_invalid_token', { token => $api_token }); - } - } + my ($self) = @_; + my $cgi = Bugzilla->cgi; + my $dbh = Bugzilla->dbh; + my ($user_id, $login_cookie); + + 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; + } + 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'}); + # If the call is for a web service, and an api token is provided, check + # it is valid. + if (i_am_webservice() && Bugzilla->input_params->{Bugzilla_api_token}) { + my $api_token = Bugzilla->input_params->{Bugzilla_api_token}; + my ($token_user_id, undef, undef, $token_type) + = Bugzilla::Token::GetTokenData($api_token); + if ( !defined $token_type + || $token_type ne 'api_token' + || $user_id != $token_user_id) + { + ThrowUserError('auth_invalid_token', {token => $api_token}); + } } + } + + # 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(); + 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 - trick_taint($ip_addr); - trick_taint($login_cookie); - detaint_natural($user_id); + if ($login_cookie && $user_id) { - my $db_cookie = - $dbh->selectrow_array('SELECT cookie + # Anything goes for these params - they're just strings which + # we're going to verify against the db + trick_taint($ip_addr); + trick_taint($login_cookie); + detaint_natural($user_id); + + my $db_cookie = $dbh->selectrow_array( + 'SELECT cookie FROM logincookies WHERE cookie = ? AND userid = ? - AND (ipaddr = ? OR ipaddr IS NULL)', - undef, ($login_cookie, $user_id, $ip_addr)); - - # If the cookie or token is valid, return a valid username. - # If they were not valid and we are using a webservice, then - # throw an error notifying the client. - 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() - WHERE cookie = ?", undef, $login_cookie); - return { user_id => $user_id }; - } - elsif (i_am_webservice()) { - ThrowUserError('invalid_cookies_or_token'); - } + AND (ipaddr = ? OR ipaddr IS NULL)', undef, + ($login_cookie, $user_id, $ip_addr) + ); + + # If the cookie or token is valid, return a valid username. + # If they were not valid and we are using a webservice, then + # throw an error notifying the client. + 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() + WHERE cookie = ?", undef, $login_cookie + ); + return {user_id => $user_id}; } - - # Either the cookie or token is invalid and we are not authenticating - # via a webservice, or we did not receive a cookie or token. We don't - # want to ever return AUTH_LOGINFAILED, because we don't want Bugzilla to - # actually throw an error when it gets a bad cookie or token. It should just - # look like there was no cookie or token to begin with. - return { failure => AUTH_NODATA }; + elsif (i_am_webservice()) { + ThrowUserError('invalid_cookies_or_token'); + } + } + + # Either the cookie or token is invalid and we are not authenticating + # via a webservice, or we did not receive a cookie or token. We don't + # want to ever return AUTH_LOGINFAILED, because we don't want Bugzilla to + # actually throw an error when it gets a bad cookie or token. It should just + # look like there was no cookie or token to begin with. + return {failure => AUTH_NODATA}; } sub login_token { - my ($self) = @_; - my $input = Bugzilla->input_params; - my $usage_mode = Bugzilla->usage_mode; + my ($self) = @_; + my $input = Bugzilla->input_params; + my $usage_mode = Bugzilla->usage_mode; - return $self->{'_login_token'} if exists $self->{'_login_token'}; + return $self->{'_login_token'} if exists $self->{'_login_token'}; - if (!i_am_webservice()) { - return $self->{'_login_token'} = undef; - } + if (!i_am_webservice()) { + 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; + # 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; - } + 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 - }; + return $self->{'_login_token'} + = {user_id => $user_id, login_token => $login_token}; } 1; diff --git a/Bugzilla/Auth/Login/Env.pm b/Bugzilla/Auth/Login/Env.pm index 653df2bb3..5fc33921b 100644 --- a/Bugzilla/Auth/Login/Env.pm +++ b/Bugzilla/Auth/Login/Env.pm @@ -16,28 +16,31 @@ use parent qw(Bugzilla::Auth::Login); use Bugzilla::Constants; use Bugzilla::Error; -use constant can_logout => 0; -use constant can_login => 0; +use constant can_logout => 0; +use constant can_login => 0; use constant requires_persistence => 0; use constant requires_verification => 0; -use constant is_automatic => 1; -use constant extern_id_used => 1; +use constant is_automatic => 1; +use constant extern_id_used => 1; sub get_login_info { - my ($self) = @_; + my ($self) = @_; - my $env_id = $ENV{Bugzilla->params->{"auth_env_id"}} || ''; - my $env_email = $ENV{Bugzilla->params->{"auth_env_email"}} || ''; - my $env_realname = $ENV{Bugzilla->params->{"auth_env_realname"}} || ''; + my $env_id = $ENV{Bugzilla->params->{"auth_env_id"}} || ''; + my $env_email = $ENV{Bugzilla->params->{"auth_env_email"}} || ''; + my $env_realname = $ENV{Bugzilla->params->{"auth_env_realname"}} || ''; - return { failure => AUTH_NODATA } if !$env_email; + return {failure => AUTH_NODATA} if !$env_email; - return { username => $env_email, extern_id => $env_id, - realname => $env_realname }; + return { + username => $env_email, + extern_id => $env_id, + realname => $env_realname + }; } sub fail_nodata { - ThrowCodeError('env_no_email'); + ThrowCodeError('env_no_email'); } 1; diff --git a/Bugzilla/Auth/Login/Stack.pm b/Bugzilla/Auth/Login/Stack.pm index dc35998e4..7786f26c8 100644 --- a/Bugzilla/Auth/Login/Stack.pm +++ b/Bugzilla/Auth/Login/Stack.pm @@ -13,8 +13,8 @@ use warnings; use base qw(Bugzilla::Auth::Login); use fields qw( - _stack - successful + _stack + successful ); use Hash::Util qw(lock_keys); use Bugzilla::Hook; @@ -22,81 +22,87 @@ use Bugzilla::Constants; use List::MoreUtils qw(any); sub new { - my $class = shift; - my $self = $class->SUPER::new(@_); - my $list = shift; - my %methods = map { $_ => "Bugzilla/Auth/Login/$_.pm" } split(',', $list); - lock_keys(%methods); - Bugzilla::Hook::process('auth_login_methods', { modules => \%methods }); - - $self->{_stack} = []; - foreach my $login_method (split(',', $list)) { - my $module = $methods{$login_method}; - require $module; - $module =~ s|/|::|g; - $module =~ s/.pm$//; - push(@{$self->{_stack}}, $module->new(@_)); - } - return $self; + my $class = shift; + my $self = $class->SUPER::new(@_); + my $list = shift; + my %methods = map { $_ => "Bugzilla/Auth/Login/$_.pm" } split(',', $list); + lock_keys(%methods); + Bugzilla::Hook::process('auth_login_methods', {modules => \%methods}); + + $self->{_stack} = []; + foreach my $login_method (split(',', $list)) { + my $module = $methods{$login_method}; + require $module; + $module =~ s|/|::|g; + $module =~ s/.pm$//; + push(@{$self->{_stack}}, $module->new(@_)); + } + return $self; } sub get_login_info { - my $self = shift; - my $result; - foreach my $object (@{$self->{_stack}}) { - # See Bugzilla::WebService::Server::JSONRPC for where and why - # auth_no_automatic_login is used. - if (Bugzilla->request_cache->{auth_no_automatic_login}) { - next if $object->is_automatic; - } - $result = $object->get_login_info(@_); - $self->{successful} = $object; - - # We only carry on down the stack if this method denied all knowledge. - last unless ($result->{failure} - && ($result->{failure} eq AUTH_NODATA - || $result->{failure} eq AUTH_NO_SUCH_USER)); - - # If none of the methods succeed, it's undef. - $self->{successful} = undef; + my $self = shift; + my $result; + foreach my $object (@{$self->{_stack}}) { + + # See Bugzilla::WebService::Server::JSONRPC for where and why + # auth_no_automatic_login is used. + if (Bugzilla->request_cache->{auth_no_automatic_login}) { + next if $object->is_automatic; } - return $result; + $result = $object->get_login_info(@_); + $self->{successful} = $object; + + # We only carry on down the stack if this method denied all knowledge. + last + unless ($result->{failure} + && ( $result->{failure} eq AUTH_NODATA + || $result->{failure} eq AUTH_NO_SUCH_USER)); + + # If none of the methods succeed, it's undef. + $self->{successful} = undef; + } + return $result; } sub fail_nodata { - my $self = shift; - # We fail from the bottom of the stack. - my @reverse_stack = reverse @{$self->{_stack}}; - foreach my $object (@reverse_stack) { - # We pick the first object that actually has the method - # implemented. - if ($object->can('fail_nodata')) { - $object->fail_nodata(@_); - } + my $self = shift; + + # We fail from the bottom of the stack. + my @reverse_stack = reverse @{$self->{_stack}}; + foreach my $object (@reverse_stack) { + + # We pick the first object that actually has the method + # implemented. + if ($object->can('fail_nodata')) { + $object->fail_nodata(@_); } + } } sub can_login { - my ($self) = @_; - # We return true if any method can log in. - foreach my $object (@{$self->{_stack}}) { - return 1 if $object->can_login; - } - return 0; + my ($self) = @_; + + # We return true if any method can log in. + foreach my $object (@{$self->{_stack}}) { + return 1 if $object->can_login; + } + return 0; } sub user_can_create_account { - my ($self) = @_; - # We return true if any method allows users to create accounts. - foreach my $object (@{$self->{_stack}}) { - return 1 if $object->user_can_create_account; - } - return 0; + my ($self) = @_; + + # We return true if any method allows users to create accounts. + foreach my $object (@{$self->{_stack}}) { + return 1 if $object->user_can_create_account; + } + return 0; } sub extern_id_used { - my ($self) = @_; - return any { $_->extern_id_used } @{ $self->{_stack} }; + my ($self) = @_; + return any { $_->extern_id_used } @{$self->{_stack}}; } 1; diff --git a/Bugzilla/Auth/Persist/Cookie.pm b/Bugzilla/Auth/Persist/Cookie.pm index 2d1291f3b..af6b0d77d 100644 --- a/Bugzilla/Auth/Persist/Cookie.pm +++ b/Bugzilla/Auth/Persist/Cookie.pm @@ -20,145 +20,154 @@ use Bugzilla::Token; use List::Util qw(first); sub new { - my ($class) = @_; - my $self = fields::new($class); - return $self; + my ($class) = @_; + my $self = fields::new($class); + return $self; } sub persist_login { - my ($self, $user) = @_; - my $dbh = Bugzilla->dbh; - my $cgi = Bugzilla->cgi; - my $input_params = Bugzilla->input_params; - - my $ip_addr; - if ($input_params->{'Bugzilla_restrictlogin'}) { - $ip_addr = remote_ip(); - # The IP address is valid, at least for comparing with itself in a - # subsequent login - trick_taint($ip_addr); - } - - $dbh->bz_start_transaction(); - - my $login_cookie = - Bugzilla::Token::GenerateUniqueToken('logincookies', 'cookie'); - - $dbh->do("INSERT INTO logincookies (cookie, userid, ipaddr, lastused) - VALUES (?, ?, ?, NOW())", - undef, $login_cookie, $user->id, $ip_addr); - - # Issuing a new cookie is a good time to clean up the old - # cookies. - $dbh->do("DELETE FROM logincookies WHERE lastused < " - . $dbh->sql_date_math('LOCALTIMESTAMP(0)', '-', - MAX_LOGINCOOKIE_AGE, 'DAY')); - - $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); - - # Remember cookie only if admin has told so - # or admin didn't forbid it and user told to remember. - if ( Bugzilla->params->{'rememberlogin'} eq 'on' || - (Bugzilla->params->{'rememberlogin'} ne 'off' && - $input_params->{'Bugzilla_remember'} && - $input_params->{'Bugzilla_remember'} eq 'on') ) - { - # Not a session cookie, so set an infinite expiry - $cookieargs{'-expires'} = 'Fri, 01-Jan-2038 00:00:00 GMT'; - } - if (Bugzilla->params->{'ssl_redirect'}) { - # Make these cookies only be sent to us by the browser during - # HTTPS sessions, if we're using SSL. - $cookieargs{'-secure'} = 1; - } - - $cgi->send_cookie(-name => 'Bugzilla_login', - -value => $user->id, - %cookieargs); - $cgi->send_cookie(-name => 'Bugzilla_logincookie', - -value => $login_cookie, - %cookieargs); + my ($self, $user) = @_; + my $dbh = Bugzilla->dbh; + my $cgi = Bugzilla->cgi; + my $input_params = Bugzilla->input_params; + + my $ip_addr; + if ($input_params->{'Bugzilla_restrictlogin'}) { + $ip_addr = remote_ip(); + + # The IP address is valid, at least for comparing with itself in a + # subsequent login + trick_taint($ip_addr); + } + + $dbh->bz_start_transaction(); + + my $login_cookie + = Bugzilla::Token::GenerateUniqueToken('logincookies', 'cookie'); + + $dbh->do( + "INSERT INTO logincookies (cookie, userid, ipaddr, lastused) + VALUES (?, ?, ?, NOW())", undef, $login_cookie, $user->id, $ip_addr + ); + + # Issuing a new cookie is a good time to clean up the old + # cookies. + $dbh->do("DELETE FROM logincookies WHERE lastused < " + . $dbh->sql_date_math('LOCALTIMESTAMP(0)', '-', MAX_LOGINCOOKIE_AGE, 'DAY')); + + $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); + + # Remember cookie only if admin has told so + # or admin didn't forbid it and user told to remember. + if ( + Bugzilla->params->{'rememberlogin'} eq 'on' + || ( Bugzilla->params->{'rememberlogin'} ne 'off' + && $input_params->{'Bugzilla_remember'} + && $input_params->{'Bugzilla_remember'} eq 'on') + ) + { + # Not a session cookie, so set an infinite expiry + $cookieargs{'-expires'} = 'Fri, 01-Jan-2038 00:00:00 GMT'; + } + if (Bugzilla->params->{'ssl_redirect'}) { + + # Make these cookies only be sent to us by the browser during + # HTTPS sessions, if we're using SSL. + $cookieargs{'-secure'} = 1; + } + + $cgi->send_cookie(-name => 'Bugzilla_login', -value => $user->id, %cookieargs); + $cgi->send_cookie( + -name => 'Bugzilla_logincookie', + -value => $login_cookie, + %cookieargs + ); } sub logout { - my ($self, $param) = @_; - - 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; - - if ($type == LOGOUT_ALL) { - $dbh->do("DELETE FROM logincookies WHERE userid = ?", - undef, $user->id); - return; - } - - # 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'}}; - if ($cookie) { - push(@login_cookies, $cookie->value); - } - elsif ($cookie = $cgi->cookie('Bugzilla_logincookie')) { - push(@login_cookies, $cookie); - } - - # 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'}); - } - - # 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 - # as a sanity check, since there is no locking here, and if the user - # logged out from two machines simultaneously, while someone else - # 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 " . - $dbh->sql_in('cookie', \@login_cookies, 1) . - " AND userid = ?", - undef, $user->id); - } elsif ($type == LOGOUT_CURRENT) { - $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()"); - } - - if ($type != LOGOUT_KEEP_CURRENT) { - clear_browser_cookies(); - } + my ($self, $param) = @_; + + 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; + + if ($type == LOGOUT_ALL) { + $dbh->do("DELETE FROM logincookies WHERE userid = ?", undef, $user->id); + return; + } + + # 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'}}; + if ($cookie) { + push(@login_cookies, $cookie->value); + } + elsif ($cookie = $cgi->cookie('Bugzilla_logincookie')) { + push(@login_cookies, $cookie); + } + + # 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'}); + } + + # 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 + # as a sanity check, since there is no locking here, and if the user + # logged out from two machines simultaneously, while someone else + # 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 " + . $dbh->sql_in('cookie', \@login_cookies, 1) + . " AND userid = ?", + undef, $user->id + ); + } + elsif ($type == LOGOUT_CURRENT) { + $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()"); + } + + if ($type != LOGOUT_KEEP_CURRENT) { + clear_browser_cookies(); + } } sub clear_browser_cookies { - my $cgi = Bugzilla->cgi; - $cgi->remove_cookie('Bugzilla_login'); - $cgi->remove_cookie('Bugzilla_logincookie'); - $cgi->remove_cookie('sudo'); + my $cgi = Bugzilla->cgi; + $cgi->remove_cookie('Bugzilla_login'); + $cgi->remove_cookie('Bugzilla_logincookie'); + $cgi->remove_cookie('sudo'); } 1; diff --git a/Bugzilla/Auth/Verify.pm b/Bugzilla/Auth/Verify.pm index 9dc83273b..639760c2b 100644 --- a/Bugzilla/Auth/Verify.pm +++ b/Bugzilla/Auth/Verify.pm @@ -19,113 +19,127 @@ use Bugzilla::User; use Bugzilla::Util; use constant user_can_create_account => 1; -use constant extern_id_used => 0; +use constant extern_id_used => 0; sub new { - my ($class, $login_type) = @_; - my $self = fields::new($class); - return $self; + my ($class, $login_type) = @_; + my $self = fields::new($class); + return $self; } sub can_change_password { - return $_[0]->can('change_password'); + return $_[0]->can('change_password'); } sub create_or_update_user { - my ($self, $params) = @_; - my $dbh = Bugzilla->dbh; - - my $extern_id = $params->{extern_id}; - my $username = $params->{bz_username} || $params->{username}; - my $password = $params->{password} || '*'; - my $real_name = $params->{realname} || ''; - my $user_id = $params->{user_id}; - - # A passed-in user_id always overrides anything else, for determining - # what account we should return. - if (!$user_id) { - my $username_user_id = login_to_id($username || ''); - my $extern_user_id; - if ($extern_id) { - trick_taint($extern_id); - $extern_user_id = $dbh->selectrow_array('SELECT userid - FROM profiles WHERE extern_id = ?', undef, $extern_id); - } - - # If we have both a valid extern_id and a valid username, and they are - # not the same id, then we have a conflict. - if ($username_user_id && $extern_user_id - && $username_user_id ne $extern_user_id) - { - my $extern_name = Bugzilla::User->new($extern_user_id)->login; - return { failure => AUTH_ERROR, error => "extern_id_conflict", - details => {extern_id => $extern_id, - extern_user => $extern_name, - username => $username} }; - } - - # If we have a valid username, but no valid id, - # then we have to create the user. This happens when we're - # passed only a username, and that username doesn't exist already. - if ($username && !$username_user_id && !$extern_user_id) { - validate_email_syntax($username) - || return { failure => AUTH_ERROR, - error => 'auth_invalid_email', - details => {addr => $username} }; - # Usually we'd call validate_password, but external authentication - # systems might follow different standards than ours. So in this - # place here, we call trick_taint without checks. - trick_taint($password); - - # XXX Theoretically this could fail with an error, but the fix for - # that is too involved to be done right now. - my $user = Bugzilla::User->create({ - login_name => $username, - cryptpassword => $password, - realname => $real_name}); - $username_user_id = $user->id; - } - - # If we have a valid username id and an extern_id, but no valid - # extern_user_id, then we have to set the user's extern_id. - if ($extern_id && $username_user_id && !$extern_user_id) { - $dbh->do('UPDATE profiles SET extern_id = ? WHERE userid = ?', - undef, $extern_id, $username_user_id); - Bugzilla->memcached->clear({ table => 'profiles', id => $username_user_id }); - } - - # Finally, at this point, one of these will give us a valid user id. - $user_id = $extern_user_id || $username_user_id; + my ($self, $params) = @_; + my $dbh = Bugzilla->dbh; + + my $extern_id = $params->{extern_id}; + my $username = $params->{bz_username} || $params->{username}; + my $password = $params->{password} || '*'; + my $real_name = $params->{realname} || ''; + my $user_id = $params->{user_id}; + + # A passed-in user_id always overrides anything else, for determining + # what account we should return. + if (!$user_id) { + my $username_user_id = login_to_id($username || ''); + my $extern_user_id; + if ($extern_id) { + trick_taint($extern_id); + $extern_user_id = $dbh->selectrow_array( + 'SELECT userid + FROM profiles WHERE extern_id = ?', undef, $extern_id + ); } - # If we still don't have a valid user_id, then we weren't passed - # enough information in $params, and we should die right here. - ThrowCodeError('bad_arg', {argument => 'params', function => - 'Bugzilla::Auth::Verify::create_or_update_user'}) - unless $user_id; - - my $user = new Bugzilla::User($user_id); - - # Now that we have a valid User, we need to see if any data has to be updated. - my $changed = 0; + # If we have both a valid extern_id and a valid username, and they are + # not the same id, then we have a conflict. + if ( $username_user_id + && $extern_user_id + && $username_user_id ne $extern_user_id) + { + my $extern_name = Bugzilla::User->new($extern_user_id)->login; + return { + failure => AUTH_ERROR, + error => "extern_id_conflict", + details => + {extern_id => $extern_id, extern_user => $extern_name, username => $username} + }; + } - if ($username && lc($user->login) ne lc($username)) { - validate_email_syntax($username) - || return { failure => AUTH_ERROR, error => 'auth_invalid_email', - details => {addr => $username} }; - $user->set_login($username); - $changed = 1; + # If we have a valid username, but no valid id, + # then we have to create the user. This happens when we're + # passed only a username, and that username doesn't exist already. + if ($username && !$username_user_id && !$extern_user_id) { + validate_email_syntax($username) || return { + failure => AUTH_ERROR, + error => 'auth_invalid_email', + details => {addr => $username} + }; + + # Usually we'd call validate_password, but external authentication + # systems might follow different standards than ours. So in this + # place here, we call trick_taint without checks. + trick_taint($password); + + # XXX Theoretically this could fail with an error, but the fix for + # that is too involved to be done right now. + my $user + = Bugzilla::User->create({ + login_name => $username, cryptpassword => $password, realname => $real_name + }); + $username_user_id = $user->id; } - if ($real_name && $user->name ne $real_name) { - # $real_name is more than likely tainted, but we only use it - # in a placeholder and we never use it after this. - trick_taint($real_name); - $user->set_name($real_name); - $changed = 1; + + # If we have a valid username id and an extern_id, but no valid + # extern_user_id, then we have to set the user's extern_id. + if ($extern_id && $username_user_id && !$extern_user_id) { + $dbh->do('UPDATE profiles SET extern_id = ? WHERE userid = ?', + undef, $extern_id, $username_user_id); + Bugzilla->memcached->clear({table => 'profiles', id => $username_user_id}); } - $user->update() if $changed; - return { user => $user }; + # Finally, at this point, one of these will give us a valid user id. + $user_id = $extern_user_id || $username_user_id; + } + + # If we still don't have a valid user_id, then we weren't passed + # enough information in $params, and we should die right here. + ThrowCodeError( + 'bad_arg', + { + argument => 'params', + function => 'Bugzilla::Auth::Verify::create_or_update_user' + } + ) unless $user_id; + + my $user = new Bugzilla::User($user_id); + + # Now that we have a valid User, we need to see if any data has to be updated. + my $changed = 0; + + if ($username && lc($user->login) ne lc($username)) { + validate_email_syntax($username) || return { + failure => AUTH_ERROR, + error => 'auth_invalid_email', + details => {addr => $username} + }; + $user->set_login($username); + $changed = 1; + } + if ($real_name && $user->name ne $real_name) { + + # $real_name is more than likely tainted, but we only use it + # in a placeholder and we never use it after this. + trick_taint($real_name); + $user->set_name($real_name); + $changed = 1; + } + $user->update() if $changed; + + return {user => $user}; } 1; diff --git a/Bugzilla/Auth/Verify/DB.pm b/Bugzilla/Auth/Verify/DB.pm index 28a9310c9..951aaaf9f 100644 --- a/Bugzilla/Auth/Verify/DB.pm +++ b/Bugzilla/Auth/Verify/DB.pm @@ -19,95 +19,97 @@ use Bugzilla::Util; use Bugzilla::User; sub check_credentials { - my ($self, $login_data) = @_; - my $dbh = Bugzilla->dbh; + my ($self, $login_data) = @_; + my $dbh = Bugzilla->dbh; - my $username = $login_data->{username}; - my $user = new Bugzilla::User({ name => $username }); + my $username = $login_data->{username}; + my $user = new Bugzilla::User({name => $username}); - return { failure => AUTH_NO_SUCH_USER } unless $user; + return {failure => AUTH_NO_SUCH_USER} unless $user; - $login_data->{user} = $user; - $login_data->{bz_username} = $user->login; + $login_data->{user} = $user; + $login_data->{bz_username} = $user->login; + if ($user->account_is_locked_out) { + return {failure => AUTH_LOCKOUT, user => $user}; + } + + my $password = $login_data->{password}; + my $real_password_crypted = $user->cryptpassword; + + # Using the internal crypted password as the salt, + # crypt the password the user entered. + my $entered_password_crypted = bz_crypt($password, $real_password_crypted); + + if ($entered_password_crypted ne $real_password_crypted) { + + # Record the login failure + $user->note_login_failure(); + + # Immediately check if we are locked out if ($user->account_is_locked_out) { - return { failure => AUTH_LOCKOUT, user => $user }; + return {failure => AUTH_LOCKOUT, user => $user, just_locked_out => 1}; } - my $password = $login_data->{password}; - my $real_password_crypted = $user->cryptpassword; - - # Using the internal crypted password as the salt, - # crypt the password the user entered. - my $entered_password_crypted = bz_crypt($password, $real_password_crypted); - - if ($entered_password_crypted ne $real_password_crypted) { - # Record the login failure - $user->note_login_failure(); - - # Immediately check if we are locked out - if ($user->account_is_locked_out) { - return { failure => AUTH_LOCKOUT, user => $user, - just_locked_out => 1 }; - } - - return { failure => AUTH_LOGINFAILED, - failure_count => scalar(@{ $user->account_ip_login_failures }), - }; - } - - # Force the user to change their password if it does not meet the current - # criteria. This should usually only happen if the criteria has changed. - if (Bugzilla->usage_mode == USAGE_MODE_BROWSER && - Bugzilla->params->{password_check_on_login}) - { - my $check = validate_password_check($password); - if ($check) { - return { - failure => AUTH_ERROR, - user_error => $check, - details => { locked_user => $user } - } - } + return { + failure => AUTH_LOGINFAILED, + failure_count => scalar(@{$user->account_ip_login_failures}), + }; + } + + # Force the user to change their password if it does not meet the current + # criteria. This should usually only happen if the criteria has changed. + if ( Bugzilla->usage_mode == USAGE_MODE_BROWSER + && Bugzilla->params->{password_check_on_login}) + { + my $check = validate_password_check($password); + if ($check) { + return { + failure => AUTH_ERROR, + user_error => $check, + details => {locked_user => $user} + }; } + } - # The user's credentials are okay, so delete any outstanding - # password tokens or login failures they may have generated. - Bugzilla::Token::DeletePasswordTokens($user->id, "user_logged_in"); - $user->clear_login_failures(); + # The user's credentials are okay, so delete any outstanding + # password tokens or login failures they may have generated. + Bugzilla::Token::DeletePasswordTokens($user->id, "user_logged_in"); + $user->clear_login_failures(); - my $update_password = 0; + my $update_password = 0; - # If their old password was using crypt() or some different hash - # than we're using now, convert the stored password to using - # whatever hashing system we're using now. - my $current_algorithm = PASSWORD_DIGEST_ALGORITHM; - $update_password = 1 if ($real_password_crypted !~ /{\Q$current_algorithm\E}$/); + # If their old password was using crypt() or some different hash + # than we're using now, convert the stored password to using + # whatever hashing system we're using now. + my $current_algorithm = PASSWORD_DIGEST_ALGORITHM; + $update_password = 1 if ($real_password_crypted !~ /{\Q$current_algorithm\E}$/); - # If their old password was using a different length salt than what - # we're using now, update the password to use the new salt length. - if ($real_password_crypted =~ /^([^,]+),/) { - $update_password = 1 if (length($1) != PASSWORD_SALT_LENGTH); - } + # If their old password was using a different length salt than what + # we're using now, update the password to use the new salt length. + if ($real_password_crypted =~ /^([^,]+),/) { + $update_password = 1 if (length($1) != PASSWORD_SALT_LENGTH); + } - # If needed, update the user's password. - if ($update_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(); - } + # If needed, update the user's password. + if ($update_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(); + } - return $login_data; + return $login_data; } sub change_password { - my ($self, $user, $password) = @_; - my $dbh = Bugzilla->dbh; - my $cryptpassword = bz_crypt($password); - $dbh->do("UPDATE profiles SET cryptpassword = ? WHERE userid = ?", - undef, $cryptpassword, $user->id); - Bugzilla->memcached->clear({ table => 'profiles', id => $user->id }); + my ($self, $user, $password) = @_; + my $dbh = Bugzilla->dbh; + my $cryptpassword = bz_crypt($password); + $dbh->do("UPDATE profiles SET cryptpassword = ? WHERE userid = ?", + undef, $cryptpassword, $user->id); + Bugzilla->memcached->clear({table => 'profiles', id => $user->id}); } 1; diff --git a/Bugzilla/Auth/Verify/LDAP.pm b/Bugzilla/Auth/Verify/LDAP.pm index e37f55793..c92a38909 100644 --- a/Bugzilla/Auth/Verify/LDAP.pm +++ b/Bugzilla/Auth/Verify/LDAP.pm @@ -13,7 +13,7 @@ use warnings; use base qw(Bugzilla::Auth::Verify); use fields qw( - ldap + ldap ); use Bugzilla::Constants; @@ -28,126 +28,139 @@ use constant admin_can_create_account => 0; use constant user_can_create_account => 0; sub check_credentials { - my ($self, $params) = @_; - my $dbh = Bugzilla->dbh; - - # We need to bind anonymously to the LDAP server. This is - # because we need to get the Distinguished Name of the user trying - # to log in. Some servers (such as iPlanet) allow you to have unique - # uids spread out over a subtree of an area (such as "People"), so - # just appending the Base DN to the uid isn't sufficient to get the - # user's DN. For servers which don't work this way, there will still - # be no harm done. + my ($self, $params) = @_; + my $dbh = Bugzilla->dbh; + + # We need to bind anonymously to the LDAP server. This is + # because we need to get the Distinguished Name of the user trying + # to log in. Some servers (such as iPlanet) allow you to have unique + # uids spread out over a subtree of an area (such as "People"), so + # just appending the Base DN to the uid isn't sufficient to get the + # user's DN. For servers which don't work this way, there will still + # be no harm done. + $self->_bind_ldap_for_search(); + + # Now, we verify that the user exists, and get a LDAP Distinguished + # Name for the user. + my $username = $params->{username}; + my $dn_result + = $self->ldap->search(_bz_search_params($username), attrs => ['dn']); + return { + failure => AUTH_ERROR, + error => "ldap_search_error", + details => {errstr => $dn_result->error, username => $username} + } + if $dn_result->code; + + return {failure => AUTH_NO_SUCH_USER} if !$dn_result->count; + + my $dn = $dn_result->shift_entry->dn; + + # Check the password. + my $pw_result = $self->ldap->bind($dn, password => $params->{password}); + return {failure => AUTH_LOGINFAILED} if $pw_result->code; + + # And now we fill in the user's details. + + # First try the search as the (already bound) user in question. + my $user_entry; + my $error_string; + my $detail_result = $self->ldap->search(_bz_search_params($username)); + if ($detail_result->code) { + + # Stash away the original error, just in case + $error_string = $detail_result->error; + } + else { + $user_entry = $detail_result->shift_entry; + } + + # If that failed (either because the search failed, or returned no + # results) then try re-binding as the initial search user, but only + # if the LDAPbinddn parameter is set. + if (!$user_entry && Bugzilla->params->{"LDAPbinddn"}) { $self->_bind_ldap_for_search(); - # Now, we verify that the user exists, and get a LDAP Distinguished - # Name for the user. - my $username = $params->{username}; - my $dn_result = $self->ldap->search(_bz_search_params($username), - attrs => ['dn']); - return { failure => AUTH_ERROR, error => "ldap_search_error", - details => {errstr => $dn_result->error, username => $username} - } if $dn_result->code; - - return { failure => AUTH_NO_SUCH_USER } if !$dn_result->count; - - my $dn = $dn_result->shift_entry->dn; - - # Check the password. - my $pw_result = $self->ldap->bind($dn, password => $params->{password}); - return { failure => AUTH_LOGINFAILED } if $pw_result->code; - - # And now we fill in the user's details. - - # First try the search as the (already bound) user in question. - my $user_entry; - my $error_string; - my $detail_result = $self->ldap->search(_bz_search_params($username)); - if ($detail_result->code) { - # Stash away the original error, just in case - $error_string = $detail_result->error; - } else { - $user_entry = $detail_result->shift_entry; + $detail_result = $self->ldap->search(_bz_search_params($username)); + if (!$detail_result->code) { + $user_entry = $detail_result->shift_entry; } + } - # If that failed (either because the search failed, or returned no - # results) then try re-binding as the initial search user, but only - # if the LDAPbinddn parameter is set. - if (!$user_entry && Bugzilla->params->{"LDAPbinddn"}) { - $self->_bind_ldap_for_search(); - - $detail_result = $self->ldap->search(_bz_search_params($username)); - if (!$detail_result->code) { - $user_entry = $detail_result->shift_entry; - } + # If we *still* don't have anything in $user_entry then give up. + return { + failure => AUTH_ERROR, + error => "ldap_search_error", + details => {errstr => $error_string, username => $username} } + if !$user_entry; - # If we *still* don't have anything in $user_entry then give up. - return { failure => AUTH_ERROR, error => "ldap_search_error", - details => {errstr => $error_string, username => $username} - } if !$user_entry; + my $mail_attr = Bugzilla->params->{"LDAPmailattribute"}; + if ($mail_attr) { + if (!$user_entry->exists($mail_attr)) { + return { + failure => AUTH_ERROR, + error => "ldap_cannot_retreive_attr", + details => {attr => $mail_attr} + }; + } - my $mail_attr = Bugzilla->params->{"LDAPmailattribute"}; - if ($mail_attr) { - if (!$user_entry->exists($mail_attr)) { - return { failure => AUTH_ERROR, - error => "ldap_cannot_retreive_attr", - details => {attr => $mail_attr} }; - } + my @emails = $user_entry->get_value($mail_attr); - my @emails = $user_entry->get_value($mail_attr); + # Default to the first email address returned. + $params->{bz_username} = $emails[0]; - # Default to the first email address returned. - $params->{bz_username} = $emails[0]; + if (@emails > 1) { - if (@emails > 1) { - # Cycle through the adresses and check if they're Bugzilla logins. - # Use the first one that returns a valid id. - foreach my $email (@emails) { - if ( login_to_id($email) ) { - $params->{bz_username} = $email; - last; - } - } + # Cycle through the adresses and check if they're Bugzilla logins. + # Use the first one that returns a valid id. + foreach my $email (@emails) { + if (login_to_id($email)) { + $params->{bz_username} = $email; + last; } - - } else { - $params->{bz_username} = $username; + } } - $params->{realname} ||= $user_entry->get_value("displayName"); - $params->{realname} ||= $user_entry->get_value("cn"); + } + else { + $params->{bz_username} = $username; + } + + $params->{realname} ||= $user_entry->get_value("displayName"); + $params->{realname} ||= $user_entry->get_value("cn"); - $params->{extern_id} = $username; + $params->{extern_id} = $username; - return $params; + return $params; } sub _bz_search_params { - my ($username) = @_; - $username = escape_filter_value($username); - return (base => Bugzilla->params->{"LDAPBaseDN"}, - scope => "sub", - filter => '(&(' . Bugzilla->params->{"LDAPuidattribute"} - . "=$username)" - . Bugzilla->params->{"LDAPfilter"} . ')'); + my ($username) = @_; + $username = escape_filter_value($username); + return ( + base => Bugzilla->params->{"LDAPBaseDN"}, + scope => "sub", + filter => '(&(' + . Bugzilla->params->{"LDAPuidattribute"} + . "=$username)" + . Bugzilla->params->{"LDAPfilter"} . ')' + ); } sub _bind_ldap_for_search { - my ($self) = @_; - my $bind_result; - if (Bugzilla->params->{"LDAPbinddn"}) { - my ($LDAPbinddn,$LDAPbindpass) = - split(":",Bugzilla->params->{"LDAPbinddn"}); - $bind_result = - $self->ldap->bind($LDAPbinddn, password => $LDAPbindpass); - } - else { - $bind_result = $self->ldap->bind(); - } - ThrowCodeError("ldap_bind_failed", {errstr => $bind_result->error}) - if $bind_result->code; + my ($self) = @_; + my $bind_result; + if (Bugzilla->params->{"LDAPbinddn"}) { + my ($LDAPbinddn, $LDAPbindpass) = split(":", Bugzilla->params->{"LDAPbinddn"}); + $bind_result = $self->ldap->bind($LDAPbinddn, password => $LDAPbindpass); + } + else { + $bind_result = $self->ldap->bind(); + } + ThrowCodeError("ldap_bind_failed", {errstr => $bind_result->error}) + if $bind_result->code; } # We can't just do this in new(), because we're not allowed to throw any @@ -156,27 +169,27 @@ sub _bind_ldap_for_search { # to fix their mistake. (Because Bugzilla->login always calls # Bugzilla::Auth->new, and almost every page calls Bugzilla->login.) sub ldap { - my ($self) = @_; - return $self->{ldap} if $self->{ldap}; - - my @servers = split(/[\s,]+/, Bugzilla->params->{"LDAPserver"}); - ThrowCodeError("ldap_server_not_defined") unless @servers; - - foreach (@servers) { - $self->{ldap} = new Net::LDAP(trim($_)); - last if $self->{ldap}; - } - ThrowCodeError("ldap_connect_failed", { server => join(", ", @servers) }) - unless $self->{ldap}; - - # try to start TLS if needed - if (Bugzilla->params->{"LDAPstarttls"}) { - my $mesg = $self->{ldap}->start_tls(); - ThrowCodeError("ldap_start_tls_failed", { error => $mesg->error() }) - if $mesg->code(); - } - - return $self->{ldap}; + my ($self) = @_; + return $self->{ldap} if $self->{ldap}; + + my @servers = split(/[\s,]+/, Bugzilla->params->{"LDAPserver"}); + ThrowCodeError("ldap_server_not_defined") unless @servers; + + foreach (@servers) { + $self->{ldap} = new Net::LDAP(trim($_)); + last if $self->{ldap}; + } + ThrowCodeError("ldap_connect_failed", {server => join(", ", @servers)}) + unless $self->{ldap}; + + # try to start TLS if needed + if (Bugzilla->params->{"LDAPstarttls"}) { + my $mesg = $self->{ldap}->start_tls(); + ThrowCodeError("ldap_start_tls_failed", {error => $mesg->error()}) + if $mesg->code(); + } + + return $self->{ldap}; } 1; diff --git a/Bugzilla/Auth/Verify/RADIUS.pm b/Bugzilla/Auth/Verify/RADIUS.pm index 283d9b466..2cbde0404 100644 --- a/Bugzilla/Auth/Verify/RADIUS.pm +++ b/Bugzilla/Auth/Verify/RADIUS.pm @@ -23,33 +23,37 @@ use constant admin_can_create_account => 0; use constant user_can_create_account => 0; sub check_credentials { - my ($self, $params) = @_; - my $dbh = Bugzilla->dbh; - my $address_suffix = Bugzilla->params->{'RADIUS_email_suffix'}; - my $username = $params->{username}; - - # If we're using RADIUS_email_suffix, we may need to cut it off from - # the login name. - if ($address_suffix) { - $username =~ s/\Q$address_suffix\E$//i; - } - - # Create RADIUS object. - my $radius = - new Authen::Radius(Host => Bugzilla->params->{'RADIUS_server'}, - Secret => Bugzilla->params->{'RADIUS_secret'}) - || return { failure => AUTH_ERROR, error => 'radius_preparation_error', - details => {errstr => Authen::Radius::strerror() } }; - - # Check the password. - $radius->check_pwd($username, $params->{password}, - Bugzilla->params->{'RADIUS_NAS_IP'} || undef) - || return { failure => AUTH_LOGINFAILED }; - - # Build the user account's e-mail address. - $params->{bz_username} = $username . $address_suffix; - - return $params; + my ($self, $params) = @_; + my $dbh = Bugzilla->dbh; + my $address_suffix = Bugzilla->params->{'RADIUS_email_suffix'}; + my $username = $params->{username}; + + # If we're using RADIUS_email_suffix, we may need to cut it off from + # the login name. + if ($address_suffix) { + $username =~ s/\Q$address_suffix\E$//i; + } + + # Create RADIUS object. + my $radius = new Authen::Radius( + Host => Bugzilla->params->{'RADIUS_server'}, + Secret => Bugzilla->params->{'RADIUS_secret'} + ) + || return { + failure => AUTH_ERROR, + error => 'radius_preparation_error', + details => {errstr => Authen::Radius::strerror()} + }; + + # Check the password. + $radius->check_pwd($username, $params->{password}, + Bugzilla->params->{'RADIUS_NAS_IP'} || undef) + || return {failure => AUTH_LOGINFAILED}; + + # Build the user account's e-mail address. + $params->{bz_username} = $username . $address_suffix; + + return $params; } 1; diff --git a/Bugzilla/Auth/Verify/Stack.pm b/Bugzilla/Auth/Verify/Stack.pm index 3e5db3cec..9a9412915 100644 --- a/Bugzilla/Auth/Verify/Stack.pm +++ b/Bugzilla/Auth/Verify/Stack.pm @@ -13,8 +13,8 @@ use warnings; use base qw(Bugzilla::Auth::Verify); use fields qw( - _stack - successful + _stack + successful ); use Bugzilla::Hook; @@ -23,70 +23,75 @@ use Hash::Util qw(lock_keys); use List::MoreUtils qw(any); sub new { - my $class = shift; - my $list = shift; - my $self = $class->SUPER::new(@_); - my %methods = map { $_ => "Bugzilla/Auth/Verify/$_.pm" } split(',', $list); - lock_keys(%methods); - Bugzilla::Hook::process('auth_verify_methods', { modules => \%methods }); - - $self->{_stack} = []; - foreach my $verify_method (split(',', $list)) { - my $module = $methods{$verify_method}; - require $module; - $module =~ s|/|::|g; - $module =~ s/.pm$//; - push(@{$self->{_stack}}, $module->new(@_)); - } - return $self; + my $class = shift; + my $list = shift; + my $self = $class->SUPER::new(@_); + my %methods = map { $_ => "Bugzilla/Auth/Verify/$_.pm" } split(',', $list); + lock_keys(%methods); + Bugzilla::Hook::process('auth_verify_methods', {modules => \%methods}); + + $self->{_stack} = []; + foreach my $verify_method (split(',', $list)) { + my $module = $methods{$verify_method}; + require $module; + $module =~ s|/|::|g; + $module =~ s/.pm$//; + push(@{$self->{_stack}}, $module->new(@_)); + } + return $self; } sub can_change_password { - my ($self) = @_; - # We return true if any method can change passwords. - foreach my $object (@{$self->{_stack}}) { - return 1 if $object->can_change_password; - } - return 0; + my ($self) = @_; + + # We return true if any method can change passwords. + foreach my $object (@{$self->{_stack}}) { + return 1 if $object->can_change_password; + } + return 0; } sub check_credentials { - my $self = shift; - my $result; - foreach my $object (@{$self->{_stack}}) { - $result = $object->check_credentials(@_); - $self->{successful} = $object; - last if !$result->{failure}; - # So that if none of them succeed, it's undef. - $self->{successful} = undef; - } - # Returns the result at the bottom of the stack if they all fail. - return $result; + my $self = shift; + my $result; + foreach my $object (@{$self->{_stack}}) { + $result = $object->check_credentials(@_); + $self->{successful} = $object; + last if !$result->{failure}; + + # So that if none of them succeed, it's undef. + $self->{successful} = undef; + } + + # Returns the result at the bottom of the stack if they all fail. + return $result; } sub create_or_update_user { - my $self = shift; - my $result; - foreach my $object (@{$self->{_stack}}) { - $result = $object->create_or_update_user(@_); - last if !$result->{failure}; - } - # Returns the result at the bottom of the stack if they all fail. - return $result; + my $self = shift; + my $result; + foreach my $object (@{$self->{_stack}}) { + $result = $object->create_or_update_user(@_); + last if !$result->{failure}; + } + + # Returns the result at the bottom of the stack if they all fail. + return $result; } sub user_can_create_account { - my ($self) = @_; - # We return true if any method allows the user to create an account. - foreach my $object (@{$self->{_stack}}) { - return 1 if $object->user_can_create_account; - } - return 0; + my ($self) = @_; + + # We return true if any method allows the user to create an account. + foreach my $object (@{$self->{_stack}}) { + return 1 if $object->user_can_create_account; + } + return 0; } sub extern_id_used { - my ($self) = @_; - return any { $_->extern_id_used } @{ $self->{_stack} }; + my ($self) = @_; + return any { $_->extern_id_used } @{$self->{_stack}}; } 1; diff --git a/Bugzilla/Bug.pm b/Bugzilla/Bug.pm index 8b4493f85..ebf00edf3 100644 --- a/Bugzilla/Bug.pm +++ b/Bugzilla/Bug.pm @@ -38,9 +38,9 @@ use Scalar::Util qw(blessed); use parent qw(Bugzilla::Object Exporter); @Bugzilla::Bug::EXPORT = qw( - bug_alias_to_id - LogActivityEntry - editable_bug_fields + bug_alias_to_id + LogActivityEntry + editable_bug_fields ); ##################################################################### @@ -51,198 +51,199 @@ use constant DB_TABLE => 'bugs'; use constant ID_FIELD => 'bug_id'; use constant NAME_FIELD => 'bug_id'; use constant LIST_ORDER => ID_FIELD; + # Bugs have their own auditing table, bugs_activity. use constant AUDIT_CREATES => 0; use constant AUDIT_UPDATES => 0; + # This will be enabled later use constant USE_MEMCACHED => 0; # This is a sub because it needs to call other subroutines. sub DB_COLUMNS { - my $dbh = Bugzilla->dbh; - my @custom = grep {$_->type != FIELD_TYPE_MULTI_SELECT} - Bugzilla->active_custom_fields; - my @custom_names = map {$_->name} @custom; - - my @columns = (qw( - assigned_to - bug_file_loc - bug_id - bug_severity - bug_status - cclist_accessible - component_id - creation_ts - delta_ts - estimated_time - everconfirmed - lastdiffed - op_sys - priority - product_id - qa_contact - remaining_time - rep_platform - reporter_accessible - resolution - short_desc - status_whiteboard - target_milestone - version - ), - 'reporter AS reporter_id', - $dbh->sql_date_format('deadline', '%Y-%m-%d') . ' AS deadline', - @custom_names); - - Bugzilla::Hook::process("bug_columns", { columns => \@columns }); - - return @columns; + my $dbh = Bugzilla->dbh; + my @custom + = grep { $_->type != FIELD_TYPE_MULTI_SELECT } Bugzilla->active_custom_fields; + my @custom_names = map { $_->name } @custom; + + my @columns = ( + qw( + assigned_to + bug_file_loc + bug_id + bug_severity + bug_status + cclist_accessible + component_id + creation_ts + delta_ts + estimated_time + everconfirmed + lastdiffed + op_sys + priority + product_id + qa_contact + remaining_time + rep_platform + reporter_accessible + resolution + short_desc + status_whiteboard + target_milestone + version + ), 'reporter AS reporter_id', + $dbh->sql_date_format('deadline', '%Y-%m-%d') . ' AS deadline', @custom_names + ); + + Bugzilla::Hook::process("bug_columns", {columns => \@columns}); + + return @columns; } sub VALIDATORS { - my $validators = { - alias => \&_check_alias, - assigned_to => \&_check_assigned_to, - blocked => \&_check_dependencies, - bug_file_loc => \&_check_bug_file_loc, - bug_severity => \&_check_select_field, - bug_status => \&_check_bug_status, - cc => \&_check_cc, - comment => \&_check_comment, - component => \&_check_component, - creation_ts => \&_check_creation_ts, - deadline => \&_check_deadline, - dependson => \&_check_dependencies, - dup_id => \&_check_dup_id, - estimated_time => \&_check_time_field, - everconfirmed => \&Bugzilla::Object::check_boolean, - groups => \&_check_groups, - keywords => \&_check_keywords, - op_sys => \&_check_select_field, - priority => \&_check_priority, - product => \&_check_product, - qa_contact => \&_check_qa_contact, - remaining_time => \&_check_time_field, - rep_platform => \&_check_select_field, - resolution => \&_check_resolution, - short_desc => \&_check_short_desc, - status_whiteboard => \&_check_status_whiteboard, - target_milestone => \&_check_target_milestone, - version => \&_check_version, - - cclist_accessible => \&Bugzilla::Object::check_boolean, - reporter_accessible => \&Bugzilla::Object::check_boolean, - }; - - # Set up validators for custom fields. - foreach my $field (Bugzilla->active_custom_fields) { - my $validator; - if ($field->type == FIELD_TYPE_SINGLE_SELECT) { - $validator = \&_check_select_field; - } - elsif ($field->type == FIELD_TYPE_MULTI_SELECT) { - $validator = \&_check_multi_select_field; - } - elsif ($field->type == FIELD_TYPE_DATETIME) { - $validator = \&_check_datetime_field; - } - elsif ($field->type == FIELD_TYPE_DATE) { - $validator = \&_check_date_field; - } - elsif ($field->type == FIELD_TYPE_FREETEXT) { - $validator = \&_check_freetext_field; - } - elsif ($field->type == FIELD_TYPE_BUG_ID) { - $validator = \&_check_bugid_field; - } - elsif ($field->type == FIELD_TYPE_TEXTAREA) { - $validator = \&_check_textarea_field; - } - elsif ($field->type == FIELD_TYPE_INTEGER) { - $validator = \&_check_integer_field; - } - else { - $validator = \&_check_default_field; - } - $validators->{$field->name} = $validator; + my $validators = { + alias => \&_check_alias, + assigned_to => \&_check_assigned_to, + blocked => \&_check_dependencies, + bug_file_loc => \&_check_bug_file_loc, + bug_severity => \&_check_select_field, + bug_status => \&_check_bug_status, + cc => \&_check_cc, + comment => \&_check_comment, + component => \&_check_component, + creation_ts => \&_check_creation_ts, + deadline => \&_check_deadline, + dependson => \&_check_dependencies, + dup_id => \&_check_dup_id, + estimated_time => \&_check_time_field, + everconfirmed => \&Bugzilla::Object::check_boolean, + groups => \&_check_groups, + keywords => \&_check_keywords, + op_sys => \&_check_select_field, + priority => \&_check_priority, + product => \&_check_product, + qa_contact => \&_check_qa_contact, + remaining_time => \&_check_time_field, + rep_platform => \&_check_select_field, + resolution => \&_check_resolution, + short_desc => \&_check_short_desc, + status_whiteboard => \&_check_status_whiteboard, + target_milestone => \&_check_target_milestone, + version => \&_check_version, + + cclist_accessible => \&Bugzilla::Object::check_boolean, + reporter_accessible => \&Bugzilla::Object::check_boolean, + }; + + # Set up validators for custom fields. + foreach my $field (Bugzilla->active_custom_fields) { + my $validator; + if ($field->type == FIELD_TYPE_SINGLE_SELECT) { + $validator = \&_check_select_field; + } + elsif ($field->type == FIELD_TYPE_MULTI_SELECT) { + $validator = \&_check_multi_select_field; + } + elsif ($field->type == FIELD_TYPE_DATETIME) { + $validator = \&_check_datetime_field; + } + elsif ($field->type == FIELD_TYPE_DATE) { + $validator = \&_check_date_field; + } + elsif ($field->type == FIELD_TYPE_FREETEXT) { + $validator = \&_check_freetext_field; + } + elsif ($field->type == FIELD_TYPE_BUG_ID) { + $validator = \&_check_bugid_field; + } + elsif ($field->type == FIELD_TYPE_TEXTAREA) { + $validator = \&_check_textarea_field; + } + elsif ($field->type == FIELD_TYPE_INTEGER) { + $validator = \&_check_integer_field; + } + else { + $validator = \&_check_default_field; } + $validators->{$field->name} = $validator; + } - return $validators; -}; + return $validators; +} sub VALIDATOR_DEPENDENCIES { - my $cache = Bugzilla->request_cache; - return $cache->{bug_validator_dependencies} - if $cache->{bug_validator_dependencies}; - - my %deps = ( - assigned_to => ['component'], - blocked => ['product'], - bug_status => ['product', 'comment', 'target_milestone'], - cc => ['component'], - comment => ['creation_ts'], - component => ['product'], - dependson => ['product'], - dup_id => ['bug_status', 'resolution'], - groups => ['product'], - keywords => ['product'], - resolution => ['bug_status', 'dependson'], - qa_contact => ['component'], - target_milestone => ['product'], - version => ['product'], - ); - - foreach my $field (@{ Bugzilla->fields }) { - $deps{$field->name} = [ $field->visibility_field->name ] - if $field->{visibility_field_id}; - } - - $cache->{bug_validator_dependencies} = \%deps; - return \%deps; -}; + my $cache = Bugzilla->request_cache; + return $cache->{bug_validator_dependencies} + if $cache->{bug_validator_dependencies}; + + my %deps = ( + assigned_to => ['component'], + blocked => ['product'], + bug_status => ['product', 'comment', 'target_milestone'], + cc => ['component'], + comment => ['creation_ts'], + component => ['product'], + dependson => ['product'], + dup_id => ['bug_status', 'resolution'], + groups => ['product'], + keywords => ['product'], + resolution => ['bug_status', 'dependson'], + qa_contact => ['component'], + target_milestone => ['product'], + version => ['product'], + ); + + foreach my $field (@{Bugzilla->fields}) { + $deps{$field->name} = [$field->visibility_field->name] + if $field->{visibility_field_id}; + } + + $cache->{bug_validator_dependencies} = \%deps; + return \%deps; +} sub UPDATE_COLUMNS { - my @custom = grep {$_->type != FIELD_TYPE_MULTI_SELECT} - Bugzilla->active_custom_fields; - my @custom_names = map {$_->name} @custom; - my @columns = qw( - assigned_to - bug_file_loc - bug_severity - bug_status - cclist_accessible - component_id - deadline - estimated_time - everconfirmed - op_sys - priority - product_id - qa_contact - remaining_time - rep_platform - reporter_accessible - resolution - short_desc - status_whiteboard - target_milestone - version - ); - push(@columns, @custom_names); - return @columns; -}; - -use constant NUMERIC_COLUMNS => qw( + my @custom + = grep { $_->type != FIELD_TYPE_MULTI_SELECT } Bugzilla->active_custom_fields; + my @custom_names = map { $_->name } @custom; + my @columns = qw( + assigned_to + bug_file_loc + bug_severity + bug_status + cclist_accessible + component_id + deadline estimated_time + everconfirmed + op_sys + priority + product_id + qa_contact remaining_time + rep_platform + reporter_accessible + resolution + short_desc + status_whiteboard + target_milestone + version + ); + push(@columns, @custom_names); + return @columns; +} + +use constant NUMERIC_COLUMNS => qw( + estimated_time + remaining_time ); sub DATE_COLUMNS { - my @fields = (@{ Bugzilla->fields({ type => [FIELD_TYPE_DATETIME, - FIELD_TYPE_DATE] }) - }); - return map { $_->name } @fields; + my @fields + = (@{Bugzilla->fields({type => [FIELD_TYPE_DATETIME, FIELD_TYPE_DATE]})}); + return map { $_->name } @fields; } # Used in LogActivityEntry(). Gives the max length of lines in the @@ -254,30 +255,28 @@ use constant MAX_LINE_LENGTH => 254; # of Bugzilla. (These are the field names that the WebService and email_in.pl # use.) use constant FIELD_MAP => { - blocks => 'blocked', - commentprivacy => 'comment_is_private', - creation_time => 'creation_ts', - creator => 'reporter', - description => 'comment', - depends_on => 'dependson', - dupe_of => 'dup_id', - id => 'bug_id', - is_confirmed => 'everconfirmed', - is_cc_accessible => 'cclist_accessible', - is_creator_accessible => 'reporter_accessible', - last_change_time => 'delta_ts', - platform => 'rep_platform', - severity => 'bug_severity', - status => 'bug_status', - summary => 'short_desc', - url => 'bug_file_loc', - whiteboard => 'status_whiteboard', + blocks => 'blocked', + commentprivacy => 'comment_is_private', + creation_time => 'creation_ts', + creator => 'reporter', + description => 'comment', + depends_on => 'dependson', + dupe_of => 'dup_id', + id => 'bug_id', + is_confirmed => 'everconfirmed', + is_cc_accessible => 'cclist_accessible', + is_creator_accessible => 'reporter_accessible', + last_change_time => 'delta_ts', + platform => 'rep_platform', + severity => 'bug_severity', + status => 'bug_status', + summary => 'short_desc', + url => 'bug_file_loc', + whiteboard => 'status_whiteboard', }; -use constant REQUIRED_FIELD_MAP => { - product_id => 'product', - component_id => 'component', -}; +use constant REQUIRED_FIELD_MAP => + {product_id => 'product', component_id => 'component',}; # Creation timestamp is here because it needs to be validated # but it can be NULL in the database (see comments in create above) @@ -295,360 +294,374 @@ use constant REQUIRED_FIELD_MAP => { # # Groups are in a separate table, but must always be validated so that # mandatory groups get set on bugs. -use constant EXTRA_REQUIRED_FIELDS => qw(creation_ts target_milestone cc qa_contact groups); +use constant EXTRA_REQUIRED_FIELDS => + qw(creation_ts target_milestone cc qa_contact groups); ##################################################################### sub new { - my $invocant = shift; - my $class = ref($invocant) || $invocant; - my $param = shift; - - # Remove leading "#" mark if we've just been passed an id. - 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 !~ /^[0-9]+$/) - || (ref($param) && $param->{id} !~ /^[0-9]+$/)) - { - if ($param) { - my $alias = ref($param) ? $param->{id} : $param; - my $bug_id = bug_alias_to_id($alias); - if (! $bug_id) { - my $error_self = {}; - bless $error_self, $class; - $error_self->{'bug_id'} = $alias; - $error_self->{'error'} = 'InvalidBugId'; - return $error_self; - } - $param = { id => $bug_id, - cache => ref($param) ? $param->{cache} : 0 }; - } - else { - # We got something that's not a number. - my $error_self = {}; - bless $error_self, $class; - $error_self->{'bug_id'} = $param; - $error_self->{'error'} = 'InvalidBugId'; - return $error_self; - } - } - - unshift @_, $param; - my $self = $class->SUPER::new(@_); - - # Bugzilla::Bug->new always returns something, but sets $self->{error} - # if the bug wasn't found in the database. - if (!$self) { + my $invocant = shift; + my $class = ref($invocant) || $invocant; + my $param = shift; + + # Remove leading "#" mark if we've just been passed an id. + 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 !~ /^[0-9]+$/) + || (ref($param) && $param->{id} !~ /^[0-9]+$/)) + { + if ($param) { + my $alias = ref($param) ? $param->{id} : $param; + my $bug_id = bug_alias_to_id($alias); + if (!$bug_id) { my $error_self = {}; - if (ref $param) { - $error_self->{bug_id} = $param->{name}; - $error_self->{error} = 'InvalidBugId'; - } - else { - $error_self->{bug_id} = $param; - $error_self->{error} = 'NotFound'; - } bless $error_self, $class; + $error_self->{'bug_id'} = $alias; + $error_self->{'error'} = 'InvalidBugId'; return $error_self; + } + $param = {id => $bug_id, cache => ref($param) ? $param->{cache} : 0}; } + else { + # We got something that's not a number. + my $error_self = {}; + bless $error_self, $class; + $error_self->{'bug_id'} = $param; + $error_self->{'error'} = 'InvalidBugId'; + return $error_self; + } + } + + unshift @_, $param; + my $self = $class->SUPER::new(@_); + + # Bugzilla::Bug->new always returns something, but sets $self->{error} + # if the bug wasn't found in the database. + if (!$self) { + my $error_self = {}; + if (ref $param) { + $error_self->{bug_id} = $param->{name}; + $error_self->{error} = 'InvalidBugId'; + } + else { + $error_self->{bug_id} = $param; + $error_self->{error} = 'NotFound'; + } + bless $error_self, $class; + return $error_self; + } - return $self; + return $self; } sub initialize { - $_[0]->_create_cf_accessors(); + $_[0]->_create_cf_accessors(); } sub object_cache_key { - my $class = shift; - my $key = $class->SUPER::object_cache_key(@_) - || return; - return $key . ',' . Bugzilla->user->id; + my $class = shift; + my $key = $class->SUPER::object_cache_key(@_) || return; + return $key . ',' . Bugzilla->user->id; } sub check { - my $class = shift; - my ($param, $field) = @_; + my $class = shift; + 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. - my $id = ref($param) - ? ($param->{id} = trim($param->{id})) - : ($param = trim($param)); - ThrowUserError('improper_bug_id_field_value', { field => $field }) unless defined $id; + # Bugzilla::Bug throws lots of special errors, so we don't call + # SUPER::check, we just call our new and do our own checks. + 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); + my $self = $class->new($param); - if ($self->{error}) { - # For error messages, use the id that was returned by new(), because - # it's cleaned up. - $id = $self->id; + if ($self->{error}) { - if ($self->{error} eq 'NotFound') { - ThrowUserError("bug_id_does_not_exist", { bug_id => $id }); - } - if ($self->{error} eq 'InvalidBugId') { - ThrowUserError("improper_bug_id_field_value", - { bug_id => $id, - field => $field }); - } - } + # For error messages, use the id that was returned by new(), because + # it's cleaned up. + $id = $self->id; - unless ($field && $field =~ /^(dependson|blocked|dup_id)$/) { - $self->check_is_visible($id); + if ($self->{error} eq 'NotFound') { + ThrowUserError("bug_id_does_not_exist", {bug_id => $id}); } - return $self; + if ($self->{error} eq 'InvalidBugId') { + ThrowUserError("improper_bug_id_field_value", {bug_id => $id, field => $field}); + } + } + + unless ($field && $field =~ /^(dependson|blocked|dup_id)$/) { + $self->check_is_visible($id); + } + return $self; } sub check_for_edit { - my $class = shift; - my $bug = $class->check(@_); + my $class = shift; + my $bug = $class->check(@_); - Bugzilla->user->can_edit_product($bug->product_id) - || ThrowUserError("product_edit_denied", { product => $bug->product }); + Bugzilla->user->can_edit_product($bug->product_id) + || ThrowUserError("product_edit_denied", {product => $bug->product}); - return $bug; + return $bug; } sub check_is_visible { - my ($self, $input_id) = @_; - $input_id ||= $self->id; - my $user = Bugzilla->user; - - if (!$user->can_see_bug($self->id)) { - # The error the user sees depends on whether or not they are - # logged in (i.e. $user->id contains the user's positive integer ID). - # If we are validating an alias, then use it in the error message - # instead of its corresponding bug ID, to not disclose it. - if ($user->id) { - ThrowUserError("bug_access_denied", { bug_id => $input_id }); - } else { - ThrowUserError("bug_access_query", { bug_id => $input_id }); - } - } -} + my ($self, $input_id) = @_; + $input_id ||= $self->id; + my $user = Bugzilla->user; -sub match { - my $class = shift; - my ($params) = @_; - - # Allow matching certain fields by name (in addition to matching by ID). - my %translate_fields = ( - assigned_to => 'Bugzilla::User', - qa_contact => 'Bugzilla::User', - reporter => 'Bugzilla::User', - product => 'Bugzilla::Product', - component => 'Bugzilla::Component', - ); - my %translated; - - foreach my $field (keys %translate_fields) { - my @ids; - # Convert names to ids. We use "exists" everywhere since people can - # legally specify "undef" to mean IS NULL (even though most of these - # fields can't be NULL, people can still specify it...). - if (exists $params->{$field}) { - my $names = $params->{$field}; - my $type = $translate_fields{$field}; - my $param = $type eq 'Bugzilla::User' ? 'login_name' : 'name'; - # We call Bugzilla::Object::match directly to avoid the - # Bugzilla::User::match implementation which is different. - my $objects = Bugzilla::Object::match($type, { $param => $names }); - push(@ids, map { $_->id } @$objects); - } - # You can also specify ids directly as arguments to this function, - # so include them in the list if they have been specified. - if (exists $params->{"${field}_id"}) { - my $current_ids = $params->{"${field}_id"}; - my @id_array = ref $current_ids ? @$current_ids : ($current_ids); - push(@ids, @id_array); - } - # We do this "or" instead of a "scalar(@ids)" to handle the case - # when people passed only invalid object names. Otherwise we'd - # end up with a SUPER::match call with zero criteria (which dies). - if (exists $params->{$field} or exists $params->{"${field}_id"}) { - $translated{$field} = scalar(@ids) == 1 ? $ids[0] : \@ids; - } - } + if (!$user->can_see_bug($self->id)) { - # The user fields don't have an _id on the end of them in the database, - # but the product & component fields do, so we have to have separate - # code to deal with the different sets of fields here. - foreach my $field (qw(assigned_to qa_contact reporter)) { - delete $params->{"${field}_id"}; - $params->{$field} = $translated{$field} - if exists $translated{$field}; + # The error the user sees depends on whether or not they are + # logged in (i.e. $user->id contains the user's positive integer ID). + # If we are validating an alias, then use it in the error message + # instead of its corresponding bug ID, to not disclose it. + if ($user->id) { + ThrowUserError("bug_access_denied", {bug_id => $input_id}); } - foreach my $field (qw(product component)) { - delete $params->{$field}; - $params->{"${field}_id"} = $translated{$field} - if exists $translated{$field}; + else { + ThrowUserError("bug_access_query", {bug_id => $input_id}); } + } +} - return $class->SUPER::match(@_); +sub match { + my $class = shift; + my ($params) = @_; + + # Allow matching certain fields by name (in addition to matching by ID). + my %translate_fields = ( + assigned_to => 'Bugzilla::User', + qa_contact => 'Bugzilla::User', + reporter => 'Bugzilla::User', + product => 'Bugzilla::Product', + component => 'Bugzilla::Component', + ); + my %translated; + + foreach my $field (keys %translate_fields) { + my @ids; + + # Convert names to ids. We use "exists" everywhere since people can + # legally specify "undef" to mean IS NULL (even though most of these + # fields can't be NULL, people can still specify it...). + if (exists $params->{$field}) { + my $names = $params->{$field}; + my $type = $translate_fields{$field}; + my $param = $type eq 'Bugzilla::User' ? 'login_name' : 'name'; + + # We call Bugzilla::Object::match directly to avoid the + # Bugzilla::User::match implementation which is different. + my $objects = Bugzilla::Object::match($type, {$param => $names}); + push(@ids, map { $_->id } @$objects); + } + + # You can also specify ids directly as arguments to this function, + # so include them in the list if they have been specified. + if (exists $params->{"${field}_id"}) { + my $current_ids = $params->{"${field}_id"}; + my @id_array = ref $current_ids ? @$current_ids : ($current_ids); + push(@ids, @id_array); + } + + # We do this "or" instead of a "scalar(@ids)" to handle the case + # when people passed only invalid object names. Otherwise we'd + # end up with a SUPER::match call with zero criteria (which dies). + if (exists $params->{$field} or exists $params->{"${field}_id"}) { + $translated{$field} = scalar(@ids) == 1 ? $ids[0] : \@ids; + } + } + + # The user fields don't have an _id on the end of them in the database, + # but the product & component fields do, so we have to have separate + # code to deal with the different sets of fields here. + foreach my $field (qw(assigned_to qa_contact reporter)) { + delete $params->{"${field}_id"}; + $params->{$field} = $translated{$field} if exists $translated{$field}; + } + foreach my $field (qw(product component)) { + delete $params->{$field}; + $params->{"${field}_id"} = $translated{$field} if exists $translated{$field}; + } + + return $class->SUPER::match(@_); } # Helps load up information for bugs for show_bug.cgi and other situations # that will need to access info on lots of bugs. sub preload { - my ($class, $bugs) = @_; - my $user = Bugzilla->user; - - # It would be faster but MUCH more complicated to select all the - # deps for the entire list in one SQL statement. If we ever have - # a profile that proves that that's necessary, we can switch over - # to the more complex method. - my @all_dep_ids; - foreach my $bug (@$bugs) { - push @all_dep_ids, @{ $bug->blocked }, @{ $bug->dependson }; - push @all_dep_ids, @{ $bug->duplicate_ids }; - push @all_dep_ids, @{ $bug->_preload_referenced_bugs }; - } - @all_dep_ids = uniq @all_dep_ids; - # If we don't do this, can_see_bug will do one call per bug in - # the dependency and duplicate lists, in Bugzilla::Template::get_bug_link. - $user->visible_bugs(\@all_dep_ids); + my ($class, $bugs) = @_; + my $user = Bugzilla->user; + + # It would be faster but MUCH more complicated to select all the + # deps for the entire list in one SQL statement. If we ever have + # a profile that proves that that's necessary, we can switch over + # to the more complex method. + my @all_dep_ids; + foreach my $bug (@$bugs) { + push @all_dep_ids, @{$bug->blocked}, @{$bug->dependson}; + push @all_dep_ids, @{$bug->duplicate_ids}; + push @all_dep_ids, @{$bug->_preload_referenced_bugs}; + } + @all_dep_ids = uniq @all_dep_ids; + + # If we don't do this, can_see_bug will do one call per bug in + # the dependency and duplicate lists, in Bugzilla::Template::get_bug_link. + $user->visible_bugs(\@all_dep_ids); } # Helps load up bugs referenced in comments by retrieving them with a single # query from the database and injecting bug objects into the object-cache. sub _preload_referenced_bugs { - my $self = shift; + my $self = shift; - # inject current duplicates into the object-cache first - foreach my $bug (@{ $self->duplicates }) { - $bug->object_cache_set() unless Bugzilla::Bug->object_cache_get($bug->id); - } + # inject current duplicates into the object-cache first + foreach my $bug (@{$self->duplicates}) { + $bug->object_cache_set() unless Bugzilla::Bug->object_cache_get($bug->id); + } - # preload bugs from comments - my $referenced_bug_ids = _extract_bug_ids($self->comments); - my @ref_bug_ids = grep { !Bugzilla::Bug->object_cache_get($_) } @$referenced_bug_ids; + # preload bugs from comments + my $referenced_bug_ids = _extract_bug_ids($self->comments); + my @ref_bug_ids + = grep { !Bugzilla::Bug->object_cache_get($_) } @$referenced_bug_ids; - # inject into object-cache - my $referenced_bugs = Bugzilla::Bug->new_from_list(\@ref_bug_ids); - $_->object_cache_set() foreach @$referenced_bugs; + # inject into object-cache + my $referenced_bugs = Bugzilla::Bug->new_from_list(\@ref_bug_ids); + $_->object_cache_set() foreach @$referenced_bugs; - return $referenced_bug_ids; + return $referenced_bug_ids; } # Extract bug IDs mentioned in comments. This is much faster than calling quoteUrls(). sub _extract_bug_ids { - my $comments = shift; - my @bug_ids; - - my $params = Bugzilla->params; - my @urlbases = ($params->{'urlbase'}); - push(@urlbases, $params->{'sslbase'}) if $params->{'sslbase'}; - my $urlbase_re = '(?:' . join('|', map { qr/$_/ } @urlbases) . ')'; - my $bug_word = template_var('terms')->{bug}; - my $bugs_word = template_var('terms')->{bugs}; - - foreach my $comment (@$comments) { - if ($comment->type == CMT_HAS_DUPE || $comment->type == CMT_DUPE_OF) { - push @bug_ids, $comment->extra_data; - next; - } - 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([0-9]+)(?:\#c[0-9]+)?/g; - # bug X - 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*([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\ )([0-9]+)(?=\ \*\*\*\Z)/; - } - # Make sure to filter invalid bug IDs. - @bug_ids = grep { $_ < MAX_INT_32 } @bug_ids; - return [uniq @bug_ids]; -} + my $comments = shift; + my @bug_ids; -sub possible_duplicates { - my ($class, $params) = @_; - my $short_desc = $params->{summary}; - my $products = $params->{products} || []; - my $limit = $params->{limit} || MAX_POSSIBLE_DUPLICATES; - $limit = MAX_POSSIBLE_DUPLICATES if $limit > MAX_POSSIBLE_DUPLICATES; - $products = [$products] if !ref($products) eq 'ARRAY'; - - my $orig_limit = $limit; - detaint_natural($limit) - || ThrowCodeError('param_must_be_numeric', - { function => 'possible_duplicates', - param => $orig_limit }); + my $params = Bugzilla->params; + my @urlbases = ($params->{'urlbase'}); + push(@urlbases, $params->{'sslbase'}) if $params->{'sslbase'}; + my $urlbase_re = '(?:' . join('|', map {qr/$_/} @urlbases) . ')'; + my $bug_word = template_var('terms')->{bug}; + my $bugs_word = template_var('terms')->{bugs}; - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; - my @words = split(/[\b\s]+/, $short_desc || ''); - # Remove leading/trailing punctuation from words - foreach my $word (@words) { - $word =~ s/(?:^\W+|\W+$)//g; + foreach my $comment (@$comments) { + if ($comment->type == CMT_HAS_DUPE || $comment->type == CMT_DUPE_OF) { + push @bug_ids, $comment->extra_data; + next; } - # And make sure that each word is longer than 2 characters. - @words = grep { defined $_ and length($_) > 2 } @words; + my $s = $comment->already_wrapped ? qr/\s/ : qr/\h/; + my $text = $comment->body; - return [] if !@words; + # Full bug links + push @bug_ids, + $text =~ /\b$urlbase_re\Qshow_bug.cgi?id=\E([0-9]+)(?:\#c[0-9]+)?/g; - my ($where_sql, $relevance_sql); - if ($dbh->FULLTEXT_OR) { - my $joined_terms = join($dbh->FULLTEXT_OR, @words); - ($where_sql, $relevance_sql) = - $dbh->sql_fulltext_search('bugs_fulltext.short_desc', $joined_terms); - $relevance_sql ||= $where_sql; - } - else { - my (@where, @relevance); - foreach my $word (@words) { - my ($term, $rel_term) = $dbh->sql_fulltext_search( - 'bugs_fulltext.short_desc', $word); - push(@where, $term); - push(@relevance, $rel_term || $term); - } + # bug X + 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*([0-9]+)(?:$s*,$s*\#?$s*([0-9]+))+/i; + push @bug_ids, $text =~ /\b$bugs_re/g; - $where_sql = join(' OR ', @where); - $relevance_sql = join(' + ', @relevance); + # Old duplicate markers + push @bug_ids, $text + =~ /(?<=^\*\*\*\ This\ bug\ has\ been\ marked\ as\ a\ duplicate\ of\ )([0-9]+)(?=\ \*\*\*\Z)/; + } + + # Make sure to filter invalid bug IDs. + @bug_ids = grep { $_ < MAX_INT_32 } @bug_ids; + return [uniq @bug_ids]; +} + +sub possible_duplicates { + my ($class, $params) = @_; + my $short_desc = $params->{summary}; + my $products = $params->{products} || []; + my $limit = $params->{limit} || MAX_POSSIBLE_DUPLICATES; + $limit = MAX_POSSIBLE_DUPLICATES if $limit > MAX_POSSIBLE_DUPLICATES; + $products = [$products] if !ref($products) eq 'ARRAY'; + + my $orig_limit = $limit; + detaint_natural($limit) + || ThrowCodeError('param_must_be_numeric', + {function => 'possible_duplicates', param => $orig_limit}); + + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + my @words = split(/[\b\s]+/, $short_desc || ''); + + # 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; + + return [] if !@words; + + my ($where_sql, $relevance_sql); + if ($dbh->FULLTEXT_OR) { + my $joined_terms = join($dbh->FULLTEXT_OR, @words); + ($where_sql, $relevance_sql) + = $dbh->sql_fulltext_search('bugs_fulltext.short_desc', $joined_terms); + $relevance_sql ||= $where_sql; + } + else { + my (@where, @relevance); + foreach my $word (@words) { + my ($term, $rel_term) + = $dbh->sql_fulltext_search('bugs_fulltext.short_desc', $word); + push(@where, $term); + push(@relevance, $rel_term || $term); } - my $product_ids = join(',', map { $_->id } @$products); - my $product_sql = $product_ids ? "AND product_id IN ($product_ids)" : ""; + $where_sql = join(' OR ', @where); + $relevance_sql = join(' + ', @relevance); + } - # Because we collapse duplicates, we want to get slightly more bugs - # than were actually asked for. - my $sql_limit = $limit + 5; + my $product_ids = join(',', map { $_->id } @$products); + my $product_sql = $product_ids ? "AND product_id IN ($product_ids)" : ""; - my $possible_dupes = $dbh->selectall_arrayref( - "SELECT bugs.bug_id AS bug_id, bugs.resolution AS resolution, + # Because we collapse duplicates, we want to get slightly more bugs + # than were actually asked for. + my $sql_limit = $limit + 5; + + my $possible_dupes = $dbh->selectall_arrayref( + "SELECT bugs.bug_id AS bug_id, bugs.resolution AS resolution, ($relevance_sql) AS relevance FROM bugs INNER JOIN bugs_fulltext ON bugs.bug_id = bugs_fulltext.bug_id WHERE ($where_sql) $product_sql - ORDER BY relevance DESC, bug_id DESC " . - $dbh->sql_limit($sql_limit), {Slice=>{}}); - - my @actual_dupe_ids; - # Resolve duplicates into their ultimate target duplicates. - foreach my $bug (@$possible_dupes) { - my $push_id = $bug->{bug_id}; - if ($bug->{resolution} && $bug->{resolution} eq 'DUPLICATE') { - $push_id = _resolve_ultimate_dup_id($bug->{bug_id}); - } - push(@actual_dupe_ids, $push_id); - } - @actual_dupe_ids = uniq @actual_dupe_ids; - if (scalar @actual_dupe_ids > $limit) { - @actual_dupe_ids = @actual_dupe_ids[0..($limit-1)]; + ORDER BY relevance DESC, bug_id DESC " . $dbh->sql_limit($sql_limit), + {Slice => {}} + ); + + my @actual_dupe_ids; + + # Resolve duplicates into their ultimate target duplicates. + foreach my $bug (@$possible_dupes) { + my $push_id = $bug->{bug_id}; + if ($bug->{resolution} && $bug->{resolution} eq 'DUPLICATE') { + $push_id = _resolve_ultimate_dup_id($bug->{bug_id}); } + push(@actual_dupe_ids, $push_id); + } + @actual_dupe_ids = uniq @actual_dupe_ids; + if (scalar @actual_dupe_ids > $limit) { + @actual_dupe_ids = @actual_dupe_ids[0 .. ($limit - 1)]; + } - my $visible = $user->visible_bugs(\@actual_dupe_ids); - return $class->new_from_list($visible); + my $visible = $user->visible_bugs(\@actual_dupe_ids); + return $class->new_from_list($visible); } # Docs for create() (there's no POD in this file yet, but we very @@ -680,591 +693,621 @@ sub possible_duplicates { # # C - The full login name of the user who the bug is # initially assigned to. -# C - The full login name of the QA Contact for this bug. +# C - The full login name of the QA Contact for this bug. # Will be ignored if C is off. # -# C - For time-tracking. Will be ignored if +# C - For time-tracking. Will be ignored if # C is not set, or if the current # user is not a member of the timetrackinggroup. # C - For time-tracking. Will be ignored for the same # reasons as C. sub create { - my ($class, $params) = @_; - my $dbh = Bugzilla->dbh; - - $dbh->bz_start_transaction(); - - # These fields have default values which we can use if they are undefined. - $params->{bug_severity} = Bugzilla->params->{defaultseverity} - unless defined $params->{bug_severity}; - $params->{priority} = Bugzilla->params->{defaultpriority} - unless defined $params->{priority}; - $params->{op_sys} = Bugzilla->params->{defaultopsys} - unless defined $params->{op_sys}; - $params->{rep_platform} = Bugzilla->params->{defaultplatform} - unless defined $params->{rep_platform}; - # Make sure a comment is always defined. - $params->{comment} = '' unless defined $params->{comment}; - - $class->check_required_create_fields($params); - $params = $class->run_create_validators($params); - - # These are not a fields in the bugs table, so we don't pass them to - # insert_create_data. - my $bug_aliases = delete $params->{alias}; - my $cc_ids = delete $params->{cc}; - my $groups = delete $params->{groups}; - my $depends_on = delete $params->{dependson}; - my $blocked = delete $params->{blocked}; - my $keywords = delete $params->{keywords}; - my $creation_comment = delete $params->{comment}; - my $see_also = delete $params->{see_also}; - - # We don't want the bug to appear in the system until it's correctly - # protected by groups. - my $timestamp = delete $params->{creation_ts}; - - my $ms_values = $class->_extract_multi_selects($params); - my $bug = $class->insert_create_data($params); - - # Add the group restrictions - my $sth_group = $dbh->prepare( - 'INSERT INTO bug_group_map (bug_id, group_id) VALUES (?, ?)'); - foreach my $group (@$groups) { - $sth_group->execute($bug->bug_id, $group->id); - } - - $dbh->do('UPDATE bugs SET creation_ts = ? WHERE bug_id = ?', undef, - $timestamp, $bug->bug_id); - # Update the bug instance as well - $bug->{creation_ts} = $timestamp; - - # Add the CCs - my $sth_cc = $dbh->prepare('INSERT INTO cc (bug_id, who) VALUES (?,?)'); - foreach my $user_id (@$cc_ids) { - $sth_cc->execute($bug->bug_id, $user_id); - } - - # Add in keywords - my $sth_keyword = $dbh->prepare( - 'INSERT INTO keywords (bug_id, keywordid) VALUES (?, ?)'); - foreach my $keyword_id (map($_->id, @$keywords)) { - $sth_keyword->execute($bug->bug_id, $keyword_id); - } - - # Set up dependencies (blocked/dependson) - my $sth_deps = $dbh->prepare( - 'INSERT INTO dependencies (blocked, dependson) VALUES (?, ?)'); - my $sth_bug_time = $dbh->prepare('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?'); - - foreach my $depends_on_id (@$depends_on) { - $sth_deps->execute($bug->bug_id, $depends_on_id); - # Log the reverse action on the other bug. - LogActivityEntry($depends_on_id, 'blocked', '', $bug->bug_id, - $bug->{reporter_id}, $timestamp); - $sth_bug_time->execute($timestamp, $depends_on_id); - } - foreach my $blocked_id (@$blocked) { - $sth_deps->execute($blocked_id, $bug->bug_id); - # Log the reverse action on the other bug. - LogActivityEntry($blocked_id, 'dependson', '', $bug->bug_id, - $bug->{reporter_id}, $timestamp); - $sth_bug_time->execute($timestamp, $blocked_id); - } - - # Insert the values into the multiselect value tables - foreach my $field (keys %$ms_values) { - $dbh->do("DELETE FROM bug_$field where bug_id = ?", - undef, $bug->bug_id); - foreach my $value ( @{$ms_values->{$field}} ) { - $dbh->do("INSERT INTO bug_$field (bug_id, value) VALUES (?,?)", - undef, $bug->bug_id, $value); - } - } - - # Insert any see_also values - if ($see_also) { - my $see_also_array = $see_also; - if (!ref $see_also_array) { - $see_also = trim($see_also); - $see_also_array = [ split(/[\s,]+/, $see_also) ]; - } - foreach my $value (@$see_also_array) { - $bug->add_see_also($value); - } - foreach my $see_also (@{ $bug->see_also }) { - $see_also->insert_create_data($see_also); - } - foreach my $ref_bug (@{ $bug->{_update_ref_bugs} || [] }) { - $ref_bug->update(); - } - delete $bug->{_update_ref_bugs}; - } - - # Comment #0 handling... - - # We now have a bug id so we can fill this out - $creation_comment->{'bug_id'} = $bug->id; - - # Insert the comment. We always insert a comment on bug creation, - # but sometimes it's blank. - Bugzilla::Comment->insert_create_data($creation_comment); - - # Set up aliases - my $sth_aliases = $dbh->prepare('INSERT INTO bugs_aliases (alias, bug_id) VALUES (?, ?)'); - foreach my $alias (@$bug_aliases) { - trick_taint($alias); - $sth_aliases->execute($alias, $bug->bug_id); - } + my ($class, $params) = @_; + my $dbh = Bugzilla->dbh; + + $dbh->bz_start_transaction(); + + # These fields have default values which we can use if they are undefined. + $params->{bug_severity} = Bugzilla->params->{defaultseverity} + unless defined $params->{bug_severity}; + $params->{priority} = Bugzilla->params->{defaultpriority} + unless defined $params->{priority}; + $params->{op_sys} = Bugzilla->params->{defaultopsys} + unless defined $params->{op_sys}; + $params->{rep_platform} = Bugzilla->params->{defaultplatform} + unless defined $params->{rep_platform}; + + # Make sure a comment is always defined. + $params->{comment} = '' unless defined $params->{comment}; + + $class->check_required_create_fields($params); + $params = $class->run_create_validators($params); + + # These are not a fields in the bugs table, so we don't pass them to + # insert_create_data. + my $bug_aliases = delete $params->{alias}; + my $cc_ids = delete $params->{cc}; + my $groups = delete $params->{groups}; + my $depends_on = delete $params->{dependson}; + my $blocked = delete $params->{blocked}; + my $keywords = delete $params->{keywords}; + my $creation_comment = delete $params->{comment}; + my $see_also = delete $params->{see_also}; + + # We don't want the bug to appear in the system until it's correctly + # protected by groups. + my $timestamp = delete $params->{creation_ts}; + + my $ms_values = $class->_extract_multi_selects($params); + my $bug = $class->insert_create_data($params); + + # Add the group restrictions + my $sth_group + = $dbh->prepare('INSERT INTO bug_group_map (bug_id, group_id) VALUES (?, ?)'); + foreach my $group (@$groups) { + $sth_group->execute($bug->bug_id, $group->id); + } + + $dbh->do('UPDATE bugs SET creation_ts = ? WHERE bug_id = ?', + undef, $timestamp, $bug->bug_id); + + # Update the bug instance as well + $bug->{creation_ts} = $timestamp; + + # Add the CCs + my $sth_cc = $dbh->prepare('INSERT INTO cc (bug_id, who) VALUES (?,?)'); + foreach my $user_id (@$cc_ids) { + $sth_cc->execute($bug->bug_id, $user_id); + } + + # Add in keywords + my $sth_keyword + = $dbh->prepare('INSERT INTO keywords (bug_id, keywordid) VALUES (?, ?)'); + foreach my $keyword_id (map($_->id, @$keywords)) { + $sth_keyword->execute($bug->bug_id, $keyword_id); + } + + # Set up dependencies (blocked/dependson) + my $sth_deps = $dbh->prepare( + 'INSERT INTO dependencies (blocked, dependson) VALUES (?, ?)'); + my $sth_bug_time + = $dbh->prepare('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?'); + + foreach my $depends_on_id (@$depends_on) { + $sth_deps->execute($bug->bug_id, $depends_on_id); + + # Log the reverse action on the other bug. + LogActivityEntry($depends_on_id, 'blocked', '', $bug->bug_id, + $bug->{reporter_id}, $timestamp); + $sth_bug_time->execute($timestamp, $depends_on_id); + } + foreach my $blocked_id (@$blocked) { + $sth_deps->execute($blocked_id, $bug->bug_id); + + # Log the reverse action on the other bug. + LogActivityEntry($blocked_id, 'dependson', '', $bug->bug_id, + $bug->{reporter_id}, $timestamp); + $sth_bug_time->execute($timestamp, $blocked_id); + } + + # Insert the values into the multiselect value tables + foreach my $field (keys %$ms_values) { + $dbh->do("DELETE FROM bug_$field where bug_id = ?", undef, $bug->bug_id); + foreach my $value (@{$ms_values->{$field}}) { + $dbh->do("INSERT INTO bug_$field (bug_id, value) VALUES (?,?)", + undef, $bug->bug_id, $value); + } + } + + # Insert any see_also values + if ($see_also) { + my $see_also_array = $see_also; + if (!ref $see_also_array) { + $see_also = trim($see_also); + $see_also_array = [split(/[\s,]+/, $see_also)]; + } + foreach my $value (@$see_also_array) { + $bug->add_see_also($value); + } + foreach my $see_also (@{$bug->see_also}) { + $see_also->insert_create_data($see_also); + } + foreach my $ref_bug (@{$bug->{_update_ref_bugs} || []}) { + $ref_bug->update(); + } + delete $bug->{_update_ref_bugs}; + } + + # Comment #0 handling... + + # We now have a bug id so we can fill this out + $creation_comment->{'bug_id'} = $bug->id; + + # Insert the comment. We always insert a comment on bug creation, + # but sometimes it's blank. + Bugzilla::Comment->insert_create_data($creation_comment); + + # Set up aliases + my $sth_aliases + = $dbh->prepare('INSERT INTO bugs_aliases (alias, bug_id) VALUES (?, ?)'); + foreach my $alias (@$bug_aliases) { + trick_taint($alias); + $sth_aliases->execute($alias, $bug->bug_id); + } - Bugzilla::Hook::process('bug_end_of_create', { bug => $bug, - timestamp => $timestamp, - }); + Bugzilla::Hook::process('bug_end_of_create', + {bug => $bug, timestamp => $timestamp,}); - $dbh->bz_commit_transaction(); + $dbh->bz_commit_transaction(); - # Because MySQL doesn't support transactions on the fulltext table, - # we do this after we've committed the transaction. That way we're - # sure we're inserting a good Bug ID. - $bug->_sync_fulltext( new_bug => 1 ); + # Because MySQL doesn't support transactions on the fulltext table, + # we do this after we've committed the transaction. That way we're + # sure we're inserting a good Bug ID. + $bug->_sync_fulltext(new_bug => 1); - return $bug; + return $bug; } sub run_create_validators { - my $class = shift; - my $params = $class->SUPER::run_create_validators(@_); - - # Add classification for checking mandatory fields which depend on it - $params->{classification} = $params->{product}->classification->name; - - my @mandatory_fields = @{ Bugzilla->fields({ is_mandatory => 1, - enter_bug => 1, - obsolete => 0 }) }; - foreach my $field (@mandatory_fields) { - $class->_check_field_is_mandatory($params->{$field->name}, $field, - $params); - } - - my $product = delete $params->{product}; - $params->{product_id} = $product->id; - my $component = delete $params->{component}; - $params->{component_id} = $component->id; - - # Callers cannot set reporter, creation_ts, or delta_ts. - $params->{reporter} = $class->_check_reporter(); - $params->{delta_ts} = $params->{creation_ts}; - - if ($params->{estimated_time}) { - $params->{remaining_time} = $params->{estimated_time}; - } - - $class->_check_strict_isolation($params->{cc}, $params->{assigned_to}, - $params->{qa_contact}, $product); - - # You can't set these fields. - delete $params->{lastdiffed}; - delete $params->{bug_id}; - delete $params->{classification}; - - Bugzilla::Hook::process('bug_end_of_create_validators', - { params => $params }); - - # And this is not a valid DB field, it's just used as part of - # _check_dependencies to avoid running it twice for both blocked - # and dependson. - delete $params->{_dependencies_validated}; - - return $params; -} - -sub update { - my $self = shift; - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; - - # XXX This is just a temporary hack until all updating happens - # inside this function. - my $delta_ts = shift || $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); - - $dbh->bz_start_transaction(); - - my ($changes, $old_bug) = $self->SUPER::update(@_); - - Bugzilla::Hook::process('bug_start_of_update', - { timestamp => $delta_ts, bug => $self, - old_bug => $old_bug, changes => $changes }); - - # Certain items in $changes have to be fixed so that they hold - # a name instead of an ID. - foreach my $field (qw(product_id component_id)) { - my $change = delete $changes->{$field}; - if ($change) { - my $new_field = $field; - $new_field =~ s/_id$//; - $changes->{$new_field} = - [$self->{"_old_${new_field}_name"}, $self->$new_field]; - } - } - foreach my $field (qw(qa_contact assigned_to)) { - if ($changes->{$field}) { - my ($from, $to) = @{ $changes->{$field} }; - $from = $old_bug->$field->login if $from; - $to = $self->$field->login if $to; - $changes->{$field} = [$from, $to]; - } - } - - # CC - my @old_cc = map {$_->id} @{$old_bug->cc_users}; - my @new_cc = map {$_->id} @{$self->cc_users}; - my ($removed_cc, $added_cc) = diff_arrays(\@old_cc, \@new_cc); - - if (scalar @$removed_cc) { - $dbh->do('DELETE FROM cc WHERE bug_id = ? AND ' - . $dbh->sql_in('who', $removed_cc), undef, $self->id); - } - foreach my $user_id (@$added_cc) { - $dbh->do('INSERT INTO cc (bug_id, who) VALUES (?,?)', - undef, $self->id, $user_id); - } - # If any changes were found, record it in the activity log - if (scalar @$removed_cc || scalar @$added_cc) { - my $removed_users = Bugzilla::User->new_from_list($removed_cc); - my $added_users = Bugzilla::User->new_from_list($added_cc); - my $removed_names = join(', ', (map {$_->login} @$removed_users)); - my $added_names = join(', ', (map {$_->login} @$added_users)); - $changes->{cc} = [$removed_names, $added_names]; - } + my $class = shift; + my $params = $class->SUPER::run_create_validators(@_); - # Aliases - my $old_aliases = $old_bug->alias; - my $new_aliases = $self->alias; - my ($removed_aliases, $added_aliases) = diff_arrays($old_aliases, $new_aliases); + # Add classification for checking mandatory fields which depend on it + $params->{classification} = $params->{product}->classification->name; - foreach my $alias (@$removed_aliases) { - $dbh->do('DELETE FROM bugs_aliases WHERE bug_id = ? AND alias = ?', - undef, $self->id, $alias); - } - foreach my $alias (@$added_aliases) { - trick_taint($alias); - $dbh->do('INSERT INTO bugs_aliases (bug_id, alias) VALUES (?,?)', - undef, $self->id, $alias); - } - # If any changes were found, record it in the activity log - if (scalar @$removed_aliases || scalar @$added_aliases) { - $changes->{alias} = [join(', ', @$removed_aliases), join(', ', @$added_aliases)]; - } + my @mandatory_fields + = @{Bugzilla->fields({is_mandatory => 1, enter_bug => 1, obsolete => 0})}; + foreach my $field (@mandatory_fields) { + $class->_check_field_is_mandatory($params->{$field->name}, $field, $params); + } - # Keywords - my @old_kw_ids = map { $_->id } @{$old_bug->keyword_objects}; - my @new_kw_ids = map { $_->id } @{$self->keyword_objects}; + my $product = delete $params->{product}; + $params->{product_id} = $product->id; + my $component = delete $params->{component}; + $params->{component_id} = $component->id; - my ($removed_kw, $added_kw) = diff_arrays(\@old_kw_ids, \@new_kw_ids); + # Callers cannot set reporter, creation_ts, or delta_ts. + $params->{reporter} = $class->_check_reporter(); + $params->{delta_ts} = $params->{creation_ts}; - if (scalar @$removed_kw) { - $dbh->do('DELETE FROM keywords WHERE bug_id = ? AND ' - . $dbh->sql_in('keywordid', $removed_kw), undef, $self->id); - } - foreach my $keyword_id (@$added_kw) { - $dbh->do('INSERT INTO keywords (bug_id, keywordid) VALUES (?,?)', - undef, $self->id, $keyword_id); - } - # If any changes were found, record it in the activity log - if (scalar @$removed_kw || scalar @$added_kw) { - my $removed_keywords = Bugzilla::Keyword->new_from_list($removed_kw); - my $added_keywords = Bugzilla::Keyword->new_from_list($added_kw); - my $removed_names = join(', ', (map {$_->name} @$removed_keywords)); - my $added_names = join(', ', (map {$_->name} @$added_keywords)); - $changes->{keywords} = [$removed_names, $added_names]; - } + if ($params->{estimated_time}) { + $params->{remaining_time} = $params->{estimated_time}; + } - # Dependencies - foreach my $pair ([qw(dependson blocked)], [qw(blocked dependson)]) { - my ($type, $other) = @$pair; - my $old = $old_bug->$type; - my $new = $self->$type; - - my ($removed, $added) = diff_arrays($old, $new); - foreach my $removed_id (@$removed) { - $dbh->do("DELETE FROM dependencies WHERE $type = ? AND $other = ?", - undef, $removed_id, $self->id); - - # Add an activity entry for the other bug. - LogActivityEntry($removed_id, $other, $self->id, '', - $user->id, $delta_ts); - # Update delta_ts on the other bug so that we trigger mid-airs. - $dbh->do('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?', - undef, $delta_ts, $removed_id); - } - foreach my $added_id (@$added) { - $dbh->do("INSERT INTO dependencies ($type, $other) VALUES (?,?)", - undef, $added_id, $self->id); - - # Add an activity entry for the other bug. - LogActivityEntry($added_id, $other, '', $self->id, - $user->id, $delta_ts); - # Update delta_ts on the other bug so that we trigger mid-airs. - $dbh->do('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?', - undef, $delta_ts, $added_id); - } - - if (scalar(@$removed) || scalar(@$added)) { - $changes->{$type} = [join(', ', @$removed), join(', ', @$added)]; - } - } + $class->_check_strict_isolation($params->{cc}, $params->{assigned_to}, + $params->{qa_contact}, $product); - # Groups - my %old_groups = map {$_->id => $_} @{$old_bug->groups_in}; - my %new_groups = map {$_->id => $_} @{$self->groups_in}; - my ($removed_gr, $added_gr) = diff_arrays([keys %old_groups], - [keys %new_groups]); - if (scalar @$removed_gr || scalar @$added_gr) { - if (@$removed_gr) { - my $qmarks = join(',', ('?') x @$removed_gr); - $dbh->do("DELETE FROM bug_group_map - WHERE bug_id = ? AND group_id IN ($qmarks)", undef, - $self->id, @$removed_gr); - } - my $sth_insert = $dbh->prepare( - 'INSERT INTO bug_group_map (bug_id, group_id) VALUES (?,?)'); - foreach my $gid (@$added_gr) { - $sth_insert->execute($self->id, $gid); - } - my @removed_names = map { $old_groups{$_}->name } @$removed_gr; - my @added_names = map { $new_groups{$_}->name } @$added_gr; - $changes->{'bug_group'} = [join(', ', @removed_names), - join(', ', @added_names)]; - } + # You can't set these fields. + delete $params->{lastdiffed}; + delete $params->{bug_id}; + delete $params->{classification}; - # Comments - foreach my $comment (@{$self->{added_comments} || []}) { - # Override the Comment's timestamp to be identical to the update - # timestamp. - $comment->{bug_when} = $delta_ts; - $comment = Bugzilla::Comment->insert_create_data($comment); - if ($comment->work_time) { - LogActivityEntry($self->id, "work_time", "", $comment->work_time, - $user->id, $delta_ts); - } - } + Bugzilla::Hook::process('bug_end_of_create_validators', {params => $params}); - # Comment Privacy - foreach my $comment (@{$self->{comment_isprivate} || []}) { - $comment->update(); - - my ($from, $to) - = $comment->is_private ? (0, 1) : (1, 0); - LogActivityEntry($self->id, "longdescs.isprivate", $from, $to, - $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; - foreach my $field (@multi_selects) { - my $name = $field->name; - my ($removed, $added) = diff_arrays($old_bug->$name, $self->$name); - if (scalar @$removed || scalar @$added) { - $changes->{$name} = [join(', ', @$removed), join(', ', @$added)]; - - $dbh->do("DELETE FROM bug_$name where bug_id = ?", - undef, $self->id); - foreach my $value (@{$self->$name}) { - $dbh->do("INSERT INTO bug_$name (bug_id, value) VALUES (?,?)", - undef, $self->id, $value); - } - } - } - - # See Also - - my ($removed_see, $added_see) = - diff_arrays($old_bug->see_also, $self->see_also, 'name'); - - $_->remove_from_db foreach @$removed_see; - $_->insert_create_data($_) foreach @$added_see; + # And this is not a valid DB field, it's just used as part of + # _check_dependencies to avoid running it twice for both blocked + # and dependson. + delete $params->{_dependencies_validated}; - # If any changes were found, record it in the activity log - if (scalar @$removed_see || scalar @$added_see) { - $changes->{see_also} = [join(', ', map { $_->name } @$removed_see), - join(', ', map { $_->name } @$added_see)]; - } + return $params; +} - # Flags - my ($removed, $added) = Bugzilla::Flag->update_flags($self, $old_bug, $delta_ts); - if ($removed || $added) { - $changes->{'flagtypes.name'} = [$removed, $added]; - } +sub update { + my $self = shift; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; - $_->update foreach @{ $self->{_update_ref_bugs} || [] }; - delete $self->{_update_ref_bugs}; - - # Log bugs_activity items - # XXX Eventually, when bugs_activity is able to track the dupe_id, - # this code should go below the duplicates-table-updating code below. - foreach my $field (keys %$changes) { - my $change = $changes->{$field}; - my $from = defined $change->[0] ? $change->[0] : ''; - my $to = defined $change->[1] ? $change->[1] : ''; - LogActivityEntry($self->id, $field, $from, $to, - $user->id, $delta_ts); - } + # XXX This is just a temporary hack until all updating happens + # inside this function. + my $delta_ts = shift || $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); - # Check if we have to update the duplicates table and the other bug. - my ($old_dup, $cur_dup) = ($old_bug->dup_id || 0, $self->dup_id || 0); - if ($old_dup != $cur_dup) { - $dbh->do("DELETE FROM duplicates WHERE dupe = ?", undef, $self->id); - if ($cur_dup) { - $dbh->do('INSERT INTO duplicates (dupe, dupe_of) VALUES (?,?)', - undef, $self->id, $cur_dup); - if (my $update_dup = delete $self->{_dup_for_update}) { - $update_dup->update(); - } - } + $dbh->bz_start_transaction(); - $changes->{'dup_id'} = [$old_dup || undef, $cur_dup || undef]; - } + my ($changes, $old_bug) = $self->SUPER::update(@_); - Bugzilla::Hook::process('bug_end_of_update', - { bug => $self, timestamp => $delta_ts, changes => $changes, - old_bug => $old_bug }); - - # If any change occurred, refresh the timestamp of the bug. - if (scalar(keys %$changes) || $self->{added_comments} - || $self->{comment_isprivate}) + Bugzilla::Hook::process( + 'bug_start_of_update', { - $dbh->do('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?', - undef, ($delta_ts, $self->id)); - $self->{delta_ts} = $delta_ts; - } - - # Update last-visited - if ($user->is_involved_in_bug($self)) { - $self->update_user_last_visit($user, $delta_ts); - } - - # If a user is no longer involved, remove their last visit entry - my $last_visits = - Bugzilla::BugUserLastVisit->match({ bug_id => $self->id }); - foreach my $lv (@$last_visits) { - $lv->remove_from_db() unless $lv->user->is_involved_in_bug($self); - } - - # Update bug ignore data if user wants to ignore mail for this bug - if (exists $self->{'bug_ignored'}) { - my $bug_ignored_changed; - if ($self->{'bug_ignored'} && !$user->is_bug_ignored($self->id)) { - $dbh->do('INSERT INTO email_bug_ignore - (user_id, bug_id) VALUES (?, ?)', - undef, $user->id, $self->id); - $bug_ignored_changed = 1; - - } - elsif (!$self->{'bug_ignored'} && $user->is_bug_ignored($self->id)) { - $dbh->do('DELETE FROM email_bug_ignore - WHERE user_id = ? AND bug_id = ?', - undef, $user->id, $self->id); - $bug_ignored_changed = 1; - } - delete $user->{bugs_ignored} if $bug_ignored_changed; - } - - $dbh->bz_commit_transaction(); - - # The only problem with this here is that update() is often called - # in the middle of a transaction, and if that transaction is rolled - # back, this change will *not* be rolled back. As we expect rollbacks - # to be extremely rare, that is OK for us. - $self->_sync_fulltext( - update_short_desc => $changes->{short_desc}, - update_comments => $self->{added_comments} || $self->{comment_isprivate} + timestamp => $delta_ts, + bug => $self, + old_bug => $old_bug, + changes => $changes + } + ); + + # Certain items in $changes have to be fixed so that they hold + # a name instead of an ID. + foreach my $field (qw(product_id component_id)) { + my $change = delete $changes->{$field}; + if ($change) { + my $new_field = $field; + $new_field =~ s/_id$//; + $changes->{$new_field} = [$self->{"_old_${new_field}_name"}, $self->$new_field]; + } + } + foreach my $field (qw(qa_contact assigned_to)) { + if ($changes->{$field}) { + my ($from, $to) = @{$changes->{$field}}; + $from = $old_bug->$field->login if $from; + $to = $self->$field->login if $to; + $changes->{$field} = [$from, $to]; + } + } + + # CC + my @old_cc = map { $_->id } @{$old_bug->cc_users}; + my @new_cc = map { $_->id } @{$self->cc_users}; + my ($removed_cc, $added_cc) = diff_arrays(\@old_cc, \@new_cc); + + if (scalar @$removed_cc) { + $dbh->do( + 'DELETE FROM cc WHERE bug_id = ? AND ' . $dbh->sql_in('who', $removed_cc), + undef, $self->id); + } + foreach my $user_id (@$added_cc) { + $dbh->do('INSERT INTO cc (bug_id, who) VALUES (?,?)', + undef, $self->id, $user_id); + } + + # If any changes were found, record it in the activity log + if (scalar @$removed_cc || scalar @$added_cc) { + my $removed_users = Bugzilla::User->new_from_list($removed_cc); + my $added_users = Bugzilla::User->new_from_list($added_cc); + my $removed_names = join(', ', (map { $_->login } @$removed_users)); + my $added_names = join(', ', (map { $_->login } @$added_users)); + $changes->{cc} = [$removed_names, $added_names]; + } + + # Aliases + my $old_aliases = $old_bug->alias; + my $new_aliases = $self->alias; + my ($removed_aliases, $added_aliases) = diff_arrays($old_aliases, $new_aliases); + + foreach my $alias (@$removed_aliases) { + $dbh->do('DELETE FROM bugs_aliases WHERE bug_id = ? AND alias = ?', + undef, $self->id, $alias); + } + foreach my $alias (@$added_aliases) { + trick_taint($alias); + $dbh->do('INSERT INTO bugs_aliases (bug_id, alias) VALUES (?,?)', + undef, $self->id, $alias); + } + + # If any changes were found, record it in the activity log + if (scalar @$removed_aliases || scalar @$added_aliases) { + $changes->{alias} + = [join(', ', @$removed_aliases), join(', ', @$added_aliases)]; + } + + # Keywords + my @old_kw_ids = map { $_->id } @{$old_bug->keyword_objects}; + my @new_kw_ids = map { $_->id } @{$self->keyword_objects}; + + my ($removed_kw, $added_kw) = diff_arrays(\@old_kw_ids, \@new_kw_ids); + + if (scalar @$removed_kw) { + $dbh->do( + 'DELETE FROM keywords WHERE bug_id = ? AND ' + . $dbh->sql_in('keywordid', $removed_kw), + undef, $self->id ); - - # Remove obsolete internal variables. - delete $self->{'_old_assigned_to'}; - delete $self->{'_old_qa_contact'}; - - # Also flush the visible_bugs cache for this bug as the user's - # relationship with this bug may have changed. - delete $user->{_visible_bugs_cache}->{$self->id}; - - return $changes; + } + foreach my $keyword_id (@$added_kw) { + $dbh->do('INSERT INTO keywords (bug_id, keywordid) VALUES (?,?)', + undef, $self->id, $keyword_id); + } + + # If any changes were found, record it in the activity log + if (scalar @$removed_kw || scalar @$added_kw) { + my $removed_keywords = Bugzilla::Keyword->new_from_list($removed_kw); + my $added_keywords = Bugzilla::Keyword->new_from_list($added_kw); + my $removed_names = join(', ', (map { $_->name } @$removed_keywords)); + my $added_names = join(', ', (map { $_->name } @$added_keywords)); + $changes->{keywords} = [$removed_names, $added_names]; + } + + # Dependencies + foreach my $pair ([qw(dependson blocked)], [qw(blocked dependson)]) { + my ($type, $other) = @$pair; + my $old = $old_bug->$type; + my $new = $self->$type; + + my ($removed, $added) = diff_arrays($old, $new); + foreach my $removed_id (@$removed) { + $dbh->do("DELETE FROM dependencies WHERE $type = ? AND $other = ?", + undef, $removed_id, $self->id); + + # Add an activity entry for the other bug. + LogActivityEntry($removed_id, $other, $self->id, '', $user->id, $delta_ts); + + # Update delta_ts on the other bug so that we trigger mid-airs. + $dbh->do('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?', + undef, $delta_ts, $removed_id); + } + foreach my $added_id (@$added) { + $dbh->do("INSERT INTO dependencies ($type, $other) VALUES (?,?)", + undef, $added_id, $self->id); + + # Add an activity entry for the other bug. + LogActivityEntry($added_id, $other, '', $self->id, $user->id, $delta_ts); + + # Update delta_ts on the other bug so that we trigger mid-airs. + $dbh->do('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?', + undef, $delta_ts, $added_id); + } + + if (scalar(@$removed) || scalar(@$added)) { + $changes->{$type} = [join(', ', @$removed), join(', ', @$added)]; + } + } + + # Groups + my %old_groups = map { $_->id => $_ } @{$old_bug->groups_in}; + my %new_groups = map { $_->id => $_ } @{$self->groups_in}; + my ($removed_gr, $added_gr) + = diff_arrays([keys %old_groups], [keys %new_groups]); + if (scalar @$removed_gr || scalar @$added_gr) { + if (@$removed_gr) { + my $qmarks = join(',', ('?') x @$removed_gr); + $dbh->do( + "DELETE FROM bug_group_map + WHERE bug_id = ? AND group_id IN ($qmarks)", undef, $self->id, + @$removed_gr + ); + } + my $sth_insert + = $dbh->prepare('INSERT INTO bug_group_map (bug_id, group_id) VALUES (?,?)'); + foreach my $gid (@$added_gr) { + $sth_insert->execute($self->id, $gid); + } + my @removed_names = map { $old_groups{$_}->name } @$removed_gr; + my @added_names = map { $new_groups{$_}->name } @$added_gr; + $changes->{'bug_group'} + = [join(', ', @removed_names), join(', ', @added_names)]; + } + + # Comments + foreach my $comment (@{$self->{added_comments} || []}) { + + # Override the Comment's timestamp to be identical to the update + # timestamp. + $comment->{bug_when} = $delta_ts; + $comment = Bugzilla::Comment->insert_create_data($comment); + if ($comment->work_time) { + LogActivityEntry($self->id, "work_time", "", $comment->work_time, $user->id, + $delta_ts); + } + } + + # Comment Privacy + foreach my $comment (@{$self->{comment_isprivate} || []}) { + $comment->update(); + + my ($from, $to) = $comment->is_private ? (0, 1) : (1, 0); + LogActivityEntry($self->id, "longdescs.isprivate", $from, $to, $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; + foreach my $field (@multi_selects) { + my $name = $field->name; + my ($removed, $added) = diff_arrays($old_bug->$name, $self->$name); + if (scalar @$removed || scalar @$added) { + $changes->{$name} = [join(', ', @$removed), join(', ', @$added)]; + + $dbh->do("DELETE FROM bug_$name where bug_id = ?", undef, $self->id); + foreach my $value (@{$self->$name}) { + $dbh->do("INSERT INTO bug_$name (bug_id, value) VALUES (?,?)", + undef, $self->id, $value); + } + } + } + + # See Also + + my ($removed_see, $added_see) + = diff_arrays($old_bug->see_also, $self->see_also, 'name'); + + $_->remove_from_db foreach @$removed_see; + $_->insert_create_data($_) foreach @$added_see; + + # If any changes were found, record it in the activity log + if (scalar @$removed_see || scalar @$added_see) { + $changes->{see_also} = [ + join(', ', map { $_->name } @$removed_see), + 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}; + + # Log bugs_activity items + # XXX Eventually, when bugs_activity is able to track the dupe_id, + # this code should go below the duplicates-table-updating code below. + foreach my $field (keys %$changes) { + my $change = $changes->{$field}; + my $from = defined $change->[0] ? $change->[0] : ''; + my $to = defined $change->[1] ? $change->[1] : ''; + LogActivityEntry($self->id, $field, $from, $to, $user->id, $delta_ts); + } + + # Check if we have to update the duplicates table and the other bug. + my ($old_dup, $cur_dup) = ($old_bug->dup_id || 0, $self->dup_id || 0); + if ($old_dup != $cur_dup) { + $dbh->do("DELETE FROM duplicates WHERE dupe = ?", undef, $self->id); + if ($cur_dup) { + $dbh->do('INSERT INTO duplicates (dupe, dupe_of) VALUES (?,?)', + undef, $self->id, $cur_dup); + if (my $update_dup = delete $self->{_dup_for_update}) { + $update_dup->update(); + } + } + + $changes->{'dup_id'} = [$old_dup || undef, $cur_dup || undef]; + } + + Bugzilla::Hook::process( + 'bug_end_of_update', + { + bug => $self, + timestamp => $delta_ts, + changes => $changes, + old_bug => $old_bug + } + ); + + # If any change occurred, refresh the timestamp of the bug. + if ( scalar(keys %$changes) + || $self->{added_comments} + || $self->{comment_isprivate}) + { + $dbh->do('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?', + undef, ($delta_ts, $self->id)); + $self->{delta_ts} = $delta_ts; + } + + # Update last-visited + if ($user->is_involved_in_bug($self)) { + $self->update_user_last_visit($user, $delta_ts); + } + + # If a user is no longer involved, remove their last visit entry + my $last_visits = Bugzilla::BugUserLastVisit->match({bug_id => $self->id}); + foreach my $lv (@$last_visits) { + $lv->remove_from_db() unless $lv->user->is_involved_in_bug($self); + } + + # Update bug ignore data if user wants to ignore mail for this bug + if (exists $self->{'bug_ignored'}) { + my $bug_ignored_changed; + if ($self->{'bug_ignored'} && !$user->is_bug_ignored($self->id)) { + $dbh->do( + 'INSERT INTO email_bug_ignore + (user_id, bug_id) VALUES (?, ?)', undef, $user->id, $self->id + ); + $bug_ignored_changed = 1; + + } + elsif (!$self->{'bug_ignored'} && $user->is_bug_ignored($self->id)) { + $dbh->do( + 'DELETE FROM email_bug_ignore + WHERE user_id = ? AND bug_id = ?', undef, $user->id, $self->id + ); + $bug_ignored_changed = 1; + } + delete $user->{bugs_ignored} if $bug_ignored_changed; + } + + $dbh->bz_commit_transaction(); + + # The only problem with this here is that update() is often called + # in the middle of a transaction, and if that transaction is rolled + # back, this change will *not* be rolled back. As we expect rollbacks + # to be extremely rare, that is OK for us. + $self->_sync_fulltext( + update_short_desc => $changes->{short_desc}, + update_comments => $self->{added_comments} || $self->{comment_isprivate} + ); + + # Remove obsolete internal variables. + delete $self->{'_old_assigned_to'}; + delete $self->{'_old_qa_contact'}; + + # Also flush the visible_bugs cache for this bug as the user's + # relationship with this bug may have changed. + delete $user->{_visible_bugs_cache}->{$self->id}; + + return $changes; } # Used by create(). # We need to handle multi-select fields differently than normal fields, # because they're arrays and don't go into the bugs table. sub _extract_multi_selects { - my ($invocant, $params) = @_; - - my @multi_selects = grep {$_->type == FIELD_TYPE_MULTI_SELECT} - Bugzilla->active_custom_fields; - my %ms_values; - foreach my $field (@multi_selects) { - my $name = $field->name; - if (exists $params->{$name}) { - my $array = delete($params->{$name}) || []; - $ms_values{$name} = $array; - } + my ($invocant, $params) = @_; + + my @multi_selects + = grep { $_->type == FIELD_TYPE_MULTI_SELECT } Bugzilla->active_custom_fields; + my %ms_values; + foreach my $field (@multi_selects) { + my $name = $field->name; + if (exists $params->{$name}) { + my $array = delete($params->{$name}) || []; + $ms_values{$name} = $array; } - return \%ms_values; + } + return \%ms_values; } # Should be called any time you update short_desc or change a comment. sub _sync_fulltext { - my ($self, %options) = @_; - my $dbh = Bugzilla->dbh; - - my($all_comments, $public_comments); - if ($options{new_bug} || $options{update_comments}) { - my $comments = $dbh->selectall_arrayref( - 'SELECT thetext, isprivate FROM longdescs WHERE bug_id = ?', - undef, $self->id); - $all_comments = join("\n", map { $_->[0] } @$comments); - my @no_private = grep { !$_->[1] } @$comments; - $public_comments = join("\n", map { $_->[0] } @no_private); - } - - if ($options{new_bug}) { - $dbh->do('INSERT INTO bugs_fulltext (bug_id, short_desc, comments, + my ($self, %options) = @_; + my $dbh = Bugzilla->dbh; + + my ($all_comments, $public_comments); + if ($options{new_bug} || $options{update_comments}) { + my $comments + = $dbh->selectall_arrayref( + 'SELECT thetext, isprivate FROM longdescs WHERE bug_id = ?', + undef, $self->id); + $all_comments = join("\n", map { $_->[0] } @$comments); + my @no_private = grep { !$_->[1] } @$comments; + $public_comments = join("\n", map { $_->[0] } @no_private); + } + + if ($options{new_bug}) { + $dbh->do( + 'INSERT INTO bugs_fulltext (bug_id, short_desc, comments, comments_noprivate) - VALUES (?, ?, ?, ?)', - undef, - $self->id, $self->short_desc, $all_comments, $public_comments); - } else { - my(@names, @values); - if ($options{update_short_desc}) { - push @names, 'short_desc'; - push @values, $self->short_desc; - } - if ($options{update_comments}) { - push @names, ('comments', 'comments_noprivate'); - push @values, ($all_comments, $public_comments); - } - if (@names) { - $dbh->do('UPDATE bugs_fulltext SET ' . - join(', ', map { "$_ = ?" } @names) . - ' WHERE bug_id = ?', - undef, - @values, $self->id); - } + VALUES (?, ?, ?, ?)', undef, $self->id, $self->short_desc, + $all_comments, $public_comments + ); + } + else { + my (@names, @values); + if ($options{update_short_desc}) { + push @names, 'short_desc'; + push @values, $self->short_desc; + } + if ($options{update_comments}) { + push @names, ('comments', 'comments_noprivate'); + push @values, ($all_comments, $public_comments); } + if (@names) { + $dbh->do( + 'UPDATE bugs_fulltext SET ' + . join(', ', map {"$_ = ?"} @names) + . ' WHERE bug_id = ?', + undef, @values, $self->id + ); + } + } } sub remove_from_db { - my ($self) = @_; - my $dbh = Bugzilla->dbh; + my ($self) = @_; + my $dbh = Bugzilla->dbh; + + ThrowCodeError("bug_error", {bug => $self}) if $self->{'error'}; - ThrowCodeError("bug_error", { bug => $self }) if $self->{'error'}; + my $bug_id = $self->{'bug_id'}; + $self->SUPER::remove_from_db(); - my $bug_id = $self->{'bug_id'}; - $self->SUPER::remove_from_db(); - # The bugs_fulltext table doesn't support foreign keys. - $dbh->do("DELETE FROM bugs_fulltext WHERE bug_id = ?", undef, $bug_id); + # The bugs_fulltext table doesn't support foreign keys. + $dbh->do("DELETE FROM bugs_fulltext WHERE bug_id = ?", undef, $bug_id); } ##################################################################### @@ -1272,96 +1315,93 @@ sub remove_from_db { ##################################################################### sub send_changes { - my ($self, $changes, $vars) = @_; - - my $user = Bugzilla->user; - - my $old_qa = $changes->{'qa_contact'} - ? $changes->{'qa_contact'}->[0] : ''; - my $old_own = $changes->{'assigned_to'} - ? $changes->{'assigned_to'}->[0] : ''; - my $old_cc = $changes->{cc} - ? $changes->{cc}->[0] : ''; - - my %forced = ( - cc => [split(/[,;]+/, $old_cc)], - owner => $old_own, - qacontact => $old_qa, - changer => $user, - ); - - _send_bugmail({ id => $self->id, type => 'bug', forced => \%forced }, - $vars); - - # If the bug was marked as a duplicate, we need to notify users on the - # other bug of any changes to that bug. - my $new_dup_id = $changes->{'dup_id'} ? $changes->{'dup_id'}->[1] : undef; - if ($new_dup_id) { - _send_bugmail({ forced => { changer => $user }, type => "dupe", - id => $new_dup_id }, $vars); - } - - # If there were changes in dependencies, we need to notify those - # dependencies. - if ($changes->{'bug_status'}) { - my ($old_status, $new_status) = @{ $changes->{'bug_status'} }; - - # If this bug has changed from opened to closed or vice-versa, - # then all of the bugs we block need to be notified. - if (is_open_state($old_status) ne is_open_state($new_status)) { - my $params = { forced => { changer => $user }, - type => 'dep', - dep_only => 1, - blocker => $self, - changes => $changes }; - - foreach my $id (@{ $self->blocked }) { - $params->{id} = $id; - _send_bugmail($params, $vars); - } - } - } - - # To get a list of all changed dependencies, convert the "changes" arrays - # into a long string, then collapse that string into unique numbers in - # a hash. - my $all_changed_deps = join(', ', @{ $changes->{'dependson'} || [] }); - $all_changed_deps = join(', ', @{ $changes->{'blocked'} || [] }, - $all_changed_deps); - my %changed_deps = map { $_ => 1 } split(', ', $all_changed_deps); - # When clearning one field (say, blocks) and filling in the other - # (say, dependson), an empty string can get into the hash and cause - # an error later. - delete $changed_deps{''}; - - foreach my $id (sort { $a <=> $b } (keys %changed_deps)) { - _send_bugmail({ forced => { changer => $user }, type => "dep", - id => $id }, $vars); - } - - # Sending emails for the referenced bugs. - foreach my $ref_bug_id (uniq @{ $self->{see_also_changes} || [] }) { - _send_bugmail({ forced => { changer => $user }, - id => $ref_bug_id }, $vars); - } + my ($self, $changes, $vars) = @_; + + my $user = Bugzilla->user; + + my $old_qa = $changes->{'qa_contact'} ? $changes->{'qa_contact'}->[0] : ''; + my $old_own = $changes->{'assigned_to'} ? $changes->{'assigned_to'}->[0] : ''; + my $old_cc = $changes->{cc} ? $changes->{cc}->[0] : ''; + + my %forced = ( + cc => [split(/[,;]+/, $old_cc)], + owner => $old_own, + qacontact => $old_qa, + changer => $user, + ); + + _send_bugmail({id => $self->id, type => 'bug', forced => \%forced}, $vars); + + # If the bug was marked as a duplicate, we need to notify users on the + # other bug of any changes to that bug. + my $new_dup_id = $changes->{'dup_id'} ? $changes->{'dup_id'}->[1] : undef; + if ($new_dup_id) { + _send_bugmail({forced => {changer => $user}, type => "dupe", id => $new_dup_id}, + $vars); + } + + # If there were changes in dependencies, we need to notify those + # dependencies. + if ($changes->{'bug_status'}) { + my ($old_status, $new_status) = @{$changes->{'bug_status'}}; + + # If this bug has changed from opened to closed or vice-versa, + # then all of the bugs we block need to be notified. + if (is_open_state($old_status) ne is_open_state($new_status)) { + my $params = { + forced => {changer => $user}, + type => 'dep', + dep_only => 1, + blocker => $self, + changes => $changes + }; + + foreach my $id (@{$self->blocked}) { + $params->{id} = $id; + _send_bugmail($params, $vars); + } + } + } + + # To get a list of all changed dependencies, convert the "changes" arrays + # into a long string, then collapse that string into unique numbers in + # a hash. + my $all_changed_deps = join(', ', @{$changes->{'dependson'} || []}); + $all_changed_deps + = join(', ', @{$changes->{'blocked'} || []}, $all_changed_deps); + my %changed_deps = map { $_ => 1 } split(', ', $all_changed_deps); + + # When clearning one field (say, blocks) and filling in the other + # (say, dependson), an empty string can get into the hash and cause + # an error later. + delete $changed_deps{''}; + + foreach my $id (sort { $a <=> $b } (keys %changed_deps)) { + _send_bugmail({forced => {changer => $user}, type => "dep", id => $id}, $vars); + } + + # Sending emails for the referenced bugs. + foreach my $ref_bug_id (uniq @{$self->{see_also_changes} || []}) { + _send_bugmail({forced => {changer => $user}, id => $ref_bug_id}, $vars); + } } sub _send_bugmail { - my ($params, $vars) = @_; + my ($params, $vars) = @_; - require Bugzilla::BugMail; + require Bugzilla::BugMail; - my $results = - Bugzilla::BugMail::Send($params->{'id'}, $params->{'forced'}, $params); + my $results + = Bugzilla::BugMail::Send($params->{'id'}, $params->{'forced'}, $params); - if (Bugzilla->usage_mode == USAGE_MODE_BROWSER) { - my $template = Bugzilla->template; - $vars->{$_} = $params->{$_} foreach keys %$params; - $vars->{'sent_bugmail'} = $results; - $template->process("bug/process/results.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - $vars->{'header_done'} = 1; - } + if (Bugzilla->usage_mode == USAGE_MODE_BROWSER) { + my $template = Bugzilla->template; + $vars->{$_} = $params->{$_} foreach keys %$params; + $vars->{'sent_bugmail'} = $results; + $template->process("bug/process/results.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + $vars->{'header_done'} = 1; + } } ##################################################################### @@ -1369,928 +1409,956 @@ sub _send_bugmail { ##################################################################### sub _check_alias { - my ($invocant, $aliases) = @_; - $aliases = ref $aliases ? $aliases : [split(/[\s,]+/, $aliases)]; + my ($invocant, $aliases) = @_; + $aliases = ref $aliases ? $aliases : [split(/[\s,]+/, $aliases)]; - # Remove empty aliases - @$aliases = grep { $_ } @$aliases; + # Remove empty aliases + @$aliases = grep {$_} @$aliases; - foreach my $alias (@$aliases) { - $alias = trim($alias); + foreach my $alias (@$aliases) { + $alias = trim($alias); - # Make sure the alias isn't too long. - if (length($alias) > 40) { - ThrowUserError("alias_too_long"); - } - # Make sure the alias isn't just a number. - if ($alias =~ /^\d+$/) { - ThrowUserError("alias_is_numeric", { alias => $alias }); - } - # Make sure the alias has no commas or spaces. - if ($alias =~ /[, ]/) { - ThrowUserError("alias_has_comma_or_space", { alias => $alias }); - } - # Make sure the alias is unique, or that it's already our alias. - my $other_bug = new Bugzilla::Bug($alias); - if (!$other_bug->{error} - && (!ref $invocant || $other_bug->id != $invocant->id)) - { - ThrowUserError("alias_in_use", { alias => $alias, - bug_id => $other_bug->id }); - } + # Make sure the alias isn't too long. + if (length($alias) > 40) { + ThrowUserError("alias_too_long"); } - return $aliases; -} + # Make sure the alias isn't just a number. + if ($alias =~ /^\d+$/) { + ThrowUserError("alias_is_numeric", {alias => $alias}); + } -sub _check_assigned_to { - my ($invocant, $assignee, undef, $params) = @_; - my $user = Bugzilla->user; - my $component = blessed($invocant) ? $invocant->component_obj - : $params->{component}; - - # Default assignee is the component owner. - my $id; - # If this is a new bug, you can only set the assignee if you have editbugs. - # If you didn't specify the assignee, we use the default assignee. - if (!ref $invocant - && (!$user->in_group('editbugs', $component->product_id) || !$assignee)) + # Make sure the alias has no commas or spaces. + if ($alias =~ /[, ]/) { + ThrowUserError("alias_has_comma_or_space", {alias => $alias}); + } + + # Make sure the alias is unique, or that it's already our alias. + my $other_bug = new Bugzilla::Bug($alias); + if (!$other_bug->{error} && (!ref $invocant || $other_bug->id != $invocant->id)) { - $id = $component->default_assignee->id; - } else { - if (!ref $assignee) { - $assignee = trim($assignee); - # When updating a bug, assigned_to can't be empty. - ThrowUserError("reassign_to_empty") if ref $invocant && !$assignee; - $assignee = Bugzilla::User->check($assignee); - } - $id = $assignee->id; - # create() checks this another way, so we don't have to run this - # check during create(). - $invocant->_check_strict_isolation_for_user($assignee) if ref $invocant; + ThrowUserError("alias_in_use", {alias => $alias, bug_id => $other_bug->id}); } - return $id; + } + + return $aliases; } -sub _check_bug_file_loc { - my ($invocant, $url) = @_; - $url = '' if !defined($url); - $url = trim($url); - # On bug entry, if bug_file_loc is "http://", the default, use an - # empty value instead. However, on bug editing people can set that - # back if they *really* want to. - if (!ref $invocant && $url eq 'http://') { - $url = ''; - } - return $url; +sub _check_assigned_to { + my ($invocant, $assignee, undef, $params) = @_; + my $user = Bugzilla->user; + my $component + = blessed($invocant) ? $invocant->component_obj : $params->{component}; + + # Default assignee is the component owner. + my $id; + + # If this is a new bug, you can only set the assignee if you have editbugs. + # If you didn't specify the assignee, we use the default assignee. + if (!ref $invocant + && (!$user->in_group('editbugs', $component->product_id) || !$assignee)) + { + $id = $component->default_assignee->id; + } + else { + if (!ref $assignee) { + $assignee = trim($assignee); + + # When updating a bug, assigned_to can't be empty. + ThrowUserError("reassign_to_empty") if ref $invocant && !$assignee; + $assignee = Bugzilla::User->check($assignee); + } + $id = $assignee->id; + + # create() checks this another way, so we don't have to run this + # check during create(). + $invocant->_check_strict_isolation_for_user($assignee) if ref $invocant; + } + return $id; } -sub _check_bug_status { - my ($invocant, $new_status, undef, $params) = @_; - my $user = Bugzilla->user; - my @valid_statuses; - my $old_status; # Note that this is undef for new bugs. +sub _check_bug_file_loc { + my ($invocant, $url) = @_; + $url = '' if !defined($url); + $url = trim($url); - my ($product, $comment); - if (ref $invocant) { - @valid_statuses = @{$invocant->statuses_available}; - $product = $invocant->product_obj; - $old_status = $invocant->status; - my $comments = $invocant->{added_comments} || []; - $comment = $comments->[-1]; - } - else { - $product = $params->{product}; - $comment = $params->{comment}; - @valid_statuses = @{ Bugzilla::Bug->statuses_available($product) }; - } + # On bug entry, if bug_file_loc is "http://", the default, use an + # empty value instead. However, on bug editing people can set that + # back if they *really* want to. + if (!ref $invocant && $url eq 'http://') { + $url = ''; + } + return $url; +} - # Check permissions for users filing new bugs. - if (!ref $invocant) { - if ($user->in_group('editbugs', $product->id) - || $user->in_group('canconfirm', $product->id)) { - # If the user with privs hasn't selected another status, - # select the first one of the list. - unless ($new_status) { - if (scalar(@valid_statuses) == 1) { - $new_status = $valid_statuses[0]; - } - else { - $new_status = ($valid_statuses[0]->name ne 'UNCONFIRMED') ? - $valid_statuses[0] : $valid_statuses[1]; - } - } +sub _check_bug_status { + my ($invocant, $new_status, undef, $params) = @_; + my $user = Bugzilla->user; + my @valid_statuses; + my $old_status; # Note that this is undef for new bugs. + + my ($product, $comment); + if (ref $invocant) { + @valid_statuses = @{$invocant->statuses_available}; + $product = $invocant->product_obj; + $old_status = $invocant->status; + my $comments = $invocant->{added_comments} || []; + $comment = $comments->[-1]; + } + else { + $product = $params->{product}; + $comment = $params->{comment}; + @valid_statuses = @{Bugzilla::Bug->statuses_available($product)}; + } + + # Check permissions for users filing new bugs. + if (!ref $invocant) { + if ( $user->in_group('editbugs', $product->id) + || $user->in_group('canconfirm', $product->id)) + { + # If the user with privs hasn't selected another status, + # select the first one of the list. + unless ($new_status) { + if (scalar(@valid_statuses) == 1) { + $new_status = $valid_statuses[0]; } else { - # A user with no privs cannot choose the initial status. - # If UNCONFIRMED is valid for this product, use it; else - # use the first bug status available. - if (grep {$_->name eq 'UNCONFIRMED'} @valid_statuses) { - $new_status = 'UNCONFIRMED'; - } - else { - $new_status = $valid_statuses[0]; - } + $new_status + = ($valid_statuses[0]->name ne 'UNCONFIRMED') + ? $valid_statuses[0] + : $valid_statuses[1]; } + } } + else { + # A user with no privs cannot choose the initial status. + # If UNCONFIRMED is valid for this product, use it; else + # use the first bug status available. + if (grep { $_->name eq 'UNCONFIRMED' } @valid_statuses) { + $new_status = 'UNCONFIRMED'; + } + else { + $new_status = $valid_statuses[0]; + } + } + } + + # Time to validate the bug status. + $new_status = Bugzilla::Status->check($new_status) unless ref($new_status); + + # We skip this check if we are changing from a status to itself. + if ((!$old_status || $old_status->id != $new_status->id) + && !grep { $_->name eq $new_status->name } @valid_statuses) + { + ThrowUserError('illegal_bug_status_transition', + {old => $old_status, new => $new_status}); + } + + # Check if a comment is required for this change. + if ($new_status->comment_required_on_change_from($old_status) + && !$comment->{'thetext'}) + { + ThrowUserError( + 'comment_required', + { + old => $old_status ? $old_status->name : undef, + new => $new_status->name, + field => 'bug_status' + } + ); + } - # Time to validate the bug status. - $new_status = Bugzilla::Status->check($new_status) unless ref($new_status); - # We skip this check if we are changing from a status to itself. - if ( (!$old_status || $old_status->id != $new_status->id) - && !grep {$_->name eq $new_status->name} @valid_statuses) - { - ThrowUserError('illegal_bug_status_transition', - { old => $old_status, new => $new_status }); - } + if ( + ref $invocant && ( + $new_status->name eq 'IN_PROGRESS' - # Check if a comment is required for this change. - if ($new_status->comment_required_on_change_from($old_status) - && !$comment->{'thetext'}) - { - ThrowUserError('comment_required', - { old => $old_status ? $old_status->name : undef, - new => $new_status->name, field => 'bug_status' }); - } - - if (ref $invocant - && ($new_status->name eq 'IN_PROGRESS' - # Backwards-compat for the old default workflow. - or $new_status->name eq 'ASSIGNED') - && Bugzilla->params->{"usetargetmilestone"} - && Bugzilla->params->{"musthavemilestoneonaccept"} - # musthavemilestoneonaccept applies only if at least two - # target milestones are defined for the product. - && scalar(@{ $product->milestones }) > 1 - && $invocant->target_milestone eq $product->default_milestone) - { - ThrowUserError("milestone_required", { bug => $invocant }); - } + # Backwards-compat for the old default workflow. + or $new_status->name eq 'ASSIGNED' + ) + && Bugzilla->params->{"usetargetmilestone"} + && Bugzilla->params->{"musthavemilestoneonaccept"} - if (!blessed $invocant) { - $params->{everconfirmed} = $new_status->name eq 'UNCONFIRMED' ? 0 : 1; - } + # musthavemilestoneonaccept applies only if at least two + # target milestones are defined for the product. + && scalar(@{$product->milestones}) > 1 + && $invocant->target_milestone eq $product->default_milestone + ) + { + ThrowUserError("milestone_required", {bug => $invocant}); + } - return $new_status->name; + if (!blessed $invocant) { + $params->{everconfirmed} = $new_status->name eq 'UNCONFIRMED' ? 0 : 1; + } + + return $new_status->name; } sub _check_cc { - my ($invocant, $ccs, undef, $params) = @_; - my $component = blessed($invocant) ? $invocant->component_obj - : $params->{component}; - return [map {$_->id} @{$component->initial_cc}] unless $ccs; - - # Allow comma-separated input as well as arrayrefs. - $ccs = [split(/[,;]+/, $ccs)] if !ref $ccs; - - my %cc_ids; - foreach my $person (@$ccs) { - $person = trim($person); - next unless $person; - my $id = login_to_id($person, THROW_ERROR); - $cc_ids{$id} = 1; - } + my ($invocant, $ccs, undef, $params) = @_; + my $component + = blessed($invocant) ? $invocant->component_obj : $params->{component}; + return [map { $_->id } @{$component->initial_cc}] unless $ccs; + + # Allow comma-separated input as well as arrayrefs. + $ccs = [split(/[,;]+/, $ccs)] if !ref $ccs; - # Enforce Default CC - $cc_ids{$_->id} = 1 foreach (@{$component->initial_cc}); + my %cc_ids; + foreach my $person (@$ccs) { + $person = trim($person); + next unless $person; + my $id = login_to_id($person, THROW_ERROR); + $cc_ids{$id} = 1; + } - return [keys %cc_ids]; + # Enforce Default CC + $cc_ids{$_->id} = 1 foreach (@{$component->initial_cc}); + + return [keys %cc_ids]; } sub _check_comment { - my ($invocant, $comment_txt, undef, $params) = @_; + my ($invocant, $comment_txt, undef, $params) = @_; - # Comment can be empty. We should force it to be empty if the text is undef - if (!defined $comment_txt) { - $comment_txt = ''; - } + # Comment can be empty. We should force it to be empty if the text is undef + if (!defined $comment_txt) { + $comment_txt = ''; + } - # Load up some data - my $isprivate = delete $params->{comment_is_private}; - my $timestamp = $params->{creation_ts}; + # Load up some data + my $isprivate = delete $params->{comment_is_private}; + my $timestamp = $params->{creation_ts}; - # Create the new comment so we can check it - my $comment = { - thetext => $comment_txt, - bug_when => $timestamp, - }; + # Create the new comment so we can check it + my $comment = {thetext => $comment_txt, bug_when => $timestamp,}; - # We don't include the "isprivate" column unless it was specified. - # This allows it to fall back to its database default. - if (defined $isprivate) { - $comment->{isprivate} = $isprivate; - } + # We don't include the "isprivate" column unless it was specified. + # This allows it to fall back to its database default. + if (defined $isprivate) { + $comment->{isprivate} = $isprivate; + } - # Validate comment. We have to do this special as a comment normally - # requires a bug to be already created. For a new bug, the first comment - # obviously can't get the bug if the bug is created after this - # (see bug 590334) - Bugzilla::Comment->check_required_create_fields($comment); - $comment = Bugzilla::Comment->run_create_validators($comment, - { skip => ['bug_id'] } - ); + # Validate comment. We have to do this special as a comment normally + # requires a bug to be already created. For a new bug, the first comment + # obviously can't get the bug if the bug is created after this + # (see bug 590334) + Bugzilla::Comment->check_required_create_fields($comment); + $comment + = Bugzilla::Comment->run_create_validators($comment, {skip => ['bug_id']}); - return $comment; + return $comment; } sub _check_commenton { - my ($invocant, $new_value, $field, $params) = @_; + my ($invocant, $new_value, $field, $params) = @_; - my $has_comment = - ref($invocant) ? $invocant->{added_comments} - : (defined $params->{comment} - and $params->{comment}->{thetext} ne ''); + my $has_comment + = ref($invocant) + ? $invocant->{added_comments} + : (defined $params->{comment} and $params->{comment}->{thetext} ne ''); - my $is_changing = ref($invocant) ? $invocant->$field ne $new_value - : $new_value ne ''; + my $is_changing + = ref($invocant) ? $invocant->$field ne $new_value : $new_value ne ''; - if ($is_changing && !$has_comment) { - my $old_value = ref($invocant) ? $invocant->$field : undef; - ThrowUserError('comment_required', - { field => $field, old => $old_value, new => $new_value }); - } + if ($is_changing && !$has_comment) { + my $old_value = ref($invocant) ? $invocant->$field : undef; + ThrowUserError('comment_required', + {field => $field, old => $old_value, new => $new_value}); + } } sub _check_component { - my ($invocant, $name, undef, $params) = @_; - $name = trim($name); - $name || ThrowUserError("require_component"); - my $product = blessed($invocant) ? $invocant->product_obj - : $params->{product}; - my $old_comp = blessed($invocant) ? $invocant->component : ''; - my $object = Bugzilla::Component->check({ product => $product, name => $name }); - if ($object->name ne $old_comp && !$object->is_active) { - ThrowUserError('value_inactive', { class => ref($object), value => $name }); - } - return $object; + my ($invocant, $name, undef, $params) = @_; + $name = trim($name); + $name || ThrowUserError("require_component"); + my $product = blessed($invocant) ? $invocant->product_obj : $params->{product}; + my $old_comp = blessed($invocant) ? $invocant->component : ''; + my $object = Bugzilla::Component->check({product => $product, name => $name}); + if ($object->name ne $old_comp && !$object->is_active) { + ThrowUserError('value_inactive', {class => ref($object), value => $name}); + } + return $object; } sub _check_creation_ts { - return Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); + return Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); } sub _check_deadline { - my ($invocant, $date) = @_; + my ($invocant, $date) = @_; - # When filing bugs, we're forgiving and just return undef if - # the user isn't a timetracker. When updating bugs, check_can_change_field - # controls permissions, so we don't want to check them here. - if (!ref $invocant and !Bugzilla->user->is_timetracker) { - return undef; - } + # When filing bugs, we're forgiving and just return undef if + # the user isn't a timetracker. When updating bugs, check_can_change_field + # controls permissions, so we don't want to check them here. + if (!ref $invocant and !Bugzilla->user->is_timetracker) { + return undef; + } - # Validate entered deadline - $date = trim($date); - return undef if !$date; - validate_date($date) - || ThrowUserError('illegal_date', { date => $date, - format => 'YYYY-MM-DD' }); - return $date; + # Validate entered deadline + $date = trim($date); + return undef if !$date; + validate_date($date) + || ThrowUserError('illegal_date', {date => $date, format => 'YYYY-MM-DD'}); + return $date; } # Takes two comma/space-separated strings and returns arrayrefs # of valid bug IDs. sub _check_dependencies { - my ($invocant, $value, $field, $params) = @_; + my ($invocant, $value, $field, $params) = @_; - return $value if $params->{_dependencies_validated}; + return $value if $params->{_dependencies_validated}; - if (!ref $invocant) { - # Only editbugs users can set dependencies on bug entry. - return ([], []) unless Bugzilla->user->in_group( - 'editbugs', $params->{product}->id); - } + if (!ref $invocant) { + + # Only editbugs users can set dependencies on bug entry. + return ([], []) + unless Bugzilla->user->in_group('editbugs', $params->{product}->id); + } + + # This is done this way so that dependson and blocked can be in + # VALIDATORS, meaning that they can be in VALIDATOR_DEPENDENCIES, + # which means that they can be checked in the right order during + # bug creation. + my $opposite = $field eq 'dependson' ? 'blocked' : 'dependson'; + my %deps_in = ($field => $value || '', $opposite => $params->{$opposite} || ''); + + foreach my $type (qw(dependson blocked)) { + my @bug_ids + = ref($deps_in{$type}) + ? @{$deps_in{$type}} + : split(/[\s,]+/, $deps_in{$type}); + + # Eliminate nulls. + @bug_ids = grep {$_} @bug_ids; + + my @check_access = @bug_ids; + + # When we're updating a bug, only added or removed bug_ids are + # checked for whether or not we can see/edit those bugs. + if (ref $invocant) { + my $old = $invocant->$type; + my ($removed, $added) = diff_arrays($old, \@bug_ids); + @check_access = (@$added, @$removed); - # This is done this way so that dependson and blocked can be in - # VALIDATORS, meaning that they can be in VALIDATOR_DEPENDENCIES, - # which means that they can be checked in the right order during - # bug creation. - my $opposite = $field eq 'dependson' ? 'blocked' : 'dependson'; - my %deps_in = ($field => $value || '', - $opposite => $params->{$opposite} || ''); - - foreach my $type (qw(dependson blocked)) { - my @bug_ids = ref($deps_in{$type}) - ? @{$deps_in{$type}} - : split(/[\s,]+/, $deps_in{$type}); - # Eliminate nulls. - @bug_ids = grep {$_} @bug_ids; - - my @check_access = @bug_ids; - # When we're updating a bug, only added or removed bug_ids are - # checked for whether or not we can see/edit those bugs. - if (ref $invocant) { - my $old = $invocant->$type; - my ($removed, $added) = diff_arrays($old, \@bug_ids); - @check_access = (@$added, @$removed); - - # Check field permissions if we've changed anything. - if (@check_access) { - my $privs; - if (!$invocant->check_can_change_field($type, 0, 1, \$privs)) { - ThrowUserError('illegal_change', { field => $type, - privs => $privs }); - } - } + # Check field permissions if we've changed anything. + if (@check_access) { + my $privs; + if (!$invocant->check_can_change_field($type, 0, 1, \$privs)) { + ThrowUserError('illegal_change', {field => $type, privs => $privs}); } + } + } - my $user = Bugzilla->user; - foreach my $modified_id (@check_access) { - my $delta_bug = $invocant->check($modified_id); - # Under strict isolation, you can't modify a bug if you can't - # edit it, even if you can see it. - if (Bugzilla->params->{"strict_isolation"}) { - if (!$user->can_edit_product($delta_bug->{'product_id'})) { - ThrowUserError("illegal_change_deps", {field => $type}); - } - } + my $user = Bugzilla->user; + foreach my $modified_id (@check_access) { + my $delta_bug = $invocant->check($modified_id); + + # Under strict isolation, you can't modify a bug if you can't + # edit it, even if you can see it. + if (Bugzilla->params->{"strict_isolation"}) { + if (!$user->can_edit_product($delta_bug->{'product_id'})) { + ThrowUserError("illegal_change_deps", {field => $type}); } - # Replace all aliases by their corresponding bug ID. - @bug_ids = map { $_ =~ /^(\d+)$/ ? $1 : $invocant->check($_, $type)->id } @bug_ids; - $deps_in{$type} = \@bug_ids; + } } - # And finally, check for dependency loops. - my $bug_id = ref($invocant) ? $invocant->id : 0; - my %deps = ValidateDependencies($deps_in{dependson}, $deps_in{blocked}, - $bug_id); + # Replace all aliases by their corresponding bug ID. + @bug_ids + = map { $_ =~ /^(\d+)$/ ? $1 : $invocant->check($_, $type)->id } @bug_ids; + $deps_in{$type} = \@bug_ids; + } + + # And finally, check for dependency loops. + my $bug_id = ref($invocant) ? $invocant->id : 0; + my %deps + = ValidateDependencies($deps_in{dependson}, $deps_in{blocked}, $bug_id); - $params->{$opposite} = $deps{$opposite}; - $params->{_dependencies_validated} = 1; - return $deps{$field}; + $params->{$opposite} = $deps{$opposite}; + $params->{_dependencies_validated} = 1; + return $deps{$field}; } sub _check_dup_id { - my ($self, $dupe_of) = @_; - my $dbh = Bugzilla->dbh; - - # Store the bug ID/alias passed by the user for visibility checks. - my $orig_dupe_of = $dupe_of = trim($dupe_of); - $dupe_of || ThrowCodeError('undefined_field', { field => 'dup_id' }); - # Validate the bug ID. The second argument will force check() to only - # make sure that the bug exists, and convert the alias to the bug ID - # if a string is passed. Group restrictions are checked below. - my $dupe_of_bug = $self->check($dupe_of, 'dup_id'); - $dupe_of = $dupe_of_bug->id; - - # If the dupe is unchanged, we have nothing more to check. - return $dupe_of if ($self->dup_id && $self->dup_id == $dupe_of); - - # If we come here, then the duplicate is new. We have to make sure - # that we can view/change it (issue A on bug 96085). - $dupe_of_bug->check_is_visible($orig_dupe_of); - - # Make sure a loop isn't created when marking this bug - # as duplicate. - _resolve_ultimate_dup_id($self->id, $dupe_of, 1); - - my $cur_dup = $self->dup_id || 0; - if ($cur_dup != $dupe_of && Bugzilla->params->{'commentonduplicate'} - && !$self->{added_comments}) - { - ThrowUserError('comment_required'); + my ($self, $dupe_of) = @_; + my $dbh = Bugzilla->dbh; + + # Store the bug ID/alias passed by the user for visibility checks. + my $orig_dupe_of = $dupe_of = trim($dupe_of); + $dupe_of || ThrowCodeError('undefined_field', {field => 'dup_id'}); + + # Validate the bug ID. The second argument will force check() to only + # make sure that the bug exists, and convert the alias to the bug ID + # if a string is passed. Group restrictions are checked below. + my $dupe_of_bug = $self->check($dupe_of, 'dup_id'); + $dupe_of = $dupe_of_bug->id; + + # If the dupe is unchanged, we have nothing more to check. + return $dupe_of if ($self->dup_id && $self->dup_id == $dupe_of); + + # If we come here, then the duplicate is new. We have to make sure + # that we can view/change it (issue A on bug 96085). + $dupe_of_bug->check_is_visible($orig_dupe_of); + + # Make sure a loop isn't created when marking this bug + # as duplicate. + _resolve_ultimate_dup_id($self->id, $dupe_of, 1); + + my $cur_dup = $self->dup_id || 0; + if ( $cur_dup != $dupe_of + && Bugzilla->params->{'commentonduplicate'} + && !$self->{added_comments}) + { + ThrowUserError('comment_required'); + } + + # Should we add the reporter to the CC list of the new bug? + # If they can see the bug... + if ($self->reporter->can_see_bug($dupe_of)) { + + # We only add them if they're not the reporter of the other bug. + $self->{_add_dup_cc} = 1 if $dupe_of_bug->reporter->id != $self->reporter->id; + } + + # What if the reporter currently can't see the new bug? In the browser + # interface, we prompt the user. In other interfaces, we default to + # not adding the user, as the safest option. + elsif (Bugzilla->usage_mode == USAGE_MODE_BROWSER) { + + # If we've already confirmed whether the user should be added... + my $cgi = Bugzilla->cgi; + my $add_confirmed = $cgi->param('confirm_add_duplicate'); + if (defined $add_confirmed) { + $self->{_add_dup_cc} = $add_confirmed; } + else { + # Note that here we don't check if the user is already the reporter + # of the dupe_of bug, since we already checked if they can *see* + # the bug, above. People might have reporter_accessible turned + # off, but cclist_accessible turned on, so they might want to + # add the reporter even though they're already the reporter of the + # dup_of bug. + my $vars = {}; + my $template = Bugzilla->template; - # Should we add the reporter to the CC list of the new bug? - # If they can see the bug... - if ($self->reporter->can_see_bug($dupe_of)) { - # We only add them if they're not the reporter of the other bug. - $self->{_add_dup_cc} = 1 - if $dupe_of_bug->reporter->id != $self->reporter->id; - } - # What if the reporter currently can't see the new bug? In the browser - # interface, we prompt the user. In other interfaces, we default to - # not adding the user, as the safest option. - elsif (Bugzilla->usage_mode == USAGE_MODE_BROWSER) { - # If we've already confirmed whether the user should be added... - my $cgi = Bugzilla->cgi; - my $add_confirmed = $cgi->param('confirm_add_duplicate'); - if (defined $add_confirmed) { - $self->{_add_dup_cc} = $add_confirmed; - } - else { - # Note that here we don't check if the user is already the reporter - # of the dupe_of bug, since we already checked if they can *see* - # the bug, above. People might have reporter_accessible turned - # off, but cclist_accessible turned on, so they might want to - # add the reporter even though they're already the reporter of the - # dup_of bug. - my $vars = {}; - my $template = Bugzilla->template; - # Ask the user what they want to do about the reporter. - $vars->{'cclist_accessible'} = $dupe_of_bug->cclist_accessible; - $vars->{'original_bug_id'} = $dupe_of; - $vars->{'duplicate_bug_id'} = $self->id; - print $cgi->header(); - $template->process("bug/process/confirm-duplicate.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; - } + # Ask the user what they want to do about the reporter. + $vars->{'cclist_accessible'} = $dupe_of_bug->cclist_accessible; + $vars->{'original_bug_id'} = $dupe_of; + $vars->{'duplicate_bug_id'} = $self->id; + print $cgi->header(); + $template->process("bug/process/confirm-duplicate.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; } + } - return $dupe_of; + return $dupe_of; } sub _check_groups { - my ($invocant, $group_names, undef, $params) = @_; - - my $bug_id = blessed($invocant) ? $invocant->id : undef; - my $product = blessed($invocant) ? $invocant->product_obj - : $params->{product}; - my %add_groups; - - # In email or WebServices, when the "groups" item actually - # isn't specified, then just add the default groups. - if (!defined $group_names) { - my $available = $product->groups_available; - foreach my $group (@$available) { - $add_groups{$group->id} = $group if $group->{is_default}; - } + my ($invocant, $group_names, undef, $params) = @_; + + my $bug_id = blessed($invocant) ? $invocant->id : undef; + my $product = blessed($invocant) ? $invocant->product_obj : $params->{product}; + my %add_groups; + + # In email or WebServices, when the "groups" item actually + # isn't specified, then just add the default groups. + if (!defined $group_names) { + my $available = $product->groups_available; + foreach my $group (@$available) { + $add_groups{$group->id} = $group if $group->{is_default}; } - else { - # Allow a comma-separated list, for email_in.pl. - $group_names = [map { trim($_) } split(',', $group_names)] - if !ref $group_names; - - # First check all the groups they chose to set. - my %args = ( product => $product->name, bug_id => $bug_id, action => 'add' ); - foreach my $name (@$group_names) { - my $group = Bugzilla::Group->check_no_disclose({ %args, name => $name }); - - if (!$product->group_is_settable($group)) { - ThrowUserError('group_restriction_not_allowed', { %args, name => $name }); - } - $add_groups{$group->id} = $group; - } + } + else { + # Allow a comma-separated list, for email_in.pl. + $group_names = [map { trim($_) } split(',', $group_names)] if !ref $group_names; + + # First check all the groups they chose to set. + my %args = (product => $product->name, bug_id => $bug_id, action => 'add'); + foreach my $name (@$group_names) { + my $group = Bugzilla::Group->check_no_disclose({%args, name => $name}); + + if (!$product->group_is_settable($group)) { + ThrowUserError('group_restriction_not_allowed', {%args, name => $name}); + } + $add_groups{$group->id} = $group; } + } - # Now enforce mandatory groups. - $add_groups{$_->id} = $_ foreach @{ $product->groups_mandatory }; + # Now enforce mandatory groups. + $add_groups{$_->id} = $_ foreach @{$product->groups_mandatory}; - my @add_groups = values %add_groups; - return \@add_groups; + my @add_groups = values %add_groups; + return \@add_groups; } sub _check_keywords { - my ($invocant, $keywords_in, undef, $params) = @_; + my ($invocant, $keywords_in, undef, $params) = @_; - return [] if !defined $keywords_in; + return [] if !defined $keywords_in; - my $keyword_array = $keywords_in; - if (!ref $keyword_array) { - $keywords_in = trim($keywords_in); - $keyword_array = [split(/[\s,]+/, $keywords_in)]; - } - - my %keywords; - foreach my $keyword (@$keyword_array) { - next unless $keyword; - my $obj = Bugzilla::Keyword->check($keyword); - $keywords{$obj->id} = $obj; - } - return [values %keywords]; + my $keyword_array = $keywords_in; + if (!ref $keyword_array) { + $keywords_in = trim($keywords_in); + $keyword_array = [split(/[\s,]+/, $keywords_in)]; + } + + my %keywords; + foreach my $keyword (@$keyword_array) { + next unless $keyword; + my $obj = Bugzilla::Keyword->check($keyword); + $keywords{$obj->id} = $obj; + } + return [values %keywords]; } sub _check_product { - my ($invocant, $name) = @_; - $name = trim($name); - # If we're updating the bug and they haven't changed the product, - # always allow it. - if (ref $invocant && lc($invocant->product_obj->name) eq lc($name)) { - return $invocant->product_obj; - } - # Check that the product exists and that the user - # is allowed to enter bugs into this product. - my $product = Bugzilla->user->can_enter_product($name, THROW_ERROR); - return $product; + my ($invocant, $name) = @_; + $name = trim($name); + + # If we're updating the bug and they haven't changed the product, + # always allow it. + if (ref $invocant && lc($invocant->product_obj->name) eq lc($name)) { + return $invocant->product_obj; + } + + # Check that the product exists and that the user + # is allowed to enter bugs into this product. + my $product = Bugzilla->user->can_enter_product($name, THROW_ERROR); + return $product; } sub _check_priority { - my ($invocant, $priority) = @_; - if (!ref $invocant && !Bugzilla->params->{'letsubmitterchoosepriority'}) { - $priority = Bugzilla->params->{'defaultpriority'}; - } - return $invocant->_check_select_field($priority, 'priority'); + my ($invocant, $priority) = @_; + if (!ref $invocant && !Bugzilla->params->{'letsubmitterchoosepriority'}) { + $priority = Bugzilla->params->{'defaultpriority'}; + } + return $invocant->_check_select_field($priority, 'priority'); } sub _check_qa_contact { - my ($invocant, $qa_contact, undef, $params) = @_; - $qa_contact = trim($qa_contact) if !ref $qa_contact; - my $component = blessed($invocant) ? $invocant->component_obj - : $params->{component}; - if (!ref $invocant) { - # Bugs get no QA Contact on creation if useqacontact is off. - return undef if !Bugzilla->params->{useqacontact}; - # Set the default QA Contact if one isn't specified or if the - # user doesn't have editbugs. - if (!Bugzilla->user->in_group('editbugs', $component->product_id) - || !$qa_contact) - { - return $component->default_qa_contact ? $component->default_qa_contact->id : undef; - } + my ($invocant, $qa_contact, undef, $params) = @_; + $qa_contact = trim($qa_contact) if !ref $qa_contact; + my $component + = blessed($invocant) ? $invocant->component_obj : $params->{component}; + if (!ref $invocant) { + + # Bugs get no QA Contact on creation if useqacontact is off. + return undef if !Bugzilla->params->{useqacontact}; + + # Set the default QA Contact if one isn't specified or if the + # user doesn't have editbugs. + if ( !Bugzilla->user->in_group('editbugs', $component->product_id) + || !$qa_contact) + { + return $component->default_qa_contact + ? $component->default_qa_contact->id + : undef; } + } - # If a QA Contact was specified or if we're updating, check - # the QA Contact for validity. - my $id; - if ($qa_contact) { - $qa_contact = Bugzilla::User->check($qa_contact) if !ref $qa_contact; - $id = $qa_contact->id; - # create() checks this another way, so we don't have to run this - # check during create(). - # If there is no QA contact, this check is not required. - $invocant->_check_strict_isolation_for_user($qa_contact) - if (ref $invocant && $id); - } + # If a QA Contact was specified or if we're updating, check + # the QA Contact for validity. + my $id; + if ($qa_contact) { + $qa_contact = Bugzilla::User->check($qa_contact) if !ref $qa_contact; + $id = $qa_contact->id; + + # create() checks this another way, so we don't have to run this + # check during create(). + # If there is no QA contact, this check is not required. + $invocant->_check_strict_isolation_for_user($qa_contact) + if (ref $invocant && $id); + } - # "0" always means "undef", for QA Contact. - return $id || undef; + # "0" always means "undef", for QA Contact. + return $id || undef; } sub _check_reporter { - my $invocant = shift; - my $reporter; - if (ref $invocant) { - # You cannot change the reporter of a bug. - $reporter = $invocant->reporter->id; - } - else { - # On bug creation, the reporter is the logged in user - # (meaning that they must be logged in first!). - Bugzilla->login(LOGIN_REQUIRED); - $reporter = Bugzilla->user->id; - } - return $reporter; + my $invocant = shift; + my $reporter; + if (ref $invocant) { + + # You cannot change the reporter of a bug. + $reporter = $invocant->reporter->id; + } + else { + # On bug creation, the reporter is the logged in user + # (meaning that they must be logged in first!). + Bugzilla->login(LOGIN_REQUIRED); + $reporter = Bugzilla->user->id; + } + return $reporter; } sub _check_resolution { - my ($invocant, $resolution, undef, $params) = @_; - $resolution = trim($resolution); - my $status = ref($invocant) ? $invocant->status->name - : $params->{bug_status}; - my $is_open = ref($invocant) ? $invocant->status->is_open - : is_open_state($status); - - # Throw a special error for resolving bugs without a resolution - # (or trying to change the resolution to '' on a closed bug without - # using clear_resolution). - ThrowUserError('missing_resolution', { status => $status }) - if !$resolution && !$is_open; - - # Make sure this is a valid resolution. - $resolution = $invocant->_check_select_field($resolution, 'resolution'); - - # Don't allow open bugs to have resolutions. - ThrowUserError('resolution_not_allowed') if $is_open; - - # Check noresolveonopenblockers. - my $dependson = ref($invocant) ? $invocant->dependson - : ($params->{dependson} || []); - if (Bugzilla->params->{"noresolveonopenblockers"} - && $resolution eq 'FIXED' - && (!ref $invocant or !$invocant->resolution - or $resolution ne $invocant->resolution) - && scalar @$dependson) - { - my $dep_bugs = Bugzilla::Bug->new_from_list($dependson); - my $count_open = grep { $_->isopened } @$dep_bugs; - if ($count_open) { - my $bug_id = ref($invocant) ? $invocant->id : undef; - ThrowUserError("still_unresolved_bugs", - { bug_id => $bug_id, dep_count => $count_open }); - } - } - - # Check if they're changing the resolution and need to comment. - if (Bugzilla->params->{'commentonchange_resolution'}) { - $invocant->_check_commenton($resolution, 'resolution', $params); - } - - return $resolution; + my ($invocant, $resolution, undef, $params) = @_; + $resolution = trim($resolution); + my $status = ref($invocant) ? $invocant->status->name : $params->{bug_status}; + my $is_open + = ref($invocant) ? $invocant->status->is_open : is_open_state($status); + + # Throw a special error for resolving bugs without a resolution + # (or trying to change the resolution to '' on a closed bug without + # using clear_resolution). + ThrowUserError('missing_resolution', {status => $status}) + if !$resolution && !$is_open; + + # Make sure this is a valid resolution. + $resolution = $invocant->_check_select_field($resolution, 'resolution'); + + # Don't allow open bugs to have resolutions. + ThrowUserError('resolution_not_allowed') if $is_open; + + # Check noresolveonopenblockers. + my $dependson + = ref($invocant) ? $invocant->dependson : ($params->{dependson} || []); + if ( + Bugzilla->params->{"noresolveonopenblockers"} + && $resolution eq 'FIXED' + && ( !ref $invocant + or !$invocant->resolution + or $resolution ne $invocant->resolution) + && scalar @$dependson + ) + { + my $dep_bugs = Bugzilla::Bug->new_from_list($dependson); + my $count_open = grep { $_->isopened } @$dep_bugs; + if ($count_open) { + my $bug_id = ref($invocant) ? $invocant->id : undef; + ThrowUserError("still_unresolved_bugs", + {bug_id => $bug_id, dep_count => $count_open}); + } + } + + # Check if they're changing the resolution and need to comment. + if (Bugzilla->params->{'commentonchange_resolution'}) { + $invocant->_check_commenton($resolution, 'resolution', $params); + } + + return $resolution; } sub _check_short_desc { - my ($invocant, $short_desc) = @_; - # Set the parameter to itself, but cleaned up - $short_desc = clean_text($short_desc) if $short_desc; + my ($invocant, $short_desc) = @_; - if (!defined $short_desc || $short_desc eq '') { - ThrowUserError("require_summary"); - } - if (length($short_desc) > MAX_FREETEXT_LENGTH) { - ThrowUserError('freetext_too_long', - { field => 'short_desc', text => $short_desc }); - } - return $short_desc; + # Set the parameter to itself, but cleaned up + $short_desc = clean_text($short_desc) if $short_desc; + + if (!defined $short_desc || $short_desc eq '') { + ThrowUserError("require_summary"); + } + if (length($short_desc) > MAX_FREETEXT_LENGTH) { + ThrowUserError('freetext_too_long', + {field => 'short_desc', text => $short_desc}); + } + return $short_desc; } sub _check_status_whiteboard { return defined $_[1] ? $_[1] : ''; } # Unlike other checkers, this one doesn't return anything. sub _check_strict_isolation { - my ($invocant, $ccs, $assignee, $qa_contact, $product) = @_; - return unless Bugzilla->params->{'strict_isolation'}; - - if (ref $invocant) { - my $original = $invocant->new($invocant->id); - - # We only check people if they've been added. This way, if - # strict_isolation is turned on when there are invalid users - # on bugs, people can still add comments and so on. - my @old_cc = map { $_->id } @{$original->cc_users}; - my @new_cc = map { $_->id } @{$invocant->cc_users}; - my ($removed, $added) = diff_arrays(\@old_cc, \@new_cc); - $ccs = Bugzilla::User->new_from_list($added); - - $assignee = $invocant->assigned_to - if $invocant->assigned_to->id != $original->assigned_to->id; - if ($invocant->qa_contact - && (!$original->qa_contact - || $invocant->qa_contact->id != $original->qa_contact->id)) - { - $qa_contact = $invocant->qa_contact; - } - $product = $invocant->product_obj; + my ($invocant, $ccs, $assignee, $qa_contact, $product) = @_; + return unless Bugzilla->params->{'strict_isolation'}; + + if (ref $invocant) { + my $original = $invocant->new($invocant->id); + + # We only check people if they've been added. This way, if + # strict_isolation is turned on when there are invalid users + # on bugs, people can still add comments and so on. + my @old_cc = map { $_->id } @{$original->cc_users}; + my @new_cc = map { $_->id } @{$invocant->cc_users}; + my ($removed, $added) = diff_arrays(\@old_cc, \@new_cc); + $ccs = Bugzilla::User->new_from_list($added); + + $assignee = $invocant->assigned_to + if $invocant->assigned_to->id != $original->assigned_to->id; + if ( + $invocant->qa_contact + && (!$original->qa_contact + || $invocant->qa_contact->id != $original->qa_contact->id) + ) + { + $qa_contact = $invocant->qa_contact; } + $product = $invocant->product_obj; + } - my @related_users = @$ccs; - push(@related_users, $assignee) if $assignee; + my @related_users = @$ccs; + push(@related_users, $assignee) if $assignee; - if (Bugzilla->params->{'useqacontact'} && $qa_contact) { - push(@related_users, $qa_contact); - } + if (Bugzilla->params->{'useqacontact'} && $qa_contact) { + push(@related_users, $qa_contact); + } - @related_users = @{Bugzilla::User->new_from_list(\@related_users)} - if !ref $invocant; - - # For each unique user in @related_users...(assignee and qa_contact - # could be duplicates of users in the CC list) - my %unique_users = map {$_->id => $_} @related_users; - my @blocked_users; - foreach my $id (keys %unique_users) { - my $related_user = $unique_users{$id}; - if (!$related_user->can_edit_product($product->id) || - !$related_user->can_see_product($product->name)) { - push (@blocked_users, $related_user->login); - } + @related_users = @{Bugzilla::User->new_from_list(\@related_users)} + if !ref $invocant; + + # For each unique user in @related_users...(assignee and qa_contact + # could be duplicates of users in the CC list) + my %unique_users = map { $_->id => $_ } @related_users; + my @blocked_users; + foreach my $id (keys %unique_users) { + my $related_user = $unique_users{$id}; + if ( !$related_user->can_edit_product($product->id) + || !$related_user->can_see_product($product->name)) + { + push(@blocked_users, $related_user->login); } - if (scalar(@blocked_users)) { - my %vars = ( users => \@blocked_users, - product => $product->name ); - if (ref $invocant) { - $vars{'bug_id'} = $invocant->id; - } - else { - $vars{'new'} = 1; - } - ThrowUserError("invalid_user_group", \%vars); + } + if (scalar(@blocked_users)) { + my %vars = (users => \@blocked_users, product => $product->name); + if (ref $invocant) { + $vars{'bug_id'} = $invocant->id; + } + else { + $vars{'new'} = 1; } + ThrowUserError("invalid_user_group", \%vars); + } } # This is used by various set_ checkers, to make their code simpler. sub _check_strict_isolation_for_user { - my ($self, $user) = @_; - return unless Bugzilla->params->{"strict_isolation"}; - if (!$user->can_edit_product($self->{product_id})) { - ThrowUserError('invalid_user_group', - { users => $user->login, - product => $self->product, - bug_id => $self->id }); - } + my ($self, $user) = @_; + return unless Bugzilla->params->{"strict_isolation"}; + if (!$user->can_edit_product($self->{product_id})) { + ThrowUserError('invalid_user_group', + {users => $user->login, product => $self->product, bug_id => $self->id}); + } } sub _check_tag_name { - my ($invocant, $tag) = @_; + my ($invocant, $tag) = @_; + + $tag = clean_text($tag); + $tag || ThrowUserError('no_tag_to_edit'); + ThrowUserError('tag_name_too_long') if length($tag) > MAX_LEN_QUERY_NAME; + trick_taint($tag); - $tag = clean_text($tag); - $tag || ThrowUserError('no_tag_to_edit'); - ThrowUserError('tag_name_too_long') if length($tag) > MAX_LEN_QUERY_NAME; - trick_taint($tag); - # Tags are all lowercase. - return lc($tag); + # Tags are all lowercase. + return lc($tag); } sub _check_target_milestone { - my ($invocant, $target, undef, $params) = @_; - my $product = blessed($invocant) ? $invocant->product_obj - : $params->{product}; - my $old_target = blessed($invocant) ? $invocant->target_milestone : ''; - $target = trim($target); - $target = $product->default_milestone if !defined $target; - my $object = Bugzilla::Milestone->check( - { product => $product, name => $target }); - if ($old_target && $object->name ne $old_target && !$object->is_active) { - ThrowUserError('value_inactive', { class => ref($object), value => $target }); - } - return $object->name; + my ($invocant, $target, undef, $params) = @_; + my $product = blessed($invocant) ? $invocant->product_obj : $params->{product}; + my $old_target = blessed($invocant) ? $invocant->target_milestone : ''; + $target = trim($target); + $target = $product->default_milestone if !defined $target; + my $object = Bugzilla::Milestone->check({product => $product, name => $target}); + if ($old_target && $object->name ne $old_target && !$object->is_active) { + ThrowUserError('value_inactive', {class => ref($object), value => $target}); + } + return $object->name; } sub _check_time_field { - my ($invocant, $value, $field, $params) = @_; + my ($invocant, $value, $field, $params) = @_; - # When filing bugs, we're forgiving and just return 0 if - # the user isn't a timetracker. When updating bugs, check_can_change_field - # controls permissions, so we don't want to check them here. - if (!ref $invocant and !Bugzilla->user->is_timetracker) { - return 0; - } + # When filing bugs, we're forgiving and just return 0 if + # the user isn't a timetracker. When updating bugs, check_can_change_field + # controls permissions, so we don't want to check them here. + if (!ref $invocant and !Bugzilla->user->is_timetracker) { + return 0; + } - # check_time is in Bugzilla::Object. - return $invocant->check_time($value, $field, $params); + # check_time is in Bugzilla::Object. + return $invocant->check_time($value, $field, $params); } sub _check_version { - my ($invocant, $version, undef, $params) = @_; - $version = trim($version); - my $product = blessed($invocant) ? $invocant->product_obj - : $params->{product}; - my $old_vers = blessed($invocant) ? $invocant->version : ''; - my $object = Bugzilla::Version->check({ product => $product, name => $version }); - if ($object->name ne $old_vers && !$object->is_active) { - ThrowUserError('value_inactive', { class => ref($object), value => $version }); - } - return $object->name; + my ($invocant, $version, undef, $params) = @_; + $version = trim($version); + my $product = blessed($invocant) ? $invocant->product_obj : $params->{product}; + my $old_vers = blessed($invocant) ? $invocant->version : ''; + my $object = Bugzilla::Version->check({product => $product, name => $version}); + if ($object->name ne $old_vers && !$object->is_active) { + ThrowUserError('value_inactive', {class => ref($object), value => $version}); + } + return $object->name; } # Custom Field Validators sub _check_field_is_mandatory { - my ($invocant, $value, $field, $params) = @_; + my ($invocant, $value, $field, $params) = @_; - if (!blessed($field)) { - $field = Bugzilla::Field->new({ name => $field }); - return if !$field; - } + if (!blessed($field)) { + $field = Bugzilla::Field->new({name => $field}); + return if !$field; + } - return if !$field->is_mandatory; + return if !$field->is_mandatory; - return if !$field->is_visible_on_bug($params || $invocant); + return if !$field->is_visible_on_bug($params || $invocant); - return if ($field->type == FIELD_TYPE_SINGLE_SELECT - && scalar @{ get_legal_field_values($field->name) } == 1); + return + if ($field->type == FIELD_TYPE_SINGLE_SELECT + && scalar @{get_legal_field_values($field->name)} == 1); - return if ($field->type == FIELD_TYPE_MULTI_SELECT - && !scalar @{ get_legal_field_values($field->name) }); + return + if ($field->type == FIELD_TYPE_MULTI_SELECT + && !scalar @{get_legal_field_values($field->name)}); - if (ref($value) eq 'ARRAY') { - $value = join('', @$value); - } + if (ref($value) eq 'ARRAY') { + $value = join('', @$value); + } - $value = trim($value); - if (!defined($value) - or $value eq "" - or ($value eq '---' and $field->type == FIELD_TYPE_SINGLE_SELECT) - or ($value =~ EMPTY_DATETIME_REGEX - and $field->type == FIELD_TYPE_DATETIME)) - { - ThrowUserError('required_field', { field => $field }); - } + $value = trim($value); + if ( !defined($value) + or $value eq "" + or ($value eq '---' and $field->type == FIELD_TYPE_SINGLE_SELECT) + or ($value =~ EMPTY_DATETIME_REGEX and $field->type == FIELD_TYPE_DATETIME)) + { + ThrowUserError('required_field', {field => $field}); + } } sub _check_date_field { - my ($invocant, $date) = @_; - return $invocant->_check_datetime_field($date, undef, {date_only => 1}); + my ($invocant, $date) = @_; + return $invocant->_check_datetime_field($date, undef, {date_only => 1}); } sub _check_datetime_field { - my ($invocant, $date_time, $field, $params) = @_; - - # Empty datetimes are empty strings or strings only containing - # 0's, whitespace, and punctuation. - if ($date_time =~ /^[\s0[:punct:]]*$/) { - return undef; - } - - $date_time = trim($date_time); - my ($date, $time) = split(' ', $date_time); - if ($date && !validate_date($date)) { - ThrowUserError('illegal_date', { date => $date, - format => 'YYYY-MM-DD' }); - } - if ($time && $params->{date_only}) { - ThrowUserError('illegal_date', { date => $date_time, - format => 'YYYY-MM-DD' }); - } - if ($time && !validate_time($time)) { - ThrowUserError('illegal_time', { 'time' => $time, - format => 'HH:MM:SS' }); - } - return $date_time + my ($invocant, $date_time, $field, $params) = @_; + + # Empty datetimes are empty strings or strings only containing + # 0's, whitespace, and punctuation. + if ($date_time =~ /^[\s0[:punct:]]*$/) { + return undef; + } + + $date_time = trim($date_time); + my ($date, $time) = split(' ', $date_time); + if ($date && !validate_date($date)) { + ThrowUserError('illegal_date', {date => $date, format => 'YYYY-MM-DD'}); + } + if ($time && $params->{date_only}) { + ThrowUserError('illegal_date', {date => $date_time, format => 'YYYY-MM-DD'}); + } + if ($time && !validate_time($time)) { + ThrowUserError('illegal_time', {'time' => $time, format => 'HH:MM:SS'}); + } + return $date_time; } sub _check_default_field { return defined $_[1] ? trim($_[1]) : ''; } sub _check_freetext_field { - my ($invocant, $text, $field) = @_; + my ($invocant, $text, $field) = @_; - $text = (defined $text) ? trim($text) : ''; - if (length($text) > MAX_FREETEXT_LENGTH) { - ThrowUserError('freetext_too_long', - { field => $field, text => $text }); - } - return $text; + $text = (defined $text) ? trim($text) : ''; + if (length($text) > MAX_FREETEXT_LENGTH) { + ThrowUserError('freetext_too_long', {field => $field, text => $text}); + } + return $text; } sub _check_multi_select_field { - my ($invocant, $values, $field) = @_; - - # Allow users (mostly email_in.pl) to specify multi-selects as - # comma-separated values. - if (defined $values and !ref $values) { - # We don't split on spaces because multi-select values can and often - # do have spaces in them. (Theoretically they can have commas in them - # too, but that's much less common and people should be able to work - # around it pretty cleanly, if they want to use email_in.pl.) - $values = [split(',', $values)]; - } + my ($invocant, $values, $field) = @_; - return [] if !$values; - my @checked_values; - foreach my $value (@$values) { - push(@checked_values, $invocant->_check_select_field($value, $field)); - } - return \@checked_values; + # Allow users (mostly email_in.pl) to specify multi-selects as + # comma-separated values. + if (defined $values and !ref $values) { + + # We don't split on spaces because multi-select values can and often + # do have spaces in them. (Theoretically they can have commas in them + # too, but that's much less common and people should be able to work + # around it pretty cleanly, if they want to use email_in.pl.) + $values = [split(',', $values)]; + } + + return [] if !$values; + my @checked_values; + foreach my $value (@$values) { + push(@checked_values, $invocant->_check_select_field($value, $field)); + } + return \@checked_values; } sub _check_select_field { - my ($invocant, $value, $field) = @_; - my $object = Bugzilla::Field::Choice->type($field)->check($value); - return $object->name; + my ($invocant, $value, $field) = @_; + my $object = Bugzilla::Field::Choice->type($field)->check($value); + return $object->name; } sub _check_bugid_field { - my ($invocant, $value, $field) = @_; - return undef if !$value; - - # check that the value is a valid, visible bug id - my $checked_id = $invocant->check($value, $field)->id; - - # check for loop (can't have a loop if this is a new bug) - if (ref $invocant) { - _check_relationship_loop($field, $invocant->bug_id, $checked_id); - } + my ($invocant, $value, $field) = @_; + return undef if !$value; + + # check that the value is a valid, visible bug id + my $checked_id = $invocant->check($value, $field)->id; - return $checked_id; + # check for loop (can't have a loop if this is a new bug) + if (ref $invocant) { + _check_relationship_loop($field, $invocant->bug_id, $checked_id); + } + + return $checked_id; } sub _check_textarea_field { - my ($invocant, $text, $field) = @_; + my ($invocant, $text, $field) = @_; - $text = (defined $text) ? trim($text) : ''; + $text = (defined $text) ? trim($text) : ''; - # Web browsers submit newlines as \r\n. - # Sanitize all input to match the web standard. - # XMLRPC input could be either \n or \r\n - $text =~ s/\r?\n/\r\n/g; + # Web browsers submit newlines as \r\n. + # Sanitize all input to match the web standard. + # XMLRPC input could be either \n or \r\n + $text =~ s/\r?\n/\r\n/g; - return $text; + return $text; } sub _check_integer_field { - my ($invocant, $value, $field) = @_; - $value = defined($value) ? trim($value) : ''; + my ($invocant, $value, $field) = @_; + $value = defined($value) ? trim($value) : ''; - if ($value eq '') { - return 0; - } + if ($value eq '') { + return 0; + } - my $orig_value = $value; - if (!detaint_signed($value)) { - ThrowUserError("number_not_integer", - {field => $field, num => $orig_value}); - } - elsif (abs($value) > MAX_INT_32) { - ThrowUserError("number_too_large", - {field => $field, num => $orig_value, max_num => MAX_INT_32}); - } + my $orig_value = $value; + if (!detaint_signed($value)) { + ThrowUserError("number_not_integer", {field => $field, num => $orig_value}); + } + elsif (abs($value) > MAX_INT_32) { + ThrowUserError("number_too_large", + {field => $field, num => $orig_value, max_num => MAX_INT_32}); + } - return $value; + return $value; } sub _check_relationship_loop { - # Generates a dependency tree for a given bug. Calls itself recursively - # to generate sub-trees for the bug's dependencies. - my ($field, $bug_id, $dep_id, $ids) = @_; - - # Don't do anything if this bug doesn't have any dependencies. - return unless defined($dep_id); - - # Check whether we have seen this bug yet - $ids = {} unless defined $ids; - $ids->{$bug_id} = 1; - if ($ids->{$dep_id}) { - ThrowUserError("relationship_loop_single", { - 'bug_id' => $bug_id, - 'dep_id' => $dep_id, - 'field_name' => $field}); - } - - # Get this dependency's record from the database - my $dbh = Bugzilla->dbh; - my $next_dep_id = $dbh->selectrow_array( - "SELECT $field FROM bugs WHERE bug_id = ?", undef, $dep_id); - _check_relationship_loop($field, $dep_id, $next_dep_id, $ids); + # Generates a dependency tree for a given bug. Calls itself recursively + # to generate sub-trees for the bug's dependencies. + my ($field, $bug_id, $dep_id, $ids) = @_; + + # Don't do anything if this bug doesn't have any dependencies. + return unless defined($dep_id); + + # Check whether we have seen this bug yet + $ids = {} unless defined $ids; + $ids->{$bug_id} = 1; + if ($ids->{$dep_id}) { + ThrowUserError("relationship_loop_single", + {'bug_id' => $bug_id, 'dep_id' => $dep_id, 'field_name' => $field}); + } + + # Get this dependency's record from the database + my $dbh = Bugzilla->dbh; + my $next_dep_id + = $dbh->selectrow_array("SELECT $field FROM bugs WHERE bug_id = ?", + undef, $dep_id); + + _check_relationship_loop($field, $dep_id, $next_dep_id, $ids); } ##################################################################### @@ -2298,63 +2366,63 @@ sub _check_relationship_loop { ##################################################################### sub fields { - my $class = shift; - - my @fields = - ( - # Standard Fields - # Keep this ordering in sync with bugzilla.dtd. - qw(bug_id alias creation_ts short_desc delta_ts - reporter_accessible cclist_accessible - classification_id classification - product component version rep_platform op_sys - bug_status resolution dup_id see_also - bug_file_loc status_whiteboard keywords - priority bug_severity target_milestone - dependson blocked everconfirmed - reporter assigned_to cc estimated_time - remaining_time actual_time deadline), - - # Conditional Fields - Bugzilla->params->{'useqacontact'} ? "qa_contact" : (), - # Custom Fields - map { $_->name } Bugzilla->active_custom_fields - ); - Bugzilla::Hook::process('bug_fields', {'fields' => \@fields} ); - - return @fields; + my $class = shift; + + my @fields = ( + + # Standard Fields + # Keep this ordering in sync with bugzilla.dtd. + qw(bug_id alias creation_ts short_desc delta_ts + reporter_accessible cclist_accessible + classification_id classification + product component version rep_platform op_sys + bug_status resolution dup_id see_also + bug_file_loc status_whiteboard keywords + priority bug_severity target_milestone + dependson blocked everconfirmed + reporter assigned_to cc estimated_time + remaining_time actual_time deadline), + + # Conditional Fields + Bugzilla->params->{'useqacontact'} ? "qa_contact" : (), + + # Custom Fields + map { $_->name } Bugzilla->active_custom_fields + ); + Bugzilla::Hook::process('bug_fields', {'fields' => \@fields}); + + return @fields; } ##################################################################### -# Mutators +# Mutators ##################################################################### # To run check_can_change_field. sub _set_global_validator { - my ($self, $value, $field) = @_; - my $current = $self->$field; - my $privs; - - if (ref $current && ref($current) ne 'ARRAY' - && $current->isa('Bugzilla::Object')) { - $current = $current->id ; - } - if (ref $value && ref($value) ne 'ARRAY' - && $value->isa('Bugzilla::Object')) { - $value = $value->id ; - } - my $can = $self->check_can_change_field($field, $current, $value, \$privs); - if (!$can) { - if ($field eq 'assigned_to' || $field eq 'qa_contact') { - $value = Bugzilla::User->new($value)->login; - $current = Bugzilla::User->new($current)->login; - } - ThrowUserError('illegal_change', { field => $field, - oldvalue => $current, - newvalue => $value, - privs => $privs }); - } - $self->_check_field_is_mandatory($value, $field); + my ($self, $value, $field) = @_; + my $current = $self->$field; + my $privs; + + if ( ref $current + && ref($current) ne 'ARRAY' + && $current->isa('Bugzilla::Object')) + { + $current = $current->id; + } + if (ref $value && ref($value) ne 'ARRAY' && $value->isa('Bugzilla::Object')) { + $value = $value->id; + } + my $can = $self->check_can_change_field($field, $current, $value, \$privs); + if (!$can) { + if ($field eq 'assigned_to' || $field eq 'qa_contact') { + $value = Bugzilla::User->new($value)->login; + $current = Bugzilla::User->new($current)->login; + } + ThrowUserError('illegal_change', + {field => $field, oldvalue => $current, newvalue => $value, privs => $privs}); + } + $self->_check_field_is_mandatory($value, $field); } @@ -2365,359 +2433,384 @@ sub _set_global_validator { # Note that if you are changing multiple bugs at once, you must pass # other_bugs to set_all in order for it to behave properly. sub set_all { - my $self = shift; - my ($input_params) = @_; - - # Clone the data as we are going to alter it, and this would affect - # subsequent bugs when calling set_all() again, as some fields would - # be modified or no longer defined. - my $params = {}; - %$params = %$input_params; - - # You cannot mark bugs as duplicate when changing several bugs at once - # (because currently there is no way to check for duplicate loops in that - # situation). You also cannot set the alias of several bugs at once. - if ($params->{other_bugs} and scalar @{ $params->{other_bugs} } > 1) { - ThrowUserError('dupe_not_allowed') if exists $params->{dup_id}; - ThrowUserError('multiple_alias_not_allowed') - if defined $params->{alias}; - } - - # For security purposes, and because lots of other checks depend on it, - # we set the product first before anything else. - my $product_changed; # Used only for strict_isolation checks. - if (exists $params->{'product'}) { - $product_changed = $self->_set_product($params->{'product'}, $params); - } - - # strict_isolation checks mean that we should set the groups - # immediately after changing the product. - $self->_add_remove($params, 'groups'); - - if (exists $params->{'dependson'} or exists $params->{'blocked'}) { - my %set_deps; - foreach my $name (qw(dependson blocked)) { - my @dep_ids = @{ $self->$name }; - # If only one of the two fields was passed in, then we need to - # retain the current value for the other one. - if (!exists $params->{$name}) { - $set_deps{$name} = \@dep_ids; - next; - } - - # Explicitly setting them to a particular value overrides - # add/remove. - if (exists $params->{$name}->{set}) { - $set_deps{$name} = $params->{$name}->{set}; - next; - } - - foreach my $add (@{ $params->{$name}->{add} || [] }) { - push(@dep_ids, $add) if !grep($_ == $add, @dep_ids); - } - foreach my $remove (@{ $params->{$name}->{remove} || [] }) { - @dep_ids = grep($_ != $remove, @dep_ids); - } - $set_deps{$name} = \@dep_ids; - } - - $self->set_dependencies($set_deps{'dependson'}, $set_deps{'blocked'}); - } - - if (exists $params->{'keywords'}) { - # Sorting makes the order "add, remove, set", just like for other - # fields. - foreach my $action (sort keys %{ $params->{'keywords'} }) { - $self->modify_keywords($params->{'keywords'}->{$action}, $action); - } - } + my $self = shift; + my ($input_params) = @_; + + # Clone the data as we are going to alter it, and this would affect + # subsequent bugs when calling set_all() again, as some fields would + # be modified or no longer defined. + my $params = {}; + %$params = %$input_params; + + # You cannot mark bugs as duplicate when changing several bugs at once + # (because currently there is no way to check for duplicate loops in that + # situation). You also cannot set the alias of several bugs at once. + if ($params->{other_bugs} and scalar @{$params->{other_bugs}} > 1) { + ThrowUserError('dupe_not_allowed') if exists $params->{dup_id}; + ThrowUserError('multiple_alias_not_allowed') if defined $params->{alias}; + } + + # For security purposes, and because lots of other checks depend on it, + # we set the product first before anything else. + my $product_changed; # Used only for strict_isolation checks. + if (exists $params->{'product'}) { + $product_changed = $self->_set_product($params->{'product'}, $params); + } + + # strict_isolation checks mean that we should set the groups + # immediately after changing the product. + $self->_add_remove($params, 'groups'); + + if (exists $params->{'dependson'} or exists $params->{'blocked'}) { + my %set_deps; + foreach my $name (qw(dependson blocked)) { + my @dep_ids = @{$self->$name}; + + # If only one of the two fields was passed in, then we need to + # retain the current value for the other one. + if (!exists $params->{$name}) { + $set_deps{$name} = \@dep_ids; + next; + } + + # Explicitly setting them to a particular value overrides + # add/remove. + if (exists $params->{$name}->{set}) { + $set_deps{$name} = $params->{$name}->{set}; + next; + } + + foreach my $add (@{$params->{$name}->{add} || []}) { + push(@dep_ids, $add) if !grep($_ == $add, @dep_ids); + } + foreach my $remove (@{$params->{$name}->{remove} || []}) { + @dep_ids = grep($_ != $remove, @dep_ids); + } + $set_deps{$name} = \@dep_ids; + } + + $self->set_dependencies($set_deps{'dependson'}, $set_deps{'blocked'}); + } + + if (exists $params->{'keywords'}) { + + # Sorting makes the order "add, remove, set", just like for other + # fields. + foreach my $action (sort keys %{$params->{'keywords'}}) { + $self->modify_keywords($params->{'keywords'}->{$action}, $action); + } + } + + if (exists $params->{'comment'} or exists $params->{'work_time'}) { + + # Add a comment as needed to each bug. This is done early because + # there are lots of things that want to check if we added a comment. + $self->add_comment( + $params->{'comment'}->{'body'}, + { + isprivate => $params->{'comment'}->{'is_private'}, + work_time => $params->{'work_time'} + } + ); + } - if (exists $params->{'comment'} or exists $params->{'work_time'}) { - # Add a comment as needed to each bug. This is done early because - # there are lots of things that want to check if we added a comment. - $self->add_comment($params->{'comment'}->{'body'}, - { isprivate => $params->{'comment'}->{'is_private'}, - work_time => $params->{'work_time'} }); - } + if (exists $params->{alias} && $params->{alias}{set}) { + my ($removed_aliases, $added_aliases) + = diff_arrays($self->alias, $params->{alias}{set}); + $params->{alias} = {add => $added_aliases, remove => $removed_aliases,}; + } - if (exists $params->{alias} && $params->{alias}{set}) { - my ($removed_aliases, $added_aliases) = diff_arrays( - $self->alias, $params->{alias}{set}); - $params->{alias} = { - add => $added_aliases, - remove => $removed_aliases, - }; - } + my %normal_set_all; + foreach my $name (keys %$params) { - my %normal_set_all; - foreach my $name (keys %$params) { - # These are handled separately below. - if ($self->can("set_$name")) { - $normal_set_all{$name} = $params->{$name}; - } + # These are handled separately below. + if ($self->can("set_$name")) { + $normal_set_all{$name} = $params->{$name}; } - $self->SUPER::set_all(\%normal_set_all); + } + $self->SUPER::set_all(\%normal_set_all); - $self->reset_assigned_to if $params->{'reset_assigned_to'}; - $self->reset_qa_contact if $params->{'reset_qa_contact'}; + $self->reset_assigned_to if $params->{'reset_assigned_to'}; + $self->reset_qa_contact if $params->{'reset_qa_contact'}; - $self->_add_remove($params, 'see_also'); + $self->_add_remove($params, 'see_also'); - # And set custom fields. - my @custom_fields = Bugzilla->active_custom_fields; - foreach my $field (@custom_fields) { - my $fname = $field->name; - if (exists $params->{$fname}) { - $self->set_custom_field($field, $params->{$fname}); - } + # And set custom fields. + my @custom_fields = Bugzilla->active_custom_fields; + foreach my $field (@custom_fields) { + my $fname = $field->name; + if (exists $params->{$fname}) { + $self->set_custom_field($field, $params->{$fname}); } + } - $self->_add_remove($params, 'cc'); - $self->_add_remove($params, 'alias'); + $self->_add_remove($params, 'cc'); + $self->_add_remove($params, 'alias'); - # Theoretically you could move a product without ever specifying - # a new assignee or qa_contact, or adding/removing any CCs. So, - # we have to check that the current assignee, qa, and CCs are still - # valid if we've switched products, under strict_isolation. We can only - # do that here, because if they *did* change the assignee, qa, or CC, - # then we don't want to check the original ones, only the new ones. - $self->_check_strict_isolation() if $product_changed; + # Theoretically you could move a product without ever specifying + # a new assignee or qa_contact, or adding/removing any CCs. So, + # we have to check that the current assignee, qa, and CCs are still + # valid if we've switched products, under strict_isolation. We can only + # do that here, because if they *did* change the assignee, qa, or CC, + # then we don't want to check the original ones, only the new ones. + $self->_check_strict_isolation() if $product_changed; } # Helper for set_all that helps with fields that have an "add/remove" # pattern instead of a "set_" pattern. sub _add_remove { - my ($self, $params, $name) = @_; - my @add = @{ $params->{$name}->{add} || [] }; - my @remove = @{ $params->{$name}->{remove} || [] }; - $name =~ s/s$// if $name ne 'alias'; - my $add_method = "add_$name"; - my $remove_method = "remove_$name"; - $self->$add_method($_) foreach @add; - $self->$remove_method($_) foreach @remove; + my ($self, $params, $name) = @_; + my @add = @{$params->{$name}->{add} || []}; + my @remove = @{$params->{$name}->{remove} || []}; + $name =~ s/s$// if $name ne 'alias'; + my $add_method = "add_$name"; + my $remove_method = "remove_$name"; + $self->$add_method($_) foreach @add; + $self->$remove_method($_) foreach @remove; } sub set_assigned_to { - my ($self, $value) = @_; - $self->set('assigned_to', $value); - # Store the old assignee. check_can_change_field() needs it. - $self->{'_old_assigned_to'} = $self->{'assigned_to_obj'}->id; - delete $self->{'assigned_to_obj'}; + my ($self, $value) = @_; + $self->set('assigned_to', $value); + + # Store the old assignee. check_can_change_field() needs it. + $self->{'_old_assigned_to'} = $self->{'assigned_to_obj'}->id; + delete $self->{'assigned_to_obj'}; } + sub reset_assigned_to { - my $self = shift; - my $comp = $self->component_obj; - $self->set_assigned_to($comp->default_assignee); + my $self = shift; + my $comp = $self->component_obj; + $self->set_assigned_to($comp->default_assignee); } sub set_bug_ignored { $_[0]->set('bug_ignored', $_[1]); } sub set_cclist_accessible { $_[0]->set('cclist_accessible', $_[1]); } sub set_comment_is_private { - my ($self, $comments, $isprivate) = @_; - $self->{comment_isprivate} ||= []; - my $is_insider = Bugzilla->user->is_insider; + my ($self, $comments, $isprivate) = @_; + $self->{comment_isprivate} ||= []; + my $is_insider = Bugzilla->user->is_insider; - $comments = { $comments => $isprivate } unless ref $comments; + $comments = {$comments => $isprivate} unless ref $comments; - foreach my $comment (@{$self->comments}) { - # Skip unmodified comment privacy. - next unless exists $comments->{$comment->id}; + foreach my $comment (@{$self->comments}) { - my $isprivate = delete $comments->{$comment->id} ? 1 : 0; - if ($isprivate != $comment->is_private) { - ThrowUserError('user_not_insider') unless $is_insider; - $comment->set_is_private($isprivate); - push @{$self->{comment_isprivate}}, $comment; - } + # Skip unmodified comment privacy. + next unless exists $comments->{$comment->id}; + + my $isprivate = delete $comments->{$comment->id} ? 1 : 0; + if ($isprivate != $comment->is_private) { + ThrowUserError('user_not_insider') unless $is_insider; + $comment->set_is_private($isprivate); + push @{$self->{comment_isprivate}}, $comment; } + } - # If there are still entries in $comments, then they are illegal. - ThrowUserError('comment_invalid_isprivate', { id => join(', ', keys %$comments) }) - if scalar keys %$comments; - - # If no comment privacy has been modified, remove this key. - delete $self->{comment_isprivate} unless scalar @{$self->{comment_isprivate}}; -} - -sub set_component { - my ($self, $name) = @_; - my $old_comp = $self->component_obj; - my $component = $self->_check_component($name); - if ($old_comp->id != $component->id) { - $self->{component_id} = $component->id; - $self->{component} = $component->name; - $self->{component_obj} = $component; - # For update() - $self->{_old_component_name} = $old_comp->name; - # Add in the Default CC of the new Component; - foreach my $cc (@{$component->initial_cc}) { - $self->add_cc($cc); - } + # If there are still entries in $comments, then they are illegal. + ThrowUserError('comment_invalid_isprivate', {id => join(', ', keys %$comments)}) + if scalar keys %$comments; + + # If no comment privacy has been modified, remove this key. + delete $self->{comment_isprivate} unless scalar @{$self->{comment_isprivate}}; +} + +sub set_component { + my ($self, $name) = @_; + my $old_comp = $self->component_obj; + my $component = $self->_check_component($name); + if ($old_comp->id != $component->id) { + $self->{component_id} = $component->id; + $self->{component} = $component->name; + $self->{component_obj} = $component; + + # For update() + $self->{_old_component_name} = $old_comp->name; + + # Add in the Default CC of the new Component; + foreach my $cc (@{$component->initial_cc}) { + $self->add_cc($cc); } + } } + sub set_custom_field { - my ($self, $field, $value) = @_; + my ($self, $field, $value) = @_; - if (ref $value eq 'ARRAY' && $field->type != FIELD_TYPE_MULTI_SELECT) { - $value = $value->[0]; - } - ThrowCodeError('field_not_custom', { field => $field }) if !$field->custom; - $self->set($field->name, $value); + if (ref $value eq 'ARRAY' && $field->type != FIELD_TYPE_MULTI_SELECT) { + $value = $value->[0]; + } + ThrowCodeError('field_not_custom', {field => $field}) if !$field->custom; + $self->set($field->name, $value); } sub set_deadline { $_[0]->set('deadline', $_[1]); } + sub set_dependencies { - my ($self, $dependson, $blocked) = @_; - my %extra = ( blocked => $blocked ); - $dependson = $self->_check_dependencies($dependson, 'dependson', \%extra); - $blocked = $extra{blocked}; - # These may already be detainted, but all setters are supposed to - # detaint their input if they've run a validator (just as though - # we had used Bugzilla::Object::set), so we do that here. - detaint_natural($_) foreach (@$dependson, @$blocked); - $self->{'dependson'} = $dependson; - $self->{'blocked'} = $blocked; - delete $self->{depends_on_obj}; - delete $self->{blocks_obj}; + my ($self, $dependson, $blocked) = @_; + my %extra = (blocked => $blocked); + $dependson = $self->_check_dependencies($dependson, 'dependson', \%extra); + $blocked = $extra{blocked}; + + # These may already be detainted, but all setters are supposed to + # detaint their input if they've run a validator (just as though + # we had used Bugzilla::Object::set), so we do that here. + detaint_natural($_) foreach (@$dependson, @$blocked); + $self->{'dependson'} = $dependson; + $self->{'blocked'} = $blocked; + delete $self->{depends_on_obj}; + delete $self->{blocks_obj}; } sub _clear_dup_id { $_[0]->{dup_id} = undef; } + sub set_dup_id { - my ($self, $dup_id) = @_; - my $old = $self->dup_id || 0; - $self->set('dup_id', $dup_id); - my $new = $self->dup_id; - return if $old == $new; - - # Make sure that we have the DUPLICATE resolution. This is needed - # if somebody calls set_dup_id without calling set_bug_status or - # set_resolution. - if ($self->resolution ne 'DUPLICATE') { - # Even if the current status is VERIFIED, we change it back to - # RESOLVED (or whatever the duplicate_or_move_bug_status is) here, - # because that's the same thing the UI does when you click on the - # "Mark as Duplicate" link. If people really want to retain their - # current status, they can use set_bug_status and set the DUPLICATE - # resolution before getting here. - $self->set_bug_status( - Bugzilla->params->{'duplicate_or_move_bug_status'}, - { resolution => 'DUPLICATE' }); - } - - # Update the other bug. - my $dupe_of = new Bugzilla::Bug($self->dup_id); - if (delete $self->{_add_dup_cc}) { - $dupe_of->add_cc($self->reporter); - } - $dupe_of->add_comment("", { type => CMT_HAS_DUPE, - extra_data => $self->id }); - $self->{_dup_for_update} = $dupe_of; - - # Now make sure that we add a duplicate comment on *this* bug. - # (Change an existing comment into a dup comment, if there is one, - # or add an empty dup comment.) - if ($self->{added_comments}) { - my @normal = grep { !defined $_->{type} || $_->{type} == CMT_NORMAL } - @{ $self->{added_comments} }; - # Turn the last one into a dup comment. - $normal[-1]->{type} = CMT_DUPE_OF; - $normal[-1]->{extra_data} = $self->dup_id; - } - else { - $self->add_comment('', { type => CMT_DUPE_OF, - extra_data => $self->dup_id }); - } + my ($self, $dup_id) = @_; + my $old = $self->dup_id || 0; + $self->set('dup_id', $dup_id); + my $new = $self->dup_id; + return if $old == $new; + + # Make sure that we have the DUPLICATE resolution. This is needed + # if somebody calls set_dup_id without calling set_bug_status or + # set_resolution. + if ($self->resolution ne 'DUPLICATE') { + + # Even if the current status is VERIFIED, we change it back to + # RESOLVED (or whatever the duplicate_or_move_bug_status is) here, + # because that's the same thing the UI does when you click on the + # "Mark as Duplicate" link. If people really want to retain their + # current status, they can use set_bug_status and set the DUPLICATE + # resolution before getting here. + $self->set_bug_status(Bugzilla->params->{'duplicate_or_move_bug_status'}, + {resolution => 'DUPLICATE'}); + } + + # Update the other bug. + my $dupe_of = new Bugzilla::Bug($self->dup_id); + if (delete $self->{_add_dup_cc}) { + $dupe_of->add_cc($self->reporter); + } + $dupe_of->add_comment("", {type => CMT_HAS_DUPE, extra_data => $self->id}); + $self->{_dup_for_update} = $dupe_of; + + # Now make sure that we add a duplicate comment on *this* bug. + # (Change an existing comment into a dup comment, if there is one, + # or add an empty dup comment.) + if ($self->{added_comments}) { + my @normal = grep { !defined $_->{type} || $_->{type} == CMT_NORMAL } + @{$self->{added_comments}}; + + # Turn the last one into a dup comment. + $normal[-1]->{type} = CMT_DUPE_OF; + $normal[-1]->{extra_data} = $self->dup_id; + } + else { + $self->add_comment('', {type => CMT_DUPE_OF, extra_data => $self->dup_id}); + } } sub set_estimated_time { $_[0]->set('estimated_time', $_[1]); } -sub _set_everconfirmed { $_[0]->set('everconfirmed', $_[1]); } +sub _set_everconfirmed { $_[0]->set('everconfirmed', $_[1]); } + sub set_flags { - my ($self, $flags, $new_flags) = @_; + my ($self, $flags, $new_flags) = @_; - Bugzilla::Flag->set_flag($self, $_) foreach (@$flags, @$new_flags); + Bugzilla::Flag->set_flag($self, $_) foreach (@$flags, @$new_flags); } -sub set_op_sys { $_[0]->set('op_sys', $_[1]); } -sub set_platform { $_[0]->set('rep_platform', $_[1]); } -sub set_priority { $_[0]->set('priority', $_[1]); } +sub set_op_sys { $_[0]->set('op_sys', $_[1]); } +sub set_platform { $_[0]->set('rep_platform', $_[1]); } +sub set_priority { $_[0]->set('priority', $_[1]); } + # For security reasons, you have to use set_all to change the product. # See the strict_isolation check in set_all for an explanation. sub _set_product { - my ($self, $name, $params) = @_; - my $old_product = $self->product_obj; - my $product = $self->_check_product($name); - - my $product_changed = 0; - if ($old_product->id != $product->id) { - $self->{product_id} = $product->id; - $self->{product} = $product->name; - $self->{product_obj} = $product; - # For update() - $self->{_old_product_name} = $old_product->name; - # Delete fields that depend upon the old Product value. - delete $self->{choices}; - $product_changed = 1; - } + my ($self, $name, $params) = @_; + my $old_product = $self->product_obj; + my $product = $self->_check_product($name); - $params ||= {}; - # We delete these so that they're not set again later in set_all. - my $comp_name = delete $params->{component} || $self->component; - my $vers_name = delete $params->{version} || $self->version; - my $tm_name = delete $params->{target_milestone}; - # This way, if usetargetmilestone is off and we've changed products, - # set_target_milestone will reset our target_milestone to - # $product->default_milestone. But if we haven't changed products, - # we don't reset anything. - if (!defined $tm_name - && (Bugzilla->params->{'usetargetmilestone'} || !$product_changed)) - { - $tm_name = $self->target_milestone; - } + my $product_changed = 0; + if ($old_product->id != $product->id) { + $self->{product_id} = $product->id; + $self->{product} = $product->name; + $self->{product_obj} = $product; - if ($product_changed && Bugzilla->usage_mode == USAGE_MODE_BROWSER) { - # Try to set each value with the new product. - # Have to set error_mode because Throw*Error calls exit() otherwise. - my $old_error_mode = Bugzilla->error_mode; - Bugzilla->error_mode(ERROR_MODE_DIE); - my $component_ok = eval { $self->set_component($comp_name); 1; }; - my $version_ok = eval { $self->set_version($vers_name); 1; }; - my $milestone_ok = 1; - # Reporters can move bugs between products but not set the TM. - if ($self->check_can_change_field('target_milestone', 0, 1)) { - $milestone_ok = eval { $self->set_target_milestone($tm_name); 1; }; - } - else { - # Have to set this directly to bypass the validators. - $self->{target_milestone} = $product->default_milestone; - } - # If there were any errors thrown, make sure we don't mess up any - # other part of Bugzilla that checks $@. - undef $@; - Bugzilla->error_mode($old_error_mode); - - my $verified = $params->{product_change_confirmed}; - my %vars; - if (!$verified || !$component_ok || !$version_ok || !$milestone_ok) { - $vars{defaults} = { - # Note that because of the eval { set } above, these are - # already set correctly if they're valid, otherwise they're - # set to some invalid value which the template will ignore. - component => $self->component, - version => $self->version, - milestone => $milestone_ok ? $self->target_milestone - : $product->default_milestone - }; - $vars{components} = [map { $_->name } grep($_->is_active, @{$product->components})]; - $vars{milestones} = [map { $_->name } grep($_->is_active, @{$product->milestones})]; - $vars{versions} = [map { $_->name } grep($_->is_active, @{$product->versions})]; - } + # For update() + $self->{_old_product_name} = $old_product->name; - if (!$verified) { - $vars{verify_bug_groups} = 1; - my $dbh = Bugzilla->dbh; - 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 + # Delete fields that depend upon the old Product value. + delete $self->{choices}; + $product_changed = 1; + } + + $params ||= {}; + + # We delete these so that they're not set again later in set_all. + my $comp_name = delete $params->{component} || $self->component; + my $vers_name = delete $params->{version} || $self->version; + my $tm_name = delete $params->{target_milestone}; + + # This way, if usetargetmilestone is off and we've changed products, + # set_target_milestone will reset our target_milestone to + # $product->default_milestone. But if we haven't changed products, + # we don't reset anything. + if (!defined $tm_name + && (Bugzilla->params->{'usetargetmilestone'} || !$product_changed)) + { + $tm_name = $self->target_milestone; + } + + if ($product_changed && Bugzilla->usage_mode == USAGE_MODE_BROWSER) { + + # Try to set each value with the new product. + # Have to set error_mode because Throw*Error calls exit() otherwise. + my $old_error_mode = Bugzilla->error_mode; + Bugzilla->error_mode(ERROR_MODE_DIE); + my $component_ok = eval { $self->set_component($comp_name); 1; }; + my $version_ok = eval { $self->set_version($vers_name); 1; }; + my $milestone_ok = 1; + + # Reporters can move bugs between products but not set the TM. + if ($self->check_can_change_field('target_milestone', 0, 1)) { + $milestone_ok = eval { $self->set_target_milestone($tm_name); 1; }; + } + else { + # Have to set this directly to bypass the validators. + $self->{target_milestone} = $product->default_milestone; + } + + # If there were any errors thrown, make sure we don't mess up any + # other part of Bugzilla that checks $@. + undef $@; + Bugzilla->error_mode($old_error_mode); + + my $verified = $params->{product_change_confirmed}; + my %vars; + if (!$verified || !$component_ok || !$version_ok || !$milestone_ok) { + $vars{defaults} = { + + # Note that because of the eval { set } above, these are + # already set correctly if they're valid, otherwise they're + # set to some invalid value which the template will ignore. + component => $self->component, + version => $self->version, + milestone => $milestone_ok + ? $self->target_milestone + : $product->default_milestone + }; + $vars{components} + = [map { $_->name } grep($_->is_active, @{$product->components})]; + $vars{milestones} + = [map { $_->name } grep($_->is_active, @{$product->milestones})]; + $vars{versions} = [map { $_->name } grep($_->is_active, @{$product->versions})]; + } + + if (!$verified) { + $vars{verify_bug_groups} = 1; + my $dbh = Bugzilla->dbh; + 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 FROM bug_group_map AS bgm WHERE bgm.bug_id IN (' . join(',', ('?') x @idlist) . ') AND bgm.group_id NOT IN @@ -2726,159 +2819,172 @@ sub _set_product { WHERE gcm.product_id = ? AND ( (gcm.membercontrol != ? AND gcm.group_id IN (' - . Bugzilla->user->groups_as_string . ')) - OR gcm.othercontrol != ?) )', - undef, (@idlist, $product->id, CONTROLMAPNA, CONTROLMAPNA)); - $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; - my $template = Bugzilla->template; - $template->process("bug/process/verify-new-product.html.tmpl", - \%vars) || ThrowTemplateError($template->error()); - exit; - } + . Bugzilla->user->groups_as_string . ')) + OR gcm.othercontrol != ?) )', undef, + (@idlist, $product->id, CONTROLMAPNA, CONTROLMAPNA) + ); + $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; + my $template = Bugzilla->template; + $template->process("bug/process/verify-new-product.html.tmpl", \%vars) + || ThrowTemplateError($template->error()); + exit; + } + } + else { + # When we're not in the browser (or we didn't change the product), we + # just die if any of these are invalid. + $self->set_component($comp_name); + $self->set_version($vers_name); + if ($product_changed + and !$self->check_can_change_field('target_milestone', 0, 1)) + { + # Have to set this directly to bypass the validators. + $self->{target_milestone} = $product->default_milestone; } else { - # When we're not in the browser (or we didn't change the product), we - # just die if any of these are invalid. - $self->set_component($comp_name); - $self->set_version($vers_name); - if ($product_changed - and !$self->check_can_change_field('target_milestone', 0, 1)) - { - # Have to set this directly to bypass the validators. - $self->{target_milestone} = $product->default_milestone; - } - else { - $self->set_target_milestone($tm_name); - } + $self->set_target_milestone($tm_name); } + } - if ($product_changed) { - # Remove groups that can't be set in the new product. - # We copy this array because the original array is modified while we're - # working, and that confuses "foreach". - my @current_groups = @{$self->groups_in}; - foreach my $group (@current_groups) { - if (!$product->group_is_valid($group)) { - $self->remove_group($group); - } - } + if ($product_changed) { - # Make sure the bug is in all the mandatory groups for the new product. - foreach my $group (@{$product->groups_mandatory}) { - $self->add_group($group); - } + # Remove groups that can't be set in the new product. + # We copy this array because the original array is modified while we're + # working, and that confuses "foreach". + my @current_groups = @{$self->groups_in}; + foreach my $group (@current_groups) { + if (!$product->group_is_valid($group)) { + $self->remove_group($group); + } } - - return $product_changed; + + # Make sure the bug is in all the mandatory groups for the new product. + foreach my $group (@{$product->groups_mandatory}) { + $self->add_group($group); + } + } + + return $product_changed; } sub set_qa_contact { - my ($self, $value) = @_; - $self->set('qa_contact', $value); - # Store the old QA contact. check_can_change_field() needs it. - if ($self->{'qa_contact_obj'}) { - $self->{'_old_qa_contact'} = $self->{'qa_contact_obj'}->id; - } - delete $self->{'qa_contact_obj'}; + my ($self, $value) = @_; + $self->set('qa_contact', $value); + + # Store the old QA contact. check_can_change_field() needs it. + if ($self->{'qa_contact_obj'}) { + $self->{'_old_qa_contact'} = $self->{'qa_contact_obj'}->id; + } + delete $self->{'qa_contact_obj'}; } + sub reset_qa_contact { - my $self = shift; - my $comp = $self->component_obj; - $self->set_qa_contact($comp->default_qa_contact); + my $self = shift; + my $comp = $self->component_obj; + $self->set_qa_contact($comp->default_qa_contact); } sub set_remaining_time { $_[0]->set('remaining_time', $_[1]); } + # Used only when closing a bug or moving between closed states. sub _zero_remaining_time { $_[0]->{'remaining_time'} = 0; } sub set_reporter_accessible { $_[0]->set('reporter_accessible', $_[1]); } + sub set_resolution { - my ($self, $value, $params) = @_; - - my $old_res = $self->resolution; - $self->set('resolution', $value); - delete $self->{choices}; - my $new_res = $self->resolution; + my ($self, $value, $params) = @_; - if ($new_res ne $old_res) { - # Clear the dup_id if we're leaving the dup resolution. - if ($old_res eq 'DUPLICATE') { - $self->_clear_dup_id(); - } - # Duplicates should have no remaining time left. - elsif ($new_res eq 'DUPLICATE' && $self->remaining_time != 0) { - $self->_zero_remaining_time(); - } + my $old_res = $self->resolution; + $self->set('resolution', $value); + delete $self->{choices}; + my $new_res = $self->resolution; + + if ($new_res ne $old_res) { + + # Clear the dup_id if we're leaving the dup resolution. + if ($old_res eq 'DUPLICATE') { + $self->_clear_dup_id(); } - - # We don't check if we're entering or leaving the dup resolution here, - # because we could be moving from being a dup of one bug to being a dup - # of another, theoretically. Note that this code block will also run - # when going between different closed states. - if ($self->resolution eq 'DUPLICATE') { - if (my $dup_id = $params->{dup_id}) { - $self->set_dup_id($dup_id); - } - elsif (!$self->dup_id) { - ThrowUserError('dupe_id_required'); - } + + # Duplicates should have no remaining time left. + elsif ($new_res eq 'DUPLICATE' && $self->remaining_time != 0) { + $self->_zero_remaining_time(); } + } - # This method has handled dup_id, so set_all doesn't have to worry - # about it now. - delete $params->{dup_id}; + # We don't check if we're entering or leaving the dup resolution here, + # because we could be moving from being a dup of one bug to being a dup + # of another, theoretically. Note that this code block will also run + # when going between different closed states. + if ($self->resolution eq 'DUPLICATE') { + if (my $dup_id = $params->{dup_id}) { + $self->set_dup_id($dup_id); + } + elsif (!$self->dup_id) { + ThrowUserError('dupe_id_required'); + } + } + + # This method has handled dup_id, so set_all doesn't have to worry + # about it now. + delete $params->{dup_id}; } + sub clear_resolution { - my $self = shift; - if (!$self->status->is_open) { - ThrowUserError('resolution_cant_clear', { bug_id => $self->id }); - } - $self->{'resolution'} = ''; - $self->_clear_dup_id; + my $self = shift; + if (!$self->status->is_open) { + ThrowUserError('resolution_cant_clear', {bug_id => $self->id}); + } + $self->{'resolution'} = ''; + $self->_clear_dup_id; } -sub set_severity { $_[0]->set('bug_severity', $_[1]); } +sub set_severity { $_[0]->set('bug_severity', $_[1]); } + sub set_bug_status { - my ($self, $status, $params) = @_; - my $old_status = $self->status; - $self->set('bug_status', $status); - delete $self->{'status'}; - delete $self->{'statuses_available'}; - delete $self->{'choices'}; - my $new_status = $self->status; - - if ($new_status->is_open) { - # Check for the everconfirmed transition - $self->_set_everconfirmed($new_status->name eq 'UNCONFIRMED' ? 0 : 1); - $self->clear_resolution(); - # Calling clear_resolution handled the "resolution" and "dup_id" - # setting, so set_all doesn't have to worry about them. - delete $params->{resolution}; - delete $params->{dup_id}; + my ($self, $status, $params) = @_; + my $old_status = $self->status; + $self->set('bug_status', $status); + delete $self->{'status'}; + delete $self->{'statuses_available'}; + delete $self->{'choices'}; + my $new_status = $self->status; + + if ($new_status->is_open) { + + # Check for the everconfirmed transition + $self->_set_everconfirmed($new_status->name eq 'UNCONFIRMED' ? 0 : 1); + $self->clear_resolution(); + + # Calling clear_resolution handled the "resolution" and "dup_id" + # setting, so set_all doesn't have to worry about them. + delete $params->{resolution}; + delete $params->{dup_id}; + } + else { + # We do this here so that we can make sure closed statuses have + # resolutions. + my $resolution = $self->resolution; + + # We need to check "defined" to prevent people from passing + # a blank resolution in the WebService, which would otherwise fail + # silently. + if (defined $params->{resolution}) { + $resolution = delete $params->{resolution}; } - else { - # We do this here so that we can make sure closed statuses have - # resolutions. - my $resolution = $self->resolution; - # We need to check "defined" to prevent people from passing - # a blank resolution in the WebService, which would otherwise fail - # silently. - if (defined $params->{resolution}) { - $resolution = delete $params->{resolution}; - } - $self->set_resolution($resolution, $params); + $self->set_resolution($resolution, $params); - # Changing between closed statuses zeros the remaining time. - if ($new_status->id != $old_status->id && $self->remaining_time != 0) { - $self->_zero_remaining_time(); - } + # Changing between closed statuses zeros the remaining time. + if ($new_status->id != $old_status->id && $self->remaining_time != 0) { + $self->_zero_remaining_time(); } + } } sub set_status_whiteboard { $_[0]->set('status_whiteboard', $_[1]); } sub set_summary { $_[0]->set('short_desc', $_[1]); } @@ -2895,373 +3001,390 @@ sub set_version { $_[0]->set('version', $_[1]); } # Accepts a User object or a username. Adds the user only if they # don't already exist as a CC on the bug. sub add_cc { - my ($self, $user_or_name) = @_; - return if !$user_or_name; - my $user = ref $user_or_name ? $user_or_name - : Bugzilla::User->check($user_or_name); - $self->_check_strict_isolation_for_user($user); - my $cc_users = $self->cc_users; - push(@$cc_users, $user) if !grep($_->id == $user->id, @$cc_users); + my ($self, $user_or_name) = @_; + return if !$user_or_name; + my $user + = ref $user_or_name ? $user_or_name : Bugzilla::User->check($user_or_name); + $self->_check_strict_isolation_for_user($user); + my $cc_users = $self->cc_users; + push(@$cc_users, $user) if !grep($_->id == $user->id, @$cc_users); } # Accepts a User object or a username. Removes the User if they exist # in the list, but doesn't throw an error if they don't exist. sub remove_cc { - my ($self, $user_or_name) = @_; - my $user = ref $user_or_name ? $user_or_name - : Bugzilla::User->check($user_or_name); - my $currentUser = Bugzilla->user; - if (!$self->user->{'canedit'} && $user->id != $currentUser->id) { - ThrowUserError('cc_remove_denied'); - } - my $cc_users = $self->cc_users; - @$cc_users = grep { $_->id != $user->id } @$cc_users; + my ($self, $user_or_name) = @_; + my $user + = ref $user_or_name ? $user_or_name : Bugzilla::User->check($user_or_name); + my $currentUser = Bugzilla->user; + if (!$self->user->{'canedit'} && $user->id != $currentUser->id) { + ThrowUserError('cc_remove_denied'); + } + my $cc_users = $self->cc_users; + @$cc_users = grep { $_->id != $user->id } @$cc_users; } sub add_alias { - my ($self, $alias) = @_; - return if !$alias; - my $aliases = $self->_check_alias($alias); - $alias = $aliases->[0]; - my @new_aliases; - my $found = 0; - foreach my $old_alias (@{ $self->alias }) { - if (lc($old_alias) eq lc($alias)) { - push(@new_aliases, $alias); - $found = 1; - } - else { - push(@new_aliases, $old_alias); - } + my ($self, $alias) = @_; + return if !$alias; + my $aliases = $self->_check_alias($alias); + $alias = $aliases->[0]; + my @new_aliases; + my $found = 0; + foreach my $old_alias (@{$self->alias}) { + if (lc($old_alias) eq lc($alias)) { + push(@new_aliases, $alias); + $found = 1; + } + else { + push(@new_aliases, $old_alias); } - push(@new_aliases, $alias) if !$found; - $self->{alias} = \@new_aliases; + } + push(@new_aliases, $alias) if !$found; + $self->{alias} = \@new_aliases; } sub remove_alias { - my ($self, $alias) = @_; - my $bug_aliases = $self->alias; - @$bug_aliases = grep { $_ ne $alias } @$bug_aliases; + my ($self, $alias) = @_; + my $bug_aliases = $self->alias; + @$bug_aliases = grep { $_ ne $alias } @$bug_aliases; } # $bug->add_comment("comment", {isprivate => 1, work_time => 10.5, # type => CMT_NORMAL, extra_data => $data}); sub add_comment { - my ($self, $comment, $params) = @_; + my ($self, $comment, $params) = @_; - $params ||= {}; + $params ||= {}; - # Fill out info that doesn't change and callers may not pass in - $params->{'bug_id'} = $self; - $params->{'thetext'} = defined($comment) ? $comment : ''; + # Fill out info that doesn't change and callers may not pass in + $params->{'bug_id'} = $self; + $params->{'thetext'} = defined($comment) ? $comment : ''; - # Validate all the entered data - Bugzilla::Comment->check_required_create_fields($params); - $params = Bugzilla::Comment->run_create_validators($params); + # Validate all the entered data + Bugzilla::Comment->check_required_create_fields($params); + $params = Bugzilla::Comment->run_create_validators($params); - # This makes it so we won't create new comments when there is nothing - # to add - if ($params->{'thetext'} eq '' - && !($params->{type} || abs($params->{work_time} || 0))) - { - return; - } + # This makes it so we won't create new comments when there is nothing + # to add + if ($params->{'thetext'} eq '' + && !($params->{type} || abs($params->{work_time} || 0))) + { + return; + } - # If the user has explicitly set remaining_time, this will be overridden - # later in set_all. But if they haven't, this keeps remaining_time - # up-to-date. - if ($params->{work_time}) { - $self->set_remaining_time(max($self->remaining_time - $params->{work_time}, 0)); - } + # If the user has explicitly set remaining_time, this will be overridden + # later in set_all. But if they haven't, this keeps remaining_time + # up-to-date. + if ($params->{work_time}) { + $self->set_remaining_time(max($self->remaining_time - $params->{work_time}, 0)); + } - $self->{added_comments} ||= []; + $self->{added_comments} ||= []; - push(@{$self->{added_comments}}, $params); + push(@{$self->{added_comments}}, $params); } sub modify_keywords { - my ($self, $keywords, $action) = @_; + my ($self, $keywords, $action) = @_; - if (!$action || !grep { $action eq $_ } qw(add remove set)) { - $action = '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; + $keywords = $self->_check_keywords($keywords); + my @old_keywords = @{$self->keyword_objects}; + my @result; - if ($action eq 'set') { - @result = @$keywords; + if ($action eq 'set') { + @result = @$keywords; + } + else { + # We're adding or deleting specific keywords. + my %keys = map { $_->id => $_ } @old_keywords; + if ($action eq 'add') { + $keys{$_->id} = $_ foreach @$keywords; } else { - # We're adding or deleting specific keywords. - my %keys = map { $_->id => $_ } @old_keywords; - if ($action eq 'add') { - $keys{$_->id} = $_ foreach @$keywords; - } - else { - delete $keys{$_->id} foreach @$keywords; - } - @result = values %keys; + delete $keys{$_->id} foreach @$keywords; } + @result = values %keys; + } - # 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; + # 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; + # 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)); - my $check = $self->check_can_change_field('keywords', 0, 1, \$privs) - || ThrowUserError('illegal_change', { field => 'keywords', - oldvalue => $self->keywords, - newvalue => $new, - privs => $privs }); - } - - $self->{'keyword_objects'} = \@result; + if ($any_changes) { + my $privs; + my $new = join(', ', (map { $_->name } @result)); + my $check + = $self->check_can_change_field('keywords', 0, 1, \$privs) || ThrowUserError( + 'illegal_change', + { + field => 'keywords', + oldvalue => $self->keywords, + newvalue => $new, + privs => $privs + } + ); + } + + $self->{'keyword_objects'} = \@result; } sub add_group { - my ($self, $group) = @_; + my ($self, $group) = @_; - # If the user enters "FoO" but the DB has "Foo", $group->name would - # return "Foo" and thus revealing the existence of the group name. - # So we have to store and pass the name as entered by the user to - # the error message, if we have it. - my $group_name = blessed($group) ? $group->name : $group; - my $args = { name => $group_name, product => $self->product, - bug_id => $self->id, action => 'add' }; + # If the user enters "FoO" but the DB has "Foo", $group->name would + # return "Foo" and thus revealing the existence of the group name. + # So we have to store and pass the name as entered by the user to + # the error message, if we have it. + my $group_name = blessed($group) ? $group->name : $group; + my $args = { + name => $group_name, + product => $self->product, + bug_id => $self->id, + action => 'add' + }; - $group = Bugzilla::Group->check_no_disclose($args) if !blessed $group; + $group = Bugzilla::Group->check_no_disclose($args) if !blessed $group; - # If the bug is already in this group, then there is nothing to do. - return if $self->in_group($group); + # If the bug is already in this group, then there is nothing to do. + return if $self->in_group($group); - # Make sure that bugs in this product can actually be restricted - # to this group by the current user. - $self->product_obj->group_is_settable($group) - || ThrowUserError('group_restriction_not_allowed', $args); + # Make sure that bugs in this product can actually be restricted + # to this group by the current user. + $self->product_obj->group_is_settable($group) + || ThrowUserError('group_restriction_not_allowed', $args); - # OtherControl people can add groups only during a product change, - # and only when the group is not NA for them. - if (!Bugzilla->user->in_group($group->name)) { - my $controls = $self->product_obj->group_controls->{$group->id}; - if (!$self->{_old_product_name} - || $controls->{othercontrol} == CONTROLMAPNA) - { - ThrowUserError('group_restriction_not_allowed', $args); - } + # OtherControl people can add groups only during a product change, + # and only when the group is not NA for them. + if (!Bugzilla->user->in_group($group->name)) { + my $controls = $self->product_obj->group_controls->{$group->id}; + if (!$self->{_old_product_name} || $controls->{othercontrol} == CONTROLMAPNA) { + ThrowUserError('group_restriction_not_allowed', $args); } + } - my $current_groups = $self->groups_in; - push(@$current_groups, $group); + my $current_groups = $self->groups_in; + push(@$current_groups, $group); } sub remove_group { - my ($self, $group) = @_; + my ($self, $group) = @_; - # See add_group() for the reason why we store the user input. - my $group_name = blessed($group) ? $group->name : $group; - my $args = { name => $group_name, product => $self->product, - bug_id => $self->id, action => 'remove' }; + # See add_group() for the reason why we store the user input. + my $group_name = blessed($group) ? $group->name : $group; + my $args = { + name => $group_name, + product => $self->product, + bug_id => $self->id, + action => 'remove' + }; - $group = Bugzilla::Group->check_no_disclose($args) if !blessed $group; + $group = Bugzilla::Group->check_no_disclose($args) if !blessed $group; - # If the bug isn't in this group, then either the name is misspelled, - # or the group really doesn't exist. Let the user know about this problem. - $self->in_group($group) || ThrowUserError('group_invalid_removal', $args); + # If the bug isn't in this group, then either the name is misspelled, + # or the group really doesn't exist. Let the user know about this problem. + $self->in_group($group) || ThrowUserError('group_invalid_removal', $args); - # Check if this is a valid group for this product. You can *always* - # remove a group that is not valid for this product (set_product does this). - # This particularly happens when we're moving a bug to a new product. - # You still have to be a member of an inactive group to remove it. - if ($self->product_obj->group_is_valid($group)) { - my $controls = $self->product_obj->group_controls->{$group->id}; + # Check if this is a valid group for this product. You can *always* + # remove a group that is not valid for this product (set_product does this). + # This particularly happens when we're moving a bug to a new product. + # You still have to be a member of an inactive group to remove it. + if ($self->product_obj->group_is_valid($group)) { + my $controls = $self->product_obj->group_controls->{$group->id}; - # Nobody can ever remove a Mandatory group, unless it became inactive. - if ($controls->{membercontrol} == CONTROLMAPMANDATORY && $group->is_active) { - ThrowUserError('group_invalid_removal', $args); - } + # Nobody can ever remove a Mandatory group, unless it became inactive. + if ($controls->{membercontrol} == CONTROLMAPMANDATORY && $group->is_active) { + ThrowUserError('group_invalid_removal', $args); + } - # OtherControl people can remove groups only during a product change, - # and only when they are non-Mandatory and non-NA. - if (!Bugzilla->user->in_group($group->name)) { - if (!$self->{_old_product_name} - || $controls->{othercontrol} == CONTROLMAPMANDATORY - || $controls->{othercontrol} == CONTROLMAPNA) - { - ThrowUserError('group_invalid_removal', $args); - } - } + # OtherControl people can remove groups only during a product change, + # and only when they are non-Mandatory and non-NA. + if (!Bugzilla->user->in_group($group->name)) { + if (!$self->{_old_product_name} + || $controls->{othercontrol} == CONTROLMAPMANDATORY + || $controls->{othercontrol} == CONTROLMAPNA) + { + ThrowUserError('group_invalid_removal', $args); + } } + } - my $current_groups = $self->groups_in; - @$current_groups = grep { $_->id != $group->id } @$current_groups; + my $current_groups = $self->groups_in; + @$current_groups = grep { $_->id != $group->id } @$current_groups; } sub add_see_also { - my ($self, $input, $skip_recursion) = @_; - - # This is needed by xt/search.t. - $input = $input->name if blessed($input); + my ($self, $input, $skip_recursion) = @_; - $input = trim($input); - return if !$input; + # This is needed by xt/search.t. + $input = $input->name if blessed($input); - my ($class, $uri) = Bugzilla::BugUrl->class_for($input); - - my $params = { value => $uri, bug_id => $self, class => $class }; - $class->check_required_create_fields($params); - - my $field_values = $class->run_create_validators($params); - my $value = $field_values->{value}->as_string; - trick_taint($value); - $field_values->{value} = $value; - - # We only add the new URI if it hasn't been added yet. URIs are - # case-sensitive, but most of our DBs are case-insensitive, so we do - # this check case-insensitively. - if (!grep { lc($_->name) eq lc($value) } @{ $self->see_also }) { - my $privs; - my $can = $self->check_can_change_field('see_also', '', $value, \$privs); - if (!$can) { - ThrowUserError('illegal_change', { field => 'see_also', - newvalue => $value, - privs => $privs }); - } - # If this is a link to a local bug then save the - # ref bug id for sending changes email. - my $ref_bug = delete $field_values->{ref_bug}; - if ($class->isa('Bugzilla::BugUrl::Bugzilla::Local') - and !$skip_recursion - and $ref_bug->check_can_change_field('see_also', '', $self->id, \$privs)) - { - $ref_bug->add_see_also($self->id, 'skip_recursion'); - push @{ $self->{_update_ref_bugs} }, $ref_bug; - push @{ $self->{see_also_changes} }, $ref_bug->id; - } - push @{ $self->{see_also} }, bless ($field_values, $class); - } -} + $input = trim($input); + return if !$input; -sub remove_see_also { - my ($self, $url, $skip_recursion) = @_; - my $see_also = $self->see_also; + my ($class, $uri) = Bugzilla::BugUrl->class_for($input); - # This is needed by xt/search.t. - $url = $url->name if blessed($url); + my $params = {value => $uri, bug_id => $self, class => $class}; + $class->check_required_create_fields($params); - my ($removed_bug_url, $new_see_also) = - part { lc($_->name) ne lc($url) } @$see_also; + my $field_values = $class->run_create_validators($params); + my $value = $field_values->{value}->as_string; + trick_taint($value); + $field_values->{value} = $value; + # We only add the new URI if it hasn't been added yet. URIs are + # case-sensitive, but most of our DBs are case-insensitive, so we do + # this check case-insensitively. + if (!grep { lc($_->name) eq lc($value) } @{$self->see_also}) { my $privs; - my $can = $self->check_can_change_field('see_also', $see_also, $new_see_also, \$privs); + my $can = $self->check_can_change_field('see_also', '', $value, \$privs); if (!$can) { - ThrowUserError('illegal_change', { field => 'see_also', - oldvalue => $url, - privs => $privs }); + ThrowUserError('illegal_change', + {field => 'see_also', newvalue => $value, privs => $privs}); } - # Since we remove also the url from the referenced bug, - # we need to notify changes for that bug too. - $removed_bug_url = $removed_bug_url->[0]; - if (!$skip_recursion and $removed_bug_url - and $removed_bug_url->isa('Bugzilla::BugUrl::Bugzilla::Local') - and $removed_bug_url->ref_bug_url) + # If this is a link to a local bug then save the + # ref bug id for sending changes email. + my $ref_bug = delete $field_values->{ref_bug}; + if ( $class->isa('Bugzilla::BugUrl::Bugzilla::Local') + and !$skip_recursion + and $ref_bug->check_can_change_field('see_also', '', $self->id, \$privs)) { - my $ref_bug - = Bugzilla::Bug->check($removed_bug_url->ref_bug_url->bug_id); + $ref_bug->add_see_also($self->id, 'skip_recursion'); + push @{$self->{_update_ref_bugs}}, $ref_bug; + push @{$self->{see_also_changes}}, $ref_bug->id; + } + push @{$self->{see_also}}, bless($field_values, $class); + } +} - if (Bugzilla->user->can_edit_product($ref_bug->product_id) - and $ref_bug->check_can_change_field('see_also', $self->id, '', \$privs)) - { - my $self_url = $removed_bug_url->local_uri($self->id); - $ref_bug->remove_see_also($self_url, 'skip_recursion'); - push @{ $self->{_update_ref_bugs} }, $ref_bug; - push @{ $self->{see_also_changes} }, $ref_bug->id; - } +sub remove_see_also { + my ($self, $url, $skip_recursion) = @_; + my $see_also = $self->see_also; + + # This is needed by xt/search.t. + $url = $url->name if blessed($url); + + my ($removed_bug_url, $new_see_also) + = part { lc($_->name) ne lc($url) } @$see_also; + + my $privs; + my $can = $self->check_can_change_field('see_also', $see_also, $new_see_also, + \$privs); + if (!$can) { + ThrowUserError('illegal_change', + {field => 'see_also', oldvalue => $url, privs => $privs}); + } + + # Since we remove also the url from the referenced bug, + # we need to notify changes for that bug too. + $removed_bug_url = $removed_bug_url->[0]; + if ( !$skip_recursion + and $removed_bug_url + and $removed_bug_url->isa('Bugzilla::BugUrl::Bugzilla::Local') + and $removed_bug_url->ref_bug_url) + { + my $ref_bug = Bugzilla::Bug->check($removed_bug_url->ref_bug_url->bug_id); + + if (Bugzilla->user->can_edit_product($ref_bug->product_id) + and $ref_bug->check_can_change_field('see_also', $self->id, '', \$privs)) + { + my $self_url = $removed_bug_url->local_uri($self->id); + $ref_bug->remove_see_also($self_url, 'skip_recursion'); + push @{$self->{_update_ref_bugs}}, $ref_bug; + push @{$self->{see_also_changes}}, $ref_bug->id; } + } - $self->{see_also} = $new_see_also || []; + $self->{see_also} = $new_see_also || []; } sub add_tag { - my ($self, $tag) = @_; - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; - $tag = $self->_check_tag_name($tag); - - my $tag_id = $user->tags->{$tag}->{id}; - # If this tag doesn't exist for this user yet, create it. - if (!$tag_id) { - $dbh->do('INSERT INTO tag (user_id, name) VALUES (?, ?)', - undef, ($user->id, $tag)); - - $tag_id = $dbh->selectrow_array('SELECT id FROM tag - WHERE name = ? AND user_id = ?', - undef, ($tag, $user->id)); - # The list has changed. - delete $user->{tags}; - } - # Do nothing if this tag is already set for this bug. - return if grep { $_ eq $tag } @{$self->tags}; + my ($self, $tag) = @_; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + $tag = $self->_check_tag_name($tag); + + my $tag_id = $user->tags->{$tag}->{id}; + + # If this tag doesn't exist for this user yet, create it. + if (!$tag_id) { + $dbh->do('INSERT INTO tag (user_id, name) VALUES (?, ?)', + undef, ($user->id, $tag)); + + $tag_id = $dbh->selectrow_array( + 'SELECT id FROM tag + WHERE name = ? AND user_id = ?', undef, + ($tag, $user->id) + ); - # Increment the counter. Do it before the SQL call below, - # to not count the tag twice. - $user->tags->{$tag}->{bug_count}++; + # The list has changed. + delete $user->{tags}; + } - $dbh->do('INSERT INTO bug_tag (bug_id, tag_id) VALUES (?, ?)', - undef, ($self->id, $tag_id)); + # Do nothing if this tag is already set for this bug. + return if grep { $_ eq $tag } @{$self->tags}; - push(@{$self->{tags}}, $tag); + # Increment the counter. Do it before the SQL call below, + # to not count the tag twice. + $user->tags->{$tag}->{bug_count}++; + + $dbh->do('INSERT INTO bug_tag (bug_id, tag_id) VALUES (?, ?)', + undef, ($self->id, $tag_id)); + + push(@{$self->{tags}}, $tag); } sub remove_tag { - my ($self, $tag) = @_; - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; - $tag = $self->_check_tag_name($tag); + my ($self, $tag) = @_; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + $tag = $self->_check_tag_name($tag); - my $tag_id = exists $user->tags->{$tag} ? $user->tags->{$tag}->{id} : undef; - # Do nothing if the user doesn't use this tag, or didn't set it for this bug. - return unless ($tag_id && grep { $_ eq $tag } @{$self->tags}); + my $tag_id = exists $user->tags->{$tag} ? $user->tags->{$tag}->{id} : undef; - $dbh->do('DELETE FROM bug_tag WHERE bug_id = ? AND tag_id = ?', - undef, ($self->id, $tag_id)); + # Do nothing if the user doesn't use this tag, or didn't set it for this bug. + return unless ($tag_id && grep { $_ eq $tag } @{$self->tags}); - $self->{tags} = [grep { $_ ne $tag } @{$self->tags}]; + $dbh->do('DELETE FROM bug_tag WHERE bug_id = ? AND tag_id = ?', + undef, ($self->id, $tag_id)); - # Decrement the counter, and delete the tag if no bugs are using it anymore. - if (!--$user->tags->{$tag}->{bug_count}) { - $dbh->do('DELETE FROM tag WHERE name = ? AND user_id = ?', - undef, ($tag, $user->id)); + $self->{tags} = [grep { $_ ne $tag } @{$self->tags}]; - # The list has changed. - delete $user->{tags}; - } + # Decrement the counter, and delete the tag if no bugs are using it anymore. + if (!--$user->tags->{$tag}->{bug_count}) { + $dbh->do('DELETE FROM tag WHERE name = ? AND user_id = ?', + undef, ($tag, $user->id)); + + # The list has changed. + delete $user->{tags}; + } } sub tags { - my $self = shift; - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; - - # This method doesn't support several users using the same bug object. - if (!exists $self->{tags}) { - $self->{tags} = $dbh->selectcol_arrayref( - 'SELECT name FROM bug_tag + my $self = shift; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + + # This method doesn't support several users using the same bug object. + if (!exists $self->{tags}) { + $self->{tags} = $dbh->selectcol_arrayref( + 'SELECT name FROM bug_tag INNER JOIN tag ON tag.id = bug_tag.tag_id - WHERE bug_id = ? AND user_id = ?', - undef, ($self->id, $user->id)); - } - return $self->{tags}; + WHERE bug_id = ? AND user_id = ?', undef, ($self->id, $user->id) + ); + } + return $self->{tags}; } ##################################################################### @@ -3271,30 +3394,30 @@ sub tags { # These are accessors that don't need to access the database. # Keep them in alphabetical order. -sub bug_file_loc { return $_[0]->{bug_file_loc} } -sub bug_id { return $_[0]->{bug_id} } -sub bug_severity { return $_[0]->{bug_severity} } -sub bug_status { return $_[0]->{bug_status} } -sub cclist_accessible { return $_[0]->{cclist_accessible} } -sub component_id { return $_[0]->{component_id} } -sub creation_ts { return $_[0]->{creation_ts} } -sub estimated_time { return $_[0]->{estimated_time} } -sub deadline { return $_[0]->{deadline} } -sub delta_ts { return $_[0]->{delta_ts} } -sub error { return $_[0]->{error} } -sub everconfirmed { return $_[0]->{everconfirmed} } -sub lastdiffed { return $_[0]->{lastdiffed} } -sub op_sys { return $_[0]->{op_sys} } -sub priority { return $_[0]->{priority} } -sub product_id { return $_[0]->{product_id} } -sub remaining_time { return $_[0]->{remaining_time} } +sub bug_file_loc { return $_[0]->{bug_file_loc} } +sub bug_id { return $_[0]->{bug_id} } +sub bug_severity { return $_[0]->{bug_severity} } +sub bug_status { return $_[0]->{bug_status} } +sub cclist_accessible { return $_[0]->{cclist_accessible} } +sub component_id { return $_[0]->{component_id} } +sub creation_ts { return $_[0]->{creation_ts} } +sub estimated_time { return $_[0]->{estimated_time} } +sub deadline { return $_[0]->{deadline} } +sub delta_ts { return $_[0]->{delta_ts} } +sub error { return $_[0]->{error} } +sub everconfirmed { return $_[0]->{everconfirmed} } +sub lastdiffed { return $_[0]->{lastdiffed} } +sub op_sys { return $_[0]->{op_sys} } +sub priority { return $_[0]->{priority} } +sub product_id { return $_[0]->{product_id} } +sub remaining_time { return $_[0]->{remaining_time} } sub reporter_accessible { return $_[0]->{reporter_accessible} } -sub rep_platform { return $_[0]->{rep_platform} } -sub resolution { return $_[0]->{resolution} } -sub short_desc { return $_[0]->{short_desc} } -sub status_whiteboard { return $_[0]->{status_whiteboard} } -sub target_milestone { return $_[0]->{target_milestone} } -sub version { return $_[0]->{version} } +sub rep_platform { return $_[0]->{rep_platform} } +sub resolution { return $_[0]->{resolution} } +sub short_desc { return $_[0]->{short_desc} } +sub status_whiteboard { return $_[0]->{status_whiteboard} } +sub target_milestone { return $_[0]->{target_milestone} } +sub version { return $_[0]->{version} } ##################################################################### # Complex Accessors @@ -3313,674 +3436,715 @@ sub version { return $_[0]->{version} } # security holes. sub dup_id { - my ($self) = @_; - return $self->{'dup_id'} if exists $self->{'dup_id'}; + my ($self) = @_; + return $self->{'dup_id'} if exists $self->{'dup_id'}; - $self->{'dup_id'} = undef; - return if $self->{'error'}; + $self->{'dup_id'} = undef; + return if $self->{'error'}; - if ($self->{'resolution'} eq 'DUPLICATE') { - my $dbh = Bugzilla->dbh; - $self->{'dup_id'} = - $dbh->selectrow_array(q{SELECT dupe_of + if ($self->{'resolution'} eq 'DUPLICATE') { + my $dbh = Bugzilla->dbh; + $self->{'dup_id'} = $dbh->selectrow_array( + q{SELECT dupe_of FROM duplicates - WHERE dupe = ?}, - undef, - $self->{'bug_id'}); - } - return $self->{'dup_id'}; + WHERE dupe = ?}, undef, $self->{'bug_id'} + ); + } + return $self->{'dup_id'}; } sub _resolve_ultimate_dup_id { - my ($bug_id, $dupe_of, $loops_are_an_error) = @_; - my $dbh = Bugzilla->dbh; - my $sth = $dbh->prepare('SELECT dupe_of FROM duplicates WHERE dupe = ?'); - - my $this_dup = $dupe_of || $dbh->selectrow_array($sth, undef, $bug_id); - my $last_dup = $bug_id; - - my %dupes; - while ($this_dup) { - if ($this_dup == $bug_id) { - if ($loops_are_an_error) { - ThrowUserError('dupe_loop_detected', { bug_id => $bug_id, - dupe_of => $dupe_of }); - } - else { - return $last_dup; - } - } - # If $dupes{$this_dup} is already set to 1, then a loop - # already exists which does not involve this bug. - # As the user is not responsible for this loop, do not - # prevent them from marking this bug as a duplicate. - return $last_dup if exists $dupes{$this_dup}; - $dupes{$this_dup} = 1; - $last_dup = $this_dup; - $this_dup = $dbh->selectrow_array($sth, undef, $this_dup); + my ($bug_id, $dupe_of, $loops_are_an_error) = @_; + my $dbh = Bugzilla->dbh; + my $sth = $dbh->prepare('SELECT dupe_of FROM duplicates WHERE dupe = ?'); + + my $this_dup = $dupe_of || $dbh->selectrow_array($sth, undef, $bug_id); + my $last_dup = $bug_id; + + my %dupes; + while ($this_dup) { + if ($this_dup == $bug_id) { + if ($loops_are_an_error) { + ThrowUserError('dupe_loop_detected', {bug_id => $bug_id, dupe_of => $dupe_of}); + } + else { + return $last_dup; + } } - return $last_dup; + # If $dupes{$this_dup} is already set to 1, then a loop + # already exists which does not involve this bug. + # As the user is not responsible for this loop, do not + # prevent them from marking this bug as a duplicate. + return $last_dup if exists $dupes{$this_dup}; + $dupes{$this_dup} = 1; + $last_dup = $this_dup; + $this_dup = $dbh->selectrow_array($sth, undef, $this_dup); + } + + return $last_dup; } sub actual_time { - my ($self) = @_; - return $self->{'actual_time'} if exists $self->{'actual_time'}; + my ($self) = @_; + return $self->{'actual_time'} if exists $self->{'actual_time'}; - if ( $self->{'error'} || !Bugzilla->user->is_timetracker ) { - $self->{'actual_time'} = undef; - return $self->{'actual_time'}; - } + if ($self->{'error'} || !Bugzilla->user->is_timetracker) { + $self->{'actual_time'} = undef; + return $self->{'actual_time'}; + } - my $sth = Bugzilla->dbh->prepare("SELECT SUM(work_time) + my $sth = Bugzilla->dbh->prepare( + "SELECT SUM(work_time) FROM longdescs - WHERE longdescs.bug_id=?"); - $sth->execute($self->{bug_id}); - $self->{'actual_time'} = $sth->fetchrow_array(); - return $self->{'actual_time'}; + WHERE longdescs.bug_id=?" + ); + $sth->execute($self->{bug_id}); + $self->{'actual_time'} = $sth->fetchrow_array(); + return $self->{'actual_time'}; } sub alias { - my ($self) = @_; - return $self->{'alias'} if exists $self->{'alias'}; - return [] if $self->{'error'}; + my ($self) = @_; + return $self->{'alias'} if exists $self->{'alias'}; + return [] if $self->{'error'}; - my $dbh = Bugzilla->dbh; - $self->{'alias'} = $dbh->selectcol_arrayref( - q{SELECT alias FROM bugs_aliases WHERE bug_id = ? ORDER BY alias}, - undef, $self->bug_id); + my $dbh = Bugzilla->dbh; + $self->{'alias'} + = $dbh->selectcol_arrayref( + q{SELECT alias FROM bugs_aliases WHERE bug_id = ? ORDER BY alias}, + undef, $self->bug_id); - return $self->{'alias'}; + return $self->{'alias'}; } sub any_flags_requesteeble { - my ($self) = @_; - return $self->{'any_flags_requesteeble'} - if exists $self->{'any_flags_requesteeble'}; - return 0 if $self->{'error'}; + my ($self) = @_; + return $self->{'any_flags_requesteeble'} + if exists $self->{'any_flags_requesteeble'}; + return 0 if $self->{'error'}; + + my $any_flags_requesteeble + = grep { $_->is_requestable && $_->is_requesteeble } @{$self->flag_types}; - my $any_flags_requesteeble = - grep { $_->is_requestable && $_->is_requesteeble } @{$self->flag_types}; - # Useful in case a flagtype is no longer requestable but a requestee - # has been set before we turned off that bit. - $any_flags_requesteeble ||= grep { $_->requestee_id } @{$self->flags}; - $self->{'any_flags_requesteeble'} = $any_flags_requesteeble; + # Useful in case a flagtype is no longer requestable but a requestee + # has been set before we turned off that bit. + $any_flags_requesteeble ||= grep { $_->requestee_id } @{$self->flags}; + $self->{'any_flags_requesteeble'} = $any_flags_requesteeble; - return $self->{'any_flags_requesteeble'}; + return $self->{'any_flags_requesteeble'}; } sub attachments { - my ($self) = @_; - return $self->{'attachments'} if exists $self->{'attachments'}; - return [] if $self->{'error'}; + my ($self) = @_; + return $self->{'attachments'} if exists $self->{'attachments'}; + return [] if $self->{'error'}; - $self->{'attachments'} = - Bugzilla::Attachment->get_attachments_by_bug($self, {preload => 1}); - $_->object_cache_set() foreach @{ $self->{'attachments'} }; - return $self->{'attachments'}; + $self->{'attachments'} + = Bugzilla::Attachment->get_attachments_by_bug($self, {preload => 1}); + $_->object_cache_set() foreach @{$self->{'attachments'}}; + return $self->{'attachments'}; } sub assigned_to { - my ($self) = @_; - return $self->{'assigned_to_obj'} if exists $self->{'assigned_to_obj'}; - $self->{'assigned_to'} = 0 if $self->{'error'}; - $self->{'assigned_to_obj'} ||= new Bugzilla::User({ id => $self->{'assigned_to'}, cache => 1 }); - return $self->{'assigned_to_obj'}; + my ($self) = @_; + return $self->{'assigned_to_obj'} if exists $self->{'assigned_to_obj'}; + $self->{'assigned_to'} = 0 if $self->{'error'}; + $self->{'assigned_to_obj'} + ||= new Bugzilla::User({id => $self->{'assigned_to'}, cache => 1}); + return $self->{'assigned_to_obj'}; } sub blocked { - my ($self) = @_; - return $self->{'blocked'} if exists $self->{'blocked'}; - return [] if $self->{'error'}; - $self->{'blocked'} = EmitDependList("dependson", "blocked", $self->bug_id); - return $self->{'blocked'}; + my ($self) = @_; + return $self->{'blocked'} if exists $self->{'blocked'}; + return [] if $self->{'error'}; + $self->{'blocked'} = EmitDependList("dependson", "blocked", $self->bug_id); + return $self->{'blocked'}; } sub blocks_obj { - my ($self) = @_; - $self->{blocks_obj} ||= $self->_bugs_in_order($self->blocked); - return $self->{blocks_obj}; + my ($self) = @_; + $self->{blocks_obj} ||= $self->_bugs_in_order($self->blocked); + return $self->{blocks_obj}; } sub bug_group { - my ($self) = @_; - return join(', ', (map { $_->name } @{$self->groups_in})); + my ($self) = @_; + return join(', ', (map { $_->name } @{$self->groups_in})); } sub related_bugs { - my ($self, $relationship) = @_; - return [] if $self->{'error'}; + my ($self, $relationship) = @_; + return [] if $self->{'error'}; - my $field_name = $relationship->name; - $self->{'related_bugs'}->{$field_name} ||= $self->match({$field_name => $self->id}); - return $self->{'related_bugs'}->{$field_name}; + my $field_name = $relationship->name; + $self->{'related_bugs'}->{$field_name} + ||= $self->match({$field_name => $self->id}); + return $self->{'related_bugs'}->{$field_name}; } sub cc { - my ($self) = @_; - return $self->{'cc'} if exists $self->{'cc'}; - return [] if $self->{'error'}; + my ($self) = @_; + return $self->{'cc'} if exists $self->{'cc'}; + return [] if $self->{'error'}; - my $dbh = Bugzilla->dbh; - $self->{'cc'} = $dbh->selectcol_arrayref( - q{SELECT profiles.login_name FROM cc, profiles + my $dbh = Bugzilla->dbh; + $self->{'cc'} = $dbh->selectcol_arrayref( + q{SELECT profiles.login_name FROM cc, profiles WHERE bug_id = ? AND cc.who = profiles.userid - ORDER BY profiles.login_name}, - undef, $self->bug_id); + ORDER BY profiles.login_name}, undef, $self->bug_id + ); - return $self->{'cc'}; + return $self->{'cc'}; } # XXX Eventually this will become the standard "cc" method used everywhere. sub cc_users { - my $self = shift; - return $self->{'cc_users'} if exists $self->{'cc_users'}; - return [] if $self->{'error'}; - - my $dbh = Bugzilla->dbh; - my $cc_ids = $dbh->selectcol_arrayref( - 'SELECT who FROM cc WHERE bug_id = ?', undef, $self->id); - $self->{'cc_users'} = Bugzilla::User->new_from_list($cc_ids); - return $self->{'cc_users'}; + my $self = shift; + return $self->{'cc_users'} if exists $self->{'cc_users'}; + return [] if $self->{'error'}; + + my $dbh = Bugzilla->dbh; + my $cc_ids = $dbh->selectcol_arrayref('SELECT who FROM cc WHERE bug_id = ?', + undef, $self->id); + $self->{'cc_users'} = Bugzilla::User->new_from_list($cc_ids); + return $self->{'cc_users'}; } sub component { - my ($self) = @_; - return '' if $self->{error}; - $self->{component} //= $self->component_obj->name; - return $self->{component}; + my ($self) = @_; + return '' if $self->{error}; + $self->{component} //= $self->component_obj->name; + return $self->{component}; } # XXX Eventually this will replace component() sub component_obj { - my ($self) = @_; - return $self->{component_obj} if defined $self->{component_obj}; - return {} if $self->{error}; - $self->{component_obj} = - new Bugzilla::Component({ id => $self->{component_id}, cache => 1 }); - return $self->{component_obj}; + my ($self) = @_; + return $self->{component_obj} if defined $self->{component_obj}; + return {} if $self->{error}; + $self->{component_obj} + = new Bugzilla::Component({id => $self->{component_id}, cache => 1}); + return $self->{component_obj}; } sub classification_id { - my ($self) = @_; - return 0 if $self->{error}; - $self->{classification_id} //= $self->product_obj->classification_id; - return $self->{classification_id}; + my ($self) = @_; + return 0 if $self->{error}; + $self->{classification_id} //= $self->product_obj->classification_id; + return $self->{classification_id}; } sub classification { - my ($self) = @_; - return '' if $self->{error}; - $self->{classification} //= $self->product_obj->classification->name; - return $self->{classification}; + my ($self) = @_; + return '' if $self->{error}; + $self->{classification} //= $self->product_obj->classification->name; + return $self->{classification}; } sub default_bug_status { - my $class = shift; - # XXX This should just call new_bug_statuses when the UI accepts closed - # bug statuses instead of accepting them as a parameter. - my @statuses = @_; - - my $status; - if (scalar(@statuses) == 1) { - $status = $statuses[0]->name; - } - else { - $status = ($statuses[0]->name ne 'UNCONFIRMED') - ? $statuses[0]->name : $statuses[1]->name; - } + my $class = shift; - return $status; + # XXX This should just call new_bug_statuses when the UI accepts closed + # bug statuses instead of accepting them as a parameter. + my @statuses = @_; + + my $status; + if (scalar(@statuses) == 1) { + $status = $statuses[0]->name; + } + else { + $status + = ($statuses[0]->name ne 'UNCONFIRMED') + ? $statuses[0]->name + : $statuses[1]->name; + } + + return $status; } sub dependson { - my ($self) = @_; - return $self->{'dependson'} if exists $self->{'dependson'}; - return [] if $self->{'error'}; - $self->{'dependson'} = - EmitDependList("blocked", "dependson", $self->bug_id); - return $self->{'dependson'}; + my ($self) = @_; + return $self->{'dependson'} if exists $self->{'dependson'}; + return [] if $self->{'error'}; + $self->{'dependson'} = EmitDependList("blocked", "dependson", $self->bug_id); + return $self->{'dependson'}; } sub depends_on_obj { - my ($self) = @_; - $self->{depends_on_obj} ||= $self->_bugs_in_order($self->dependson); - return $self->{depends_on_obj}; + my ($self) = @_; + $self->{depends_on_obj} ||= $self->_bugs_in_order($self->dependson); + return $self->{depends_on_obj}; } sub duplicates { - my $self = shift; - return $self->{duplicates} if exists $self->{duplicates}; - return [] if $self->{error}; - $self->{duplicates} = Bugzilla::Bug->new_from_list($self->duplicate_ids); - return $self->{duplicates}; + my $self = shift; + return $self->{duplicates} if exists $self->{duplicates}; + return [] if $self->{error}; + $self->{duplicates} = Bugzilla::Bug->new_from_list($self->duplicate_ids); + return $self->{duplicates}; } sub duplicate_ids { - my $self = shift; - return $self->{duplicate_ids} if exists $self->{duplicate_ids}; - return [] if $self->{error}; + my $self = shift; + return $self->{duplicate_ids} if exists $self->{duplicate_ids}; + return [] if $self->{error}; - my $dbh = Bugzilla->dbh; - $self->{duplicate_ids} = - $dbh->selectcol_arrayref('SELECT dupe FROM duplicates WHERE dupe_of = ?', - undef, $self->id); - return $self->{duplicate_ids}; + my $dbh = Bugzilla->dbh; + $self->{duplicate_ids} + = $dbh->selectcol_arrayref('SELECT dupe FROM duplicates WHERE dupe_of = ?', + undef, $self->id); + return $self->{duplicate_ids}; } sub flag_types { - my ($self) = @_; - return $self->{'flag_types'} if exists $self->{'flag_types'}; - return [] if $self->{'error'}; + my ($self) = @_; + return $self->{'flag_types'} if exists $self->{'flag_types'}; + return [] if $self->{'error'}; - my $vars = { target_type => 'bug', - product_id => $self->{product_id}, - component_id => $self->{component_id}, - bug_id => $self->bug_id }; + my $vars = { + target_type => 'bug', + product_id => $self->{product_id}, + component_id => $self->{component_id}, + bug_id => $self->bug_id + }; - $self->{'flag_types'} = Bugzilla::Flag->_flag_types($vars); - return $self->{'flag_types'}; + $self->{'flag_types'} = Bugzilla::Flag->_flag_types($vars); + return $self->{'flag_types'}; } sub flags { - my $self = shift; + my $self = shift; - # Don't cache it as it must be in sync with ->flag_types. - $self->{flags} = [map { @{$_->{flags}} } @{$self->flag_types}]; - return $self->{flags}; + # Don't cache it as it must be in sync with ->flag_types. + $self->{flags} = [map { @{$_->{flags}} } @{$self->flag_types}]; + return $self->{flags}; } sub isopened { - my $self = shift; - unless (exists $self->{isopened}) { - $self->{isopened} = is_open_state($self->{bug_status}) ? 1 : 0; - } - return $self->{isopened}; + my $self = shift; + unless (exists $self->{isopened}) { + $self->{isopened} = is_open_state($self->{bug_status}) ? 1 : 0; + } + return $self->{isopened}; } sub keywords { - my ($self) = @_; - return join(', ', (map { $_->name } @{$self->keyword_objects})); + my ($self) = @_; + return join(', ', (map { $_->name } @{$self->keyword_objects})); } # XXX At some point, this should probably replace the normal "keywords" sub. sub keyword_objects { - my $self = shift; - return $self->{'keyword_objects'} if defined $self->{'keyword_objects'}; - return [] if $self->{'error'}; + my $self = shift; + return $self->{'keyword_objects'} if defined $self->{'keyword_objects'}; + return [] if $self->{'error'}; - my $dbh = Bugzilla->dbh; - my $ids = $dbh->selectcol_arrayref( - "SELECT keywordid FROM keywords WHERE bug_id = ?", undef, $self->id); - $self->{'keyword_objects'} = Bugzilla::Keyword->new_from_list($ids); - return $self->{'keyword_objects'}; + my $dbh = Bugzilla->dbh; + my $ids + = $dbh->selectcol_arrayref("SELECT keywordid FROM keywords WHERE bug_id = ?", + undef, $self->id); + $self->{'keyword_objects'} = Bugzilla::Keyword->new_from_list($ids); + return $self->{'keyword_objects'}; } sub comments { - my ($self, $params) = @_; - return [] if $self->{'error'}; - $params ||= {}; - - if (!defined $self->{'comments'}) { - $self->{'comments'} = Bugzilla::Comment->match({ bug_id => $self->id }); - my $count = 0; - state $is_mysql = Bugzilla->dbh->isa('Bugzilla::DB::Mysql') ? 1 : 0; - foreach my $comment (@{ $self->{'comments'} }) { - $comment->{count} = $count++; - $comment->{bug} = $self; - # XXX - hack for MySQL. Convert [U+....] back into its Unicode - # equivalent for characters above U+FFFF as MySQL older than 5.5.3 - # cannot store them, see Bugzilla::Comment::_check_thetext(). - if ($is_mysql) { - # Perl 5.13.8 and older complain about non-characters. - no warnings 'utf8'; - $comment->{thetext} =~ s/\x{FDD0}\[U\+((?:[1-9A-F]|10)[0-9A-F]{4})\]\x{FDD1}/chr(hex $1)/eg - } - } - # Some bugs may have no comments when upgrading old installations. - Bugzilla::Comment->preload($self->{'comments'}) if $count; - } - my @comments = @{ $self->{'comments'} }; - - my $order = $params->{order} - || Bugzilla->user->setting('comment_sort_order'); - if ($order ne 'oldest_to_newest') { - @comments = reverse @comments; - if ($order eq 'newest_to_oldest_desc_first') { - unshift(@comments, pop @comments); - } - } - - if ($params->{after}) { - my $from = datetime_from($params->{after}); - @comments = grep { datetime_from($_->creation_ts) > $from } @comments; - } - if ($params->{to}) { - my $to = datetime_from($params->{to}); - @comments = grep { datetime_from($_->creation_ts) <= $to } @comments; - } - return \@comments; + my ($self, $params) = @_; + return [] if $self->{'error'}; + $params ||= {}; + + if (!defined $self->{'comments'}) { + $self->{'comments'} = Bugzilla::Comment->match({bug_id => $self->id}); + my $count = 0; + state $is_mysql = Bugzilla->dbh->isa('Bugzilla::DB::Mysql') ? 1 : 0; + foreach my $comment (@{$self->{'comments'}}) { + $comment->{count} = $count++; + $comment->{bug} = $self; + + # XXX - hack for MySQL. Convert [U+....] back into its Unicode + # equivalent for characters above U+FFFF as MySQL older than 5.5.3 + # cannot store them, see Bugzilla::Comment::_check_thetext(). + if ($is_mysql) { + + # Perl 5.13.8 and older complain about non-characters. + no warnings 'utf8'; + $comment->{thetext} + =~ s/\x{FDD0}\[U\+((?:[1-9A-F]|10)[0-9A-F]{4})\]\x{FDD1}/chr(hex $1)/eg; + } + } + + # Some bugs may have no comments when upgrading old installations. + Bugzilla::Comment->preload($self->{'comments'}) if $count; + } + my @comments = @{$self->{'comments'}}; + + my $order = $params->{order} || Bugzilla->user->setting('comment_sort_order'); + if ($order ne 'oldest_to_newest') { + @comments = reverse @comments; + if ($order eq 'newest_to_oldest_desc_first') { + unshift(@comments, pop @comments); + } + } + + if ($params->{after}) { + my $from = datetime_from($params->{after}); + @comments = grep { datetime_from($_->creation_ts) > $from } @comments; + } + if ($params->{to}) { + my $to = datetime_from($params->{to}); + @comments = grep { datetime_from($_->creation_ts) <= $to } @comments; + } + return \@comments; } sub new_bug_statuses { - my ($class, $product) = @_; - my $user = Bugzilla->user; + my ($class, $product) = @_; + my $user = Bugzilla->user; - # Construct the list of allowable statuses. - my @statuses = @{ Bugzilla::Bug->statuses_available($product) }; + # Construct the list of allowable statuses. + my @statuses = @{Bugzilla::Bug->statuses_available($product)}; - # If the user has no privs... - unless ($user->in_group('editbugs', $product->id) - || $user->in_group('canconfirm', $product->id)) - { - # ... use UNCONFIRMED if available, else use the first status of the list. - my ($unconfirmed) = grep { $_->name eq 'UNCONFIRMED' } @statuses; - - # Because of an apparent Perl bug, "$unconfirmed || $statuses[0]" doesn't - # work, so we're using an "?:" operator. See bug 603314 for details. - @statuses = ($unconfirmed ? $unconfirmed : $statuses[0]); - } + # If the user has no privs... + unless ($user->in_group('editbugs', $product->id) + || $user->in_group('canconfirm', $product->id)) + { + # ... use UNCONFIRMED if available, else use the first status of the list. + my ($unconfirmed) = grep { $_->name eq 'UNCONFIRMED' } @statuses; + + # Because of an apparent Perl bug, "$unconfirmed || $statuses[0]" doesn't + # work, so we're using an "?:" operator. See bug 603314 for details. + @statuses = ($unconfirmed ? $unconfirmed : $statuses[0]); + } - return \@statuses; + return \@statuses; } # This is needed by xt/search.t. sub percentage_complete { - my $self = shift; - return undef if $self->{'error'} || !Bugzilla->user->is_timetracker; - my $remaining = $self->remaining_time; - my $actual = $self->actual_time; - my $total = $remaining + $actual; - return undef if $total == 0; - # Search.pm truncates this value to an integer, so we want to as well, - # since this is mostly used in a test where its value needs to be - # identical to what the database will return. - return int(100 * ($actual / $total)); + my $self = shift; + return undef if $self->{'error'} || !Bugzilla->user->is_timetracker; + my $remaining = $self->remaining_time; + my $actual = $self->actual_time; + my $total = $remaining + $actual; + return undef if $total == 0; + + # Search.pm truncates this value to an integer, so we want to as well, + # since this is mostly used in a test where its value needs to be + # identical to what the database will return. + return int(100 * ($actual / $total)); } sub product { - my ($self) = @_; - return '' if $self->{error}; - $self->{product} //= $self->product_obj->name; - return $self->{product}; + my ($self) = @_; + return '' if $self->{error}; + $self->{product} //= $self->product_obj->name; + return $self->{product}; } # XXX This should eventually replace the "product" subroutine. sub product_obj { - my $self = shift; - return {} if $self->{error}; - $self->{product_obj} ||= - new Bugzilla::Product({ id => $self->{product_id}, cache => 1 }); - return $self->{product_obj}; + my $self = shift; + return {} if $self->{error}; + $self->{product_obj} + ||= new Bugzilla::Product({id => $self->{product_id}, cache => 1}); + return $self->{product_obj}; } sub qa_contact { - my ($self) = @_; - return $self->{'qa_contact_obj'} if exists $self->{'qa_contact_obj'}; - return undef if $self->{'error'}; + my ($self) = @_; + return $self->{'qa_contact_obj'} if exists $self->{'qa_contact_obj'}; + return undef if $self->{'error'}; - if (Bugzilla->params->{'useqacontact'} && $self->{'qa_contact'}) { - $self->{'qa_contact_obj'} = new Bugzilla::User({ id => $self->{'qa_contact'}, cache => 1 }); - } else { - $self->{'qa_contact_obj'} = undef; - } - return $self->{'qa_contact_obj'}; + if (Bugzilla->params->{'useqacontact'} && $self->{'qa_contact'}) { + $self->{'qa_contact_obj'} + = new Bugzilla::User({id => $self->{'qa_contact'}, cache => 1}); + } + else { + $self->{'qa_contact_obj'} = undef; + } + return $self->{'qa_contact_obj'}; } sub reporter { - my ($self) = @_; - return $self->{'reporter'} if exists $self->{'reporter'}; - $self->{'reporter_id'} = 0 if $self->{'error'}; - $self->{'reporter'} = new Bugzilla::User({ id => $self->{'reporter_id'}, cache => 1 }); - return $self->{'reporter'}; + my ($self) = @_; + return $self->{'reporter'} if exists $self->{'reporter'}; + $self->{'reporter_id'} = 0 if $self->{'error'}; + $self->{'reporter'} + = new Bugzilla::User({id => $self->{'reporter_id'}, cache => 1}); + return $self->{'reporter'}; } sub see_also { - my ($self) = @_; - return [] if $self->{'error'}; - if (!exists $self->{see_also}) { - my $ids = Bugzilla->dbh->selectcol_arrayref( - 'SELECT id FROM bug_see_also WHERE bug_id = ?', - undef, $self->id); + my ($self) = @_; + return [] if $self->{'error'}; + if (!exists $self->{see_also}) { + my $ids + = Bugzilla->dbh->selectcol_arrayref( + 'SELECT id FROM bug_see_also WHERE bug_id = ?', + undef, $self->id); - my $bug_urls = Bugzilla::BugUrl->new_from_list($ids); + my $bug_urls = Bugzilla::BugUrl->new_from_list($ids); - $self->{see_also} = $bug_urls; - } - return $self->{see_also}; + $self->{see_also} = $bug_urls; + } + return $self->{see_also}; } sub status { - my $self = shift; - return undef if $self->{'error'}; + my $self = shift; + return undef if $self->{'error'}; - $self->{'status'} ||= new Bugzilla::Status({name => $self->{'bug_status'}}); - return $self->{'status'}; + $self->{'status'} ||= new Bugzilla::Status({name => $self->{'bug_status'}}); + return $self->{'status'}; } sub statuses_available { - my ($invocant, $product) = @_; + my ($invocant, $product) = @_; - my @statuses; + my @statuses; - if (ref $invocant) { - return [] if $invocant->{'error'}; + if (ref $invocant) { + return [] if $invocant->{'error'}; - return $invocant->{'statuses_available'} - if defined $invocant->{'statuses_available'}; + return $invocant->{'statuses_available'} + if defined $invocant->{'statuses_available'}; - @statuses = @{ $invocant->status->can_change_to }; - $product = $invocant->product_obj; - } else { - @statuses = @{ Bugzilla::Status->can_change_to }; - } + @statuses = @{$invocant->status->can_change_to}; + $product = $invocant->product_obj; + } + else { + @statuses = @{Bugzilla::Status->can_change_to}; + } - # UNCONFIRMED is only a valid status if it is enabled in this product. - if (!$product->allows_unconfirmed) { - @statuses = grep { $_->name ne 'UNCONFIRMED' } @statuses; - } + # UNCONFIRMED is only a valid status if it is enabled in this product. + if (!$product->allows_unconfirmed) { + @statuses = grep { $_->name ne 'UNCONFIRMED' } @statuses; + } - if (ref $invocant) { - my $available = $invocant->_refine_available_statuses(@statuses); - $invocant->{'statuses_available'} = $available; - return $available; - } + if (ref $invocant) { + my $available = $invocant->_refine_available_statuses(@statuses); + $invocant->{'statuses_available'} = $available; + return $available; + } - return \@statuses; + return \@statuses; } sub _refine_available_statuses { - my $self = shift; - my @statuses = @_; - - my @available; - foreach my $status (@statuses) { - # Make sure this is a legal status transition - next if !$self->check_can_change_field( - 'bug_status', $self->status->name, $status->name); - push(@available, $status); - } + my $self = shift; + my @statuses = @_; - # If this bug has an inactive status set, it should still be in the list. - if (!grep($_->name eq $self->status->name, @available)) { - unshift(@available, $self->status); - } - - return \@available; -} + my @available; + foreach my $status (@statuses) { -sub show_attachment_flags { - my ($self) = @_; - return $self->{'show_attachment_flags'} - if exists $self->{'show_attachment_flags'}; - return 0 if $self->{'error'}; + # Make sure this is a legal status transition + next + if !$self->check_can_change_field('bug_status', $self->status->name, + $status->name); + push(@available, $status); + } - # The number of types of flags that can be set on attachments to this bug - # and the number of flags on those attachments. One of these counts must be - # greater than zero in order for the "flags" column to appear in the table - # of attachments. - my $num_attachment_flag_types = Bugzilla::FlagType::count( - { 'target_type' => 'attachment', - 'product_id' => $self->{'product_id'}, - 'component_id' => $self->{'component_id'} }); - my $num_attachment_flags = Bugzilla::Flag->count( - { 'target_type' => 'attachment', - 'bug_id' => $self->bug_id }); + # If this bug has an inactive status set, it should still be in the list. + if (!grep($_->name eq $self->status->name, @available)) { + unshift(@available, $self->status); + } - $self->{'show_attachment_flags'} = - ($num_attachment_flag_types || $num_attachment_flags); + return \@available; +} - return $self->{'show_attachment_flags'}; +sub show_attachment_flags { + my ($self) = @_; + return $self->{'show_attachment_flags'} + if exists $self->{'show_attachment_flags'}; + return 0 if $self->{'error'}; + + # The number of types of flags that can be set on attachments to this bug + # and the number of flags on those attachments. One of these counts must be + # greater than zero in order for the "flags" column to appear in the table + # of attachments. + my $num_attachment_flag_types = Bugzilla::FlagType::count({ + 'target_type' => 'attachment', + 'product_id' => $self->{'product_id'}, + 'component_id' => $self->{'component_id'} + }); + my $num_attachment_flags + = Bugzilla::Flag->count({ + 'target_type' => 'attachment', 'bug_id' => $self->bug_id + }); + + $self->{'show_attachment_flags'} + = ($num_attachment_flag_types || $num_attachment_flags); + + return $self->{'show_attachment_flags'}; } sub groups { - my $self = shift; - return $self->{'groups'} if exists $self->{'groups'}; - return [] if $self->{'error'}; + my $self = shift; + return $self->{'groups'} if exists $self->{'groups'}; + return [] if $self->{'error'}; + + my $dbh = Bugzilla->dbh; + my @groups; + + # Some of this stuff needs to go into Bugzilla::User + + # For every group, we need to know if there is ANY bug_group_map + # record putting the current bug in that group and if there is ANY + # user_group_map record putting the user in that group. + # The LEFT JOINs are checking for record existence. + # + my $grouplist = Bugzilla->user->groups_as_string; + my $sth + = $dbh->prepare("SELECT DISTINCT groups.id, name, description," + . " CASE WHEN bug_group_map.group_id IS NOT NULL" + . " THEN 1 ELSE 0 END," + . " CASE WHEN groups.id IN($grouplist) THEN 1 ELSE 0 END," + . " isactive, membercontrol, othercontrol" + . " FROM groups" + . " LEFT JOIN bug_group_map" + . " ON bug_group_map.group_id = groups.id" + . " AND bug_id = ?" + . " LEFT JOIN group_control_map" + . " ON group_control_map.group_id = groups.id" + . " AND group_control_map.product_id = ? " + . " WHERE isbuggroup = 1" + . " ORDER BY description"); + $sth->execute($self->{'bug_id'}, $self->{'product_id'}); + + while ( + my ( + $groupid, $name, $description, $ison, + $ingroup, $isactive, $membercontrol, $othercontrol + ) + = $sth->fetchrow_array() + ) + { + + $membercontrol ||= 0; + + # For product groups, we only want to use the group if either + # (1) The bit is set and not required, or + # (2) The group is Shown or Default for members and + # the user is a member of the group. + if ( + $ison + || ( $isactive + && $ingroup + && ( ($membercontrol == CONTROLMAPDEFAULT) + || ($membercontrol == CONTROLMAPSHOWN))) + ) + { + my $ismandatory = $isactive && ($membercontrol == CONTROLMAPMANDATORY); - my $dbh = Bugzilla->dbh; - my @groups; - - # Some of this stuff needs to go into Bugzilla::User - - # For every group, we need to know if there is ANY bug_group_map - # record putting the current bug in that group and if there is ANY - # user_group_map record putting the user in that group. - # The LEFT JOINs are checking for record existence. - # - my $grouplist = Bugzilla->user->groups_as_string; - my $sth = $dbh->prepare( - "SELECT DISTINCT groups.id, name, description," . - " CASE WHEN bug_group_map.group_id IS NOT NULL" . - " THEN 1 ELSE 0 END," . - " CASE WHEN groups.id IN($grouplist) THEN 1 ELSE 0 END," . - " isactive, membercontrol, othercontrol" . - " FROM groups" . - " LEFT JOIN bug_group_map" . - " ON bug_group_map.group_id = groups.id" . - " AND bug_id = ?" . - " LEFT JOIN group_control_map" . - " ON group_control_map.group_id = groups.id" . - " AND group_control_map.product_id = ? " . - " WHERE isbuggroup = 1" . - " ORDER BY description"); - $sth->execute($self->{'bug_id'}, - $self->{'product_id'}); - - while (my ($groupid, $name, $description, $ison, $ingroup, $isactive, - $membercontrol, $othercontrol) = $sth->fetchrow_array()) { - - $membercontrol ||= 0; - - # For product groups, we only want to use the group if either - # (1) The bit is set and not required, or - # (2) The group is Shown or Default for members and - # the user is a member of the group. - if ($ison || - ($isactive && $ingroup - && (($membercontrol == CONTROLMAPDEFAULT) - || ($membercontrol == CONTROLMAPSHOWN)) - )) + push( + @groups, { - my $ismandatory = $isactive - && ($membercontrol == CONTROLMAPMANDATORY); - - push (@groups, { "bit" => $groupid, - "name" => $name, - "ison" => $ison, - "ingroup" => $ingroup, - "mandatory" => $ismandatory, - "description" => $description }); + "bit" => $groupid, + "name" => $name, + "ison" => $ison, + "ingroup" => $ingroup, + "mandatory" => $ismandatory, + "description" => $description } + ); } + } - $self->{'groups'} = \@groups; + $self->{'groups'} = \@groups; - return $self->{'groups'}; + return $self->{'groups'}; } sub groups_in { - my $self = shift; - return $self->{'groups_in'} if exists $self->{'groups_in'}; - return [] if $self->{'error'}; - my $group_ids = Bugzilla->dbh->selectcol_arrayref( - 'SELECT group_id FROM bug_group_map WHERE bug_id = ?', - undef, $self->id); - $self->{'groups_in'} = Bugzilla::Group->new_from_list($group_ids); - return $self->{'groups_in'}; + my $self = shift; + return $self->{'groups_in'} if exists $self->{'groups_in'}; + return [] if $self->{'error'}; + my $group_ids + = Bugzilla->dbh->selectcol_arrayref( + 'SELECT group_id FROM bug_group_map WHERE bug_id = ?', + undef, $self->id); + $self->{'groups_in'} = Bugzilla::Group->new_from_list($group_ids); + return $self->{'groups_in'}; } sub in_group { - my ($self, $group) = @_; - return grep($_->id == $group->id, @{$self->groups_in}) ? 1 : 0; + my ($self, $group) = @_; + return grep($_->id == $group->id, @{$self->groups_in}) ? 1 : 0; } sub user { - my $self = shift; - return $self->{'user'} if exists $self->{'user'}; - return {} if $self->{'error'}; - - my $user = Bugzilla->user; - my $prod_id = $self->{'product_id'}; - - my $editbugs = $user->in_group('editbugs', $prod_id); - my $is_reporter = $user->id == $self->{reporter_id} ? 1 : 0; - my $is_assignee = $user->id == $self->{'assigned_to'} ? 1 : 0; - my $is_qa_contact = Bugzilla->params->{'useqacontact'} - && $self->{'qa_contact'} - && $user->id == $self->{'qa_contact'} ? 1 : 0; - - my $canedit = $editbugs || $is_assignee || $is_qa_contact; - my $canconfirm = $editbugs || $user->in_group('canconfirm', $prod_id); - my $has_any_role = $is_reporter || $is_assignee || $is_qa_contact; - - $self->{'user'} = {canconfirm => $canconfirm, - canedit => $canedit, - isreporter => $is_reporter, - has_any_role => $has_any_role}; - return $self->{'user'}; + my $self = shift; + return $self->{'user'} if exists $self->{'user'}; + return {} if $self->{'error'}; + + my $user = Bugzilla->user; + my $prod_id = $self->{'product_id'}; + + my $editbugs = $user->in_group('editbugs', $prod_id); + my $is_reporter = $user->id == $self->{reporter_id} ? 1 : 0; + my $is_assignee = $user->id == $self->{'assigned_to'} ? 1 : 0; + my $is_qa_contact + = Bugzilla->params->{'useqacontact'} + && $self->{'qa_contact'} + && $user->id == $self->{'qa_contact'} ? 1 : 0; + + my $canedit = $editbugs || $is_assignee || $is_qa_contact; + my $canconfirm = $editbugs || $user->in_group('canconfirm', $prod_id); + my $has_any_role = $is_reporter || $is_assignee || $is_qa_contact; + + $self->{'user'} = { + canconfirm => $canconfirm, + canedit => $canedit, + isreporter => $is_reporter, + has_any_role => $has_any_role + }; + return $self->{'user'}; } # This is intended to get values that can be selected by the user in the # UI. It should not be used for security or validation purposes. sub choices { - my $self = shift; - return $self->{'choices'} if exists $self->{'choices'}; - return {} if $self->{'error'}; - my $user = Bugzilla->user; - - my @products = @{ $user->get_enterable_products }; - # The current product is part of the popup, even if new bugs are no longer - # allowed for that product - if (!grep($_->name eq $self->product_obj->name, @products)) { - unshift(@products, $self->product_obj); - } - my %class_ids = map { $_->classification_id => 1 } @products; - my $classifications = - Bugzilla::Classification->new_from_list([keys %class_ids]); - - my %choices = ( - bug_status => $self->statuses_available, - classification => $classifications, - product => \@products, - component => $self->product_obj->components, - version => $self->product_obj->versions, - target_milestone => $self->product_obj->milestones, - ); - - my $resolution_field = new Bugzilla::Field({ name => 'resolution' }); - # Don't include the empty resolution in drop-downs. - my @resolutions = grep($_->name, @{ $resolution_field->legal_values }); - $choices{'resolution'} = \@resolutions; - - foreach my $key (keys %choices) { - my $value = $self->$key; - $choices{$key} = [grep { $_->is_active || $_->name eq $value } @{ $choices{$key} }]; - } - - $self->{'choices'} = \%choices; - return $self->{'choices'}; + my $self = shift; + return $self->{'choices'} if exists $self->{'choices'}; + return {} if $self->{'error'}; + my $user = Bugzilla->user; + + my @products = @{$user->get_enterable_products}; + + # The current product is part of the popup, even if new bugs are no longer + # allowed for that product + if (!grep($_->name eq $self->product_obj->name, @products)) { + unshift(@products, $self->product_obj); + } + my %class_ids = map { $_->classification_id => 1 } @products; + my $classifications + = Bugzilla::Classification->new_from_list([keys %class_ids]); + + my %choices = ( + bug_status => $self->statuses_available, + classification => $classifications, + product => \@products, + component => $self->product_obj->components, + version => $self->product_obj->versions, + target_milestone => $self->product_obj->milestones, + ); + + my $resolution_field = new Bugzilla::Field({name => 'resolution'}); + + # Don't include the empty resolution in drop-downs. + my @resolutions = grep($_->name, @{$resolution_field->legal_values}); + $choices{'resolution'} = \@resolutions; + + foreach my $key (keys %choices) { + my $value = $self->$key; + $choices{$key} + = [grep { $_->is_active || $_->name eq $value } @{$choices{$key}}]; + } + + $self->{'choices'} = \%choices; + return $self->{'choices'}; } # Convenience Function. If you need speed, use this. If you need @@ -3989,11 +4153,11 @@ sub choices { # Queries the database for the bug with a given alias, and returns # the ID of the bug if it exists or the undefined value if it doesn't. sub bug_alias_to_id { - my ($alias) = @_; - my $dbh = Bugzilla->dbh; - trick_taint($alias); - return $dbh->selectrow_array( - "SELECT bug_id FROM bugs_aliases WHERE alias = ?", undef, $alias); + my ($alias) = @_; + my $dbh = Bugzilla->dbh; + trick_taint($alias); + return $dbh->selectrow_array("SELECT bug_id FROM bugs_aliases WHERE alias = ?", + undef, $alias); } ##################################################################### @@ -4003,21 +4167,26 @@ sub bug_alias_to_id { # Returns a list of currently active and editable bug fields, # including multi-select fields. sub editable_bug_fields { - my @fields = Bugzilla->dbh->bz_table_columns('bugs'); - # Add multi-select fields - push(@fields, map { $_->name } @{Bugzilla->fields({obsolete => 0, - type => FIELD_TYPE_MULTI_SELECT})}); - # Obsolete custom fields are not editable. - my @obsolete_fields = @{ Bugzilla->fields({obsolete => 1, custom => 1}) }; - @obsolete_fields = map { $_->name } @obsolete_fields; - foreach my $remove ("bug_id", "reporter", "creation_ts", "delta_ts", - "lastdiffed", @obsolete_fields) - { - my $location = firstidx { $_ eq $remove } @fields; - # Ensure field exists before attempting to remove it. - splice(@fields, $location, 1) if ($location > -1); - } - return @fields; + my @fields = Bugzilla->dbh->bz_table_columns('bugs'); + + # Add multi-select fields + push(@fields, + map { $_->name } + @{Bugzilla->fields({obsolete => 0, type => FIELD_TYPE_MULTI_SELECT})}); + + # Obsolete custom fields are not editable. + my @obsolete_fields = @{Bugzilla->fields({obsolete => 1, custom => 1})}; + @obsolete_fields = map { $_->name } @obsolete_fields; + foreach + my $remove ("bug_id", "reporter", "creation_ts", "delta_ts", "lastdiffed", + @obsolete_fields) + { + my $location = firstidx { $_ eq $remove } @fields; + + # Ensure field exists before attempting to remove it. + splice(@fields, $location, 1) if ($location > -1); + } + return @fields; } # XXX - When Bug::update() will be implemented, we should make this routine @@ -4025,103 +4194,107 @@ 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 ($my_field, $target_field, $bug_id, $exclude_resolved) = @_; - my $cache = Bugzilla->request_cache->{bug_dependency_list} ||= {}; + my ($my_field, $target_field, $bug_id, $exclude_resolved) = @_; + my $cache = Bugzilla->request_cache->{bug_dependency_list} ||= {}; - my $dbh = Bugzilla->dbh; - $exclude_resolved = $exclude_resolved ? 1 : 0; - my $is_open_clause = $exclude_resolved ? 'AND is_open = 1' : ''; + my $dbh = Bugzilla->dbh; + $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 + $cache->{"${target_field}_sth_$exclude_resolved"} ||= $dbh->prepare( + "SELECT $target_field FROM dependencies INNER JOIN bugs ON dependencies.$target_field = bugs.bug_id INNER JOIN bug_status ON bugs.bug_status = bug_status.value WHERE $my_field = ? $is_open_clause - ORDER BY is_open DESC, $target_field"); + ORDER BY is_open DESC, $target_field" + ); - return $dbh->selectcol_arrayref( - $cache->{"${target_field}_sth_$exclude_resolved"}, - undef, $bug_id); + 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. sub _bugs_in_order { - my ($self, $bug_ids) = @_; - return [] unless @$bug_ids; + my ($self, $bug_ids) = @_; + return [] unless @$bug_ids; - my %bug_map; - my $dbh = Bugzilla->dbh; + my %bug_map; + my $dbh = Bugzilla->dbh; - # there's no need to load bugs from the database if they are already in the - # object-cache - my @missing_ids; - foreach my $bug_id (@$bug_ids) { - if (my $bug = Bugzilla::Bug->object_cache_get($bug_id)) { - $bug_map{$bug_id} = $bug; - } - else { - push @missing_ids, $bug_id; - } + # there's no need to load bugs from the database if they are already in the + # object-cache + my @missing_ids; + foreach my $bug_id (@$bug_ids) { + if (my $bug = Bugzilla::Bug->object_cache_get($bug_id)) { + $bug_map{$bug_id} = $bug; } - if (@missing_ids) { - my $bugs = Bugzilla::Bug->new_from_list(\@missing_ids); - $bug_map{$_->id} = $_ foreach @$bugs; + else { + push @missing_ids, $bug_id; } + } + if (@missing_ids) { + my $bugs = Bugzilla::Bug->new_from_list(\@missing_ids); + $bug_map{$_->id} = $_ foreach @$bugs; + } - # Dependencies are often displayed using their aliases instead of their - # bug ID. Load them all at once. - my $rows = $dbh->selectall_arrayref( - 'SELECT bug_id, alias FROM bugs_aliases WHERE ' . - $dbh->sql_in('bug_id', $bug_ids) . ' ORDER BY alias'); + # Dependencies are often displayed using their aliases instead of their + # bug ID. Load them all at once. + my $rows + = $dbh->selectall_arrayref('SELECT bug_id, alias FROM bugs_aliases WHERE ' + . $dbh->sql_in('bug_id', $bug_ids) + . ' ORDER BY alias'); - foreach my $row (@$rows) { - my ($bug_id, $alias) = @$row; - $bug_map{$bug_id}->{alias} ||= []; - push @{ $bug_map{$bug_id}->{alias} }, $alias; - } - # Make sure all bugs have their alias attribute set. - $bug_map{$_}->{alias} ||= [] foreach @$bug_ids; + foreach my $row (@$rows) { + my ($bug_id, $alias) = @$row; + $bug_map{$bug_id}->{alias} ||= []; + push @{$bug_map{$bug_id}->{alias}}, $alias; + } - return [ map { $bug_map{$_} } @$bug_ids ]; + # Make sure all bugs have their alias attribute set. + $bug_map{$_}->{alias} ||= [] foreach @$bug_ids; + + return [map { $bug_map{$_} } @$bug_ids]; } # Get the activity of a bug, starting from $starttime (if given). # This routine assumes Bugzilla::Bug->check has been previously called. sub get_activity { - my ($self, $attach_id, $starttime, $include_comment_tags) = @_; - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; - - # Arguments passed to the SQL query. - my @args = ($self->id); - - # Only consider changes since $starttime, if given. - my $datepart = ""; - if (defined $starttime) { - trick_taint($starttime); - push (@args, $starttime); - $datepart = "AND bug_when > ?"; - } - - my $attachpart = ""; - if ($attach_id) { - push(@args, $attach_id); - $attachpart = "AND bugs_activity.attach_id = ?"; - } - - # Only includes attachments the user is allowed to see. - my $suppjoins = ""; - my $suppwhere = ""; - if (!$user->is_insider) { - $suppjoins = "LEFT JOIN attachments + my ($self, $attach_id, $starttime, $include_comment_tags) = @_; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + + # Arguments passed to the SQL query. + my @args = ($self->id); + + # Only consider changes since $starttime, if given. + my $datepart = ""; + if (defined $starttime) { + trick_taint($starttime); + push(@args, $starttime); + $datepart = "AND bug_when > ?"; + } + + my $attachpart = ""; + if ($attach_id) { + push(@args, $attach_id); + $attachpart = "AND bugs_activity.attach_id = ?"; + } + + # Only includes attachments the user is allowed to see. + my $suppjoins = ""; + my $suppwhere = ""; + if (!$user->is_insider) { + $suppjoins = "LEFT JOIN attachments ON attachments.attach_id = bugs_activity.attach_id"; - $suppwhere = "AND COALESCE(attachments.isprivate, 0) = 0"; - } + $suppwhere = "AND COALESCE(attachments.isprivate, 0) = 0"; + } - my $query = "SELECT fielddefs.name, bugs_activity.attach_id, " . - $dbh->sql_date_format('bugs_activity.bug_when', '%Y.%m.%d %H:%i:%s') . - " AS bug_when, bugs_activity.removed, bugs_activity.added, profiles.login_name, + my $query + = "SELECT fielddefs.name, bugs_activity.attach_id, " + . $dbh->sql_date_format('bugs_activity.bug_when', '%Y.%m.%d %H:%i:%s') + . " AS bug_when, bugs_activity.removed, bugs_activity.added, profiles.login_name, bugs_activity.comment_id FROM bugs_activity $suppjoins @@ -4134,24 +4307,26 @@ sub get_activity { $attachpart $suppwhere "; - if (Bugzilla->params->{'comment_taggers_group'} - && $include_comment_tags - && !$attach_id) - { - # Only includes comment tag activity for comments the user is allowed to see. - $suppjoins = ""; - $suppwhere = ""; - if (!Bugzilla->user->is_insider) { - $suppjoins = "INNER JOIN longdescs + if ( Bugzilla->params->{'comment_taggers_group'} + && $include_comment_tags + && !$attach_id) + { + # Only includes comment tag activity for comments the user is allowed to see. + $suppjoins = ""; + $suppwhere = ""; + if (!Bugzilla->user->is_insider) { + $suppjoins = "INNER JOIN longdescs ON longdescs.comment_id = longdescs_tags_activity.comment_id"; - $suppwhere = "AND longdescs.isprivate = 0"; - } + $suppwhere = "AND longdescs.isprivate = 0"; + } - $query .= " + $query .= " UNION ALL SELECT 'comment_tag' AS name, - NULL AS attach_id," . - $dbh->sql_date_format('longdescs_tags_activity.bug_when', '%Y.%m.%d %H:%i:%s') . " AS bug_when, + NULL AS attach_id," + . $dbh->sql_date_format('longdescs_tags_activity.bug_when', + '%Y.%m.%d %H:%i:%s') + . " AS bug_when, longdescs_tags_activity.removed, longdescs_tags_activity.added, profiles.login_name, @@ -4163,168 +4338,179 @@ sub get_activity { $datepart $suppwhere "; - push @args, $self->id; - push @args, $starttime if defined $starttime; - } + push @args, $self->id; + push @args, $starttime if defined $starttime; + } - $query .= "ORDER BY bug_when, comment_id"; + $query .= "ORDER BY bug_when, comment_id"; - my $list = $dbh->selectall_arrayref($query, undef, @args); + my $list = $dbh->selectall_arrayref($query, undef, @args); - my @operations; - my $operation = {}; - my $changes = []; - my $incomplete_data = 0; + my @operations; + my $operation = {}; + my $changes = []; + my $incomplete_data = 0; - foreach my $entry (@$list) { - my ($fieldname, $attachid, $when, $removed, $added, $who, $comment_id) = @$entry; - my %change; - my $activity_visible = 1; - - # check if the user should see this field's activity - if (grep { $fieldname eq $_ } TIMETRACKING_FIELDS) { - $activity_visible = $user->is_timetracker; - } - elsif ($fieldname eq 'longdescs.isprivate' - && !$user->is_insider && $added) - { - $activity_visible = 0; - } - else { - $activity_visible = 1; - } + foreach my $entry (@$list) { + my ($fieldname, $attachid, $when, $removed, $added, $who, $comment_id) + = @$entry; + my %change; + my $activity_visible = 1; - if ($activity_visible) { - # Check for the results of an old Bugzilla data corruption bug - if (($added eq '?' && $removed eq '?') - || ($added =~ /^\? / || $removed =~ /^\? /)) { - $incomplete_data = 1; - } - - # An operation, done by 'who' at time 'when', has a number of - # 'changes' associated with it. - # If this is the start of a new operation, store the data from the - # previous one, and set up the new one. - if ($operation->{'who'} - && ($who ne $operation->{'who'} - || $when ne $operation->{'when'})) - { - $operation->{'changes'} = $changes; - push (@operations, $operation); - - # Create new empty anonymous data structures. - $operation = {}; - $changes = []; - } - - # If this is the same field as the previous item, then concatenate - # the data into the same change. - 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); - } - $operation->{'who'} = $who; - $operation->{'when'} = $when; - $operation->{'fieldname'} = $change{'fieldname'} = $fieldname; - $operation->{'attachid'} = $change{'attachid'} = $attachid; - $change{'removed'} = $removed; - $change{'added'} = $added; - - if ($comment_id) { - $operation->{comment_id} = $change{'comment'} = Bugzilla::Comment->new($comment_id); - } - - push (@$changes, \%change); - } + # check if the user should see this field's activity + if (grep { $fieldname eq $_ } TIMETRACKING_FIELDS) { + $activity_visible = $user->is_timetracker; } - - if ($operation->{'who'}) { - $operation->{'changes'} = $changes; - push (@operations, $operation); + elsif ($fieldname eq 'longdescs.isprivate' && !$user->is_insider && $added) { + $activity_visible = 0; + } + else { + $activity_visible = 1; } - return(\@operations, $incomplete_data); + if ($activity_visible) { + + # Check for the results of an old Bugzilla data corruption bug + if ( ($added eq '?' && $removed eq '?') + || ($added =~ /^\? / || $removed =~ /^\? /)) + { + $incomplete_data = 1; + } + + # An operation, done by 'who' at time 'when', has a number of + # 'changes' associated with it. + # If this is the start of a new operation, store the data from the + # previous one, and set up the new one. + if ($operation->{'who'} + && ($who ne $operation->{'who'} || $when ne $operation->{'when'})) + { + $operation->{'changes'} = $changes; + push(@operations, $operation); + + # Create new empty anonymous data structures. + $operation = {}; + $changes = []; + } + + # If this is the same field as the previous item, then concatenate + # the data into the same change. + 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); + } + $operation->{'who'} = $who; + $operation->{'when'} = $when; + $operation->{'fieldname'} = $change{'fieldname'} = $fieldname; + $operation->{'attachid'} = $change{'attachid'} = $attachid; + $change{'removed'} = $removed; + $change{'added'} = $added; + + if ($comment_id) { + $operation->{comment_id} = $change{'comment'} + = Bugzilla::Comment->new($comment_id); + } + + push(@$changes, \%change); + } + } + + if ($operation->{'who'}) { + $operation->{'changes'} = $changes; + push(@operations, $operation); + } + + return (\@operations, $incomplete_data); } # Update the bugs_activity table to reflect changes made in bugs. sub LogActivityEntry { - my ($bug_id, $field, $removed, $added, $user_id, $timestamp, $comment_id, - $attach_id) = @_; - my $sth = Bugzilla->dbh->prepare_cached( - 'INSERT INTO bugs_activity + my ($bug_id, $field, $removed, $added, $user_id, $timestamp, $comment_id, + $attach_id) + = @_; + my $sth = Bugzilla->dbh->prepare_cached( + 'INSERT INTO bugs_activity (bug_id, who, bug_when, fieldid, removed, added, comment_id, attach_id) - VALUES (?, ?, ?, ?, ?, ?, ?, ?)'); - - # 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 - # space than the activity table allows. We'll solve this by splitting it - # into multiple entries if it's too long. - while ($removed || $added) { - my ($removestr, $addstr) = ($removed, $added); - if (length($removestr) > MAX_LINE_LENGTH) { - my $commaposition = find_wrap_point($removed, MAX_LINE_LENGTH); - $removestr = substr($removed, 0, $commaposition); - $removed = substr($removed, $commaposition); - } else { - $removed = ""; # no more entries - } - if (length($addstr) > MAX_LINE_LENGTH) { - my $commaposition = find_wrap_point($added, MAX_LINE_LENGTH); - $addstr = substr($added, 0, $commaposition); - $added = substr($added, $commaposition); - } else { - $added = ""; # no more entries - } - trick_taint($addstr); - trick_taint($removestr); - my $fieldid = get_field_id($field); - $sth->execute($bug_id, $user_id, $timestamp, $fieldid, $removestr, - $addstr, $comment_id, $attach_id); + VALUES (?, ?, ?, ?, ?, ?, ?, ?)' + ); + + # 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 + # space than the activity table allows. We'll solve this by splitting it + # into multiple entries if it's too long. + while ($removed || $added) { + my ($removestr, $addstr) = ($removed, $added); + if (length($removestr) > MAX_LINE_LENGTH) { + my $commaposition = find_wrap_point($removed, MAX_LINE_LENGTH); + $removestr = substr($removed, 0, $commaposition); + $removed = substr($removed, $commaposition); } + else { + $removed = ""; # no more entries + } + if (length($addstr) > MAX_LINE_LENGTH) { + my $commaposition = find_wrap_point($added, MAX_LINE_LENGTH); + $addstr = substr($added, 0, $commaposition); + $added = substr($added, $commaposition); + } + else { + $added = ""; # no more entries + } + trick_taint($addstr); + trick_taint($removestr); + my $fieldid = get_field_id($field); + $sth->execute( + $bug_id, $user_id, $timestamp, $fieldid, + $removestr, $addstr, $comment_id, $attach_id + ); + } } # Update bug_user_last_visit table sub update_user_last_visit { - my ($self, $user, $last_visit_ts) = @_; - my $lv = Bugzilla::BugUserLastVisit->match({ bug_id => $self->id, - user_id => $user->id })->[0]; - - if ($lv) { - $lv->set(last_visit_ts => $last_visit_ts); - $lv->update; - } - else { - Bugzilla::BugUserLastVisit->create({ bug_id => $self->id, - user_id => $user->id, - last_visit_ts => $last_visit_ts }); - } + my ($self, $user, $last_visit_ts) = @_; + my $lv + = Bugzilla::BugUserLastVisit->match({bug_id => $self->id, user_id => $user->id + })->[0]; + + if ($lv) { + $lv->set(last_visit_ts => $last_visit_ts); + $lv->update; + } + else { + Bugzilla::BugUserLastVisit->create({ + bug_id => $self->id, user_id => $user->id, last_visit_ts => $last_visit_ts + }); + } } # Convert WebService API and email_in.pl field names to internal DB field # names. sub map_fields { - my ($params, $except) = @_; - - my %field_values; - foreach my $field (keys %$params) { - # Don't allow setting private fields via email_in or the WebService. - next if $field =~ /^_/; - my $field_name; - if ($except->{$field}) { - $field_name = $field; - } - else { - $field_name = FIELD_MAP->{$field} || $field; - } - $field_values{$field_name} = $params->{$field}; + my ($params, $except) = @_; + + my %field_values; + foreach my $field (keys %$params) { + + # Don't allow setting private fields via email_in or the WebService. + next if $field =~ /^_/; + my $field_name; + if ($except->{$field}) { + $field_name = $field; + } + else { + $field_name = FIELD_MAP->{$field} || $field; } - return \%field_values; + $field_values{$field_name} = $params->{$field}; + } + return \%field_values; } ################################################################################ @@ -4344,164 +4530,187 @@ sub map_fields { # $PrivilegesRequired - return the reason of the failure, if any ################################################################################ sub check_can_change_field { - my $self = shift; - my ($field, $oldvalue, $newvalue, $PrivilegesRequired) = (@_); - my $user = Bugzilla->user; + my $self = shift; + my ($field, $oldvalue, $newvalue, $PrivilegesRequired) = (@_); + my $user = Bugzilla->user; + + $oldvalue = defined($oldvalue) ? $oldvalue : ''; + $newvalue = defined($newvalue) ? $newvalue : ''; + + # Return true if they haven't changed this field at all. + if ($oldvalue eq $newvalue) { + return 1; + } + elsif (ref($newvalue) eq 'ARRAY' && ref($oldvalue) eq 'ARRAY') { + my ($removed, $added) = diff_arrays($oldvalue, $newvalue); + return 1 if !scalar(@$removed) && !scalar(@$added); + } + elsif (trim($oldvalue) eq trim($newvalue)) { + return 1; - $oldvalue = defined($oldvalue) ? $oldvalue : ''; - $newvalue = defined($newvalue) ? $newvalue : ''; - - # Return true if they haven't changed this field at all. - if ($oldvalue eq $newvalue) { - return 1; - } elsif (ref($newvalue) eq 'ARRAY' && ref($oldvalue) eq 'ARRAY') { - my ($removed, $added) = diff_arrays($oldvalue, $newvalue); - return 1 if !scalar(@$removed) && !scalar(@$added); - } elsif (trim($oldvalue) eq trim($newvalue)) { - return 1; # numeric fields need to be compared using == - } elsif (($field eq 'estimated_time' || $field eq 'remaining_time' - || $field eq 'work_time') - && $oldvalue == $newvalue) + } + elsif ( + ( + $field eq 'estimated_time' + || $field eq 'remaining_time' + || $field eq 'work_time' + ) + && $oldvalue == $newvalue + ) + { + return 1; + } + + my @priv_results; + Bugzilla::Hook::process( + 'bug_check_can_change_field', { - return 1; - } - - my @priv_results; - Bugzilla::Hook::process('bug_check_can_change_field', - { bug => $self, field => $field, - new_value => $newvalue, old_value => $oldvalue, - priv_results => \@priv_results }); - if (my $priv_required = first { $_ > 0 } @priv_results) { - $$PrivilegesRequired = $priv_required; - return 0; - } - my $allow_found = first { $_ == 0 } @priv_results; - if (defined $allow_found) { - return 1; + bug => $self, + field => $field, + new_value => $newvalue, + old_value => $oldvalue, + priv_results => \@priv_results + } + ); + if (my $priv_required = first { $_ > 0 } @priv_results) { + $$PrivilegesRequired = $priv_required; + return 0; + } + my $allow_found = first { $_ == 0 } @priv_results; + if (defined $allow_found) { + return 1; + } + + # Allow anyone to change comments, or set flags + if ($field =~ /^longdesc/ || $field eq 'flagtypes.name') { + return 1; + } + +# If the user isn't allowed to change a field, we must tell them who can. +# We store the required permission set into the $PrivilegesRequired +# variable which gets passed to the error template. +# +# $PrivilegesRequired = PRIVILEGES_REQUIRED_NONE : no privileges required; +# $PrivilegesRequired = PRIVILEGES_REQUIRED_REPORTER : the reporter, assignee or an empowered user; +# $PrivilegesRequired = PRIVILEGES_REQUIRED_ASSIGNEE : the assignee or an empowered user; +# $PrivilegesRequired = PRIVILEGES_REQUIRED_EMPOWERED : an empowered user. + + # Only users in the time-tracking group can change time-tracking fields, + # including the deadline. + if (grep { $_ eq $field } (TIMETRACKING_FIELDS, 'deadline')) { + if (!$user->is_timetracker) { + $$PrivilegesRequired = PRIVILEGES_REQUIRED_EMPOWERED; + return 0; + } + } + + # Allow anyone with (product-specific) "editbugs" privs to change anything. + if ($user->in_group('editbugs', $self->{'product_id'})) { + return 1; + } + + # *Only* users with (product-specific) "canconfirm" privs can confirm bugs. + if ($self->_changes_everconfirmed($field, $oldvalue, $newvalue)) { + $$PrivilegesRequired = PRIVILEGES_REQUIRED_EMPOWERED; + return $user->in_group('canconfirm', $self->{'product_id'}); + } + + # Make sure that a valid bug ID has been given. + if (!$self->{'error'}) { + + # Allow the assignee to change anything else. + if ( $self->{'assigned_to'} == $user->id + || $self->{'_old_assigned_to'} && $self->{'_old_assigned_to'} == $user->id) + { + return 1; } - # Allow anyone to change comments, or set flags - if ($field =~ /^longdesc/ || $field eq 'flagtypes.name') { - return 1; + # Allow the QA contact to change anything else. + if ( + Bugzilla->params->{'useqacontact'} + && ( ($self->{'qa_contact'} && $self->{'qa_contact'} == $user->id) + || ($self->{'_old_qa_contact'} && $self->{'_old_qa_contact'} == $user->id)) + ) + { + return 1; } + } - # If the user isn't allowed to change a field, we must tell them who can. - # We store the required permission set into the $PrivilegesRequired - # variable which gets passed to the error template. - # - # $PrivilegesRequired = PRIVILEGES_REQUIRED_NONE : no privileges required; - # $PrivilegesRequired = PRIVILEGES_REQUIRED_REPORTER : the reporter, assignee or an empowered user; - # $PrivilegesRequired = PRIVILEGES_REQUIRED_ASSIGNEE : the assignee or an empowered user; - # $PrivilegesRequired = PRIVILEGES_REQUIRED_EMPOWERED : an empowered user. - - # Only users in the time-tracking group can change time-tracking fields, - # including the deadline. - if (grep { $_ eq $field } (TIMETRACKING_FIELDS, 'deadline')) { - if (!$user->is_timetracker) { - $$PrivilegesRequired = PRIVILEGES_REQUIRED_EMPOWERED; - return 0; - } - } + # At this point, the user is either the reporter or an + # unprivileged user. We first check for fields the reporter + # is not allowed to change. - # Allow anyone with (product-specific) "editbugs" privs to change anything. - if ($user->in_group('editbugs', $self->{'product_id'})) { - return 1; - } + # The reporter may not: + # - reassign bugs, unless the bugs are assigned to them; + # in that case we will have already returned 1 above + # when checking for the assignee of the bug. + if ($field eq 'assigned_to') { + $$PrivilegesRequired = PRIVILEGES_REQUIRED_ASSIGNEE; + return 0; + } - # *Only* users with (product-specific) "canconfirm" privs can confirm bugs. - if ($self->_changes_everconfirmed($field, $oldvalue, $newvalue)) { - $$PrivilegesRequired = PRIVILEGES_REQUIRED_EMPOWERED; - return $user->in_group('canconfirm', $self->{'product_id'}); - } + # - change the QA contact + if ($field eq 'qa_contact') { + $$PrivilegesRequired = PRIVILEGES_REQUIRED_ASSIGNEE; + return 0; + } - # Make sure that a valid bug ID has been given. - if (!$self->{'error'}) { - # Allow the assignee to change anything else. - if ($self->{'assigned_to'} == $user->id - || $self->{'_old_assigned_to'} && $self->{'_old_assigned_to'} == $user->id) - { - return 1; - } + # - change the target milestone + if ($field eq 'target_milestone') { + $$PrivilegesRequired = PRIVILEGES_REQUIRED_ASSIGNEE; + return 0; + } - # Allow the QA contact to change anything else. - if (Bugzilla->params->{'useqacontact'} - && (($self->{'qa_contact'} && $self->{'qa_contact'} == $user->id) - || ($self->{'_old_qa_contact'} && $self->{'_old_qa_contact'} == $user->id))) - { - return 1; - } - } + # - change the priority (unless they could have set it originally) + if ($field eq 'priority' && !Bugzilla->params->{'letsubmitterchoosepriority'}) { + $$PrivilegesRequired = PRIVILEGES_REQUIRED_ASSIGNEE; + return 0; + } - # At this point, the user is either the reporter or an - # unprivileged user. We first check for fields the reporter - # is not allowed to change. - - # The reporter may not: - # - reassign bugs, unless the bugs are assigned to them; - # in that case we will have already returned 1 above - # when checking for the assignee of the bug. - if ($field eq 'assigned_to') { - $$PrivilegesRequired = PRIVILEGES_REQUIRED_ASSIGNEE; - return 0; - } - # - change the QA contact - if ($field eq 'qa_contact') { - $$PrivilegesRequired = PRIVILEGES_REQUIRED_ASSIGNEE; - return 0; - } - # - change the target milestone - if ($field eq 'target_milestone') { - $$PrivilegesRequired = PRIVILEGES_REQUIRED_ASSIGNEE; - return 0; - } - # - change the priority (unless they could have set it originally) - if ($field eq 'priority' - && !Bugzilla->params->{'letsubmitterchoosepriority'}) - { - $$PrivilegesRequired = PRIVILEGES_REQUIRED_ASSIGNEE; - return 0; - } - # - unconfirm bugs (confirming them is handled above) - if ($field eq 'everconfirmed') { - $$PrivilegesRequired = PRIVILEGES_REQUIRED_ASSIGNEE; - return 0; - } - # - change the status from one open state to another - if ($field eq 'bug_status' - && is_open_state($oldvalue) && is_open_state($newvalue)) - { - $$PrivilegesRequired = PRIVILEGES_REQUIRED_ASSIGNEE; - return 0; - } + # - unconfirm bugs (confirming them is handled above) + if ($field eq 'everconfirmed') { + $$PrivilegesRequired = PRIVILEGES_REQUIRED_ASSIGNEE; + return 0; + } + + # - change the status from one open state to another + if ( $field eq 'bug_status' + && is_open_state($oldvalue) + && is_open_state($newvalue)) + { + $$PrivilegesRequired = PRIVILEGES_REQUIRED_ASSIGNEE; + return 0; + } - # The reporter is allowed to change anything else. - if (!$self->{'error'} && $self->{'reporter_id'} == $user->id) { - return 1; - } + # The reporter is allowed to change anything else. + if (!$self->{'error'} && $self->{'reporter_id'} == $user->id) { + return 1; + } - # If we haven't returned by this point, then the user doesn't - # have the necessary permissions to change this field. - $$PrivilegesRequired = PRIVILEGES_REQUIRED_REPORTER; - return 0; + # If we haven't returned by this point, then the user doesn't + # have the necessary permissions to change this field. + $$PrivilegesRequired = PRIVILEGES_REQUIRED_REPORTER; + return 0; } # A helper for check_can_change_field sub _changes_everconfirmed { - my ($self, $field, $old, $new) = @_; - return 1 if $field eq 'everconfirmed'; - if ($field eq 'bug_status') { - if ($self->everconfirmed) { - # Moving a confirmed bug to UNCONFIRMED will change everconfirmed. - return 1 if $new eq 'UNCONFIRMED'; - } - else { - # Moving an unconfirmed bug to an open state that isn't - # UNCONFIRMED will confirm the bug. - return 1 if (is_open_state($new) and $new ne 'UNCONFIRMED'); - } + my ($self, $field, $old, $new) = @_; + return 1 if $field eq 'everconfirmed'; + if ($field eq 'bug_status') { + if ($self->everconfirmed) { + + # Moving a confirmed bug to UNCONFIRMED will change everconfirmed. + return 1 if $new eq 'UNCONFIRMED'; } - return 0; + else { + # Moving an unconfirmed bug to an open state that isn't + # UNCONFIRMED will confirm the bug. + return 1 if (is_open_state($new) and $new ne 'UNCONFIRMED'); + } + } + return 0; } # @@ -4510,72 +4719,77 @@ sub _changes_everconfirmed { # Validate and return a hash of dependencies sub ValidateDependencies { - my $fields = {}; - # These can be arrayrefs or they can be strings. - $fields->{'dependson'} = shift; - $fields->{'blocked'} = shift; - my $id = shift || 0; - - unless (defined($fields->{'dependson'}) - || defined($fields->{'blocked'})) - { - return; - } - - my $dbh = Bugzilla->dbh; - my %deps; - my %deptree; - my %sth; - $sth{dependson} = $dbh->prepare('SELECT dependson FROM dependencies WHERE blocked = ?'); - $sth{blocked} = $dbh->prepare('SELECT blocked FROM dependencies WHERE dependson = ?'); - - foreach my $pair (["blocked", "dependson"], ["dependson", "blocked"]) { - my ($me, $target) = @{$pair}; - $deptree{$target} = []; - $deps{$target} = []; - next unless $fields->{$target}; - - my %seen; - my $target_array = ref($fields->{$target}) ? $fields->{$target} - : [split(/[\s,]+/, $fields->{$target})]; - foreach my $i (@$target_array) { - if ($id == $i) { - ThrowUserError("dependency_loop_single"); - } - if (!exists $seen{$i}) { - push(@{$deptree{$target}}, $i); - $seen{$i} = 1; - } - } - # populate $deps{$target} as first-level deps only. - # and find remainder of dependency tree in $deptree{$target} - @{$deps{$target}} = @{$deptree{$target}}; - my @stack = @{$deps{$target}}; - while (@stack) { - my $i = shift @stack; - my $dep_list = $dbh->selectcol_arrayref($sth{$target}, undef, $i); - foreach my $t (@$dep_list) { - # ignore any _current_ dependencies involving this bug, - # as they will be overwritten with data from the form. - if ($t != $id && !exists $seen{$t}) { - push(@{$deptree{$target}}, $t); - push @stack, $t; - $seen{$t} = 1; - } - } + my $fields = {}; + + # These can be arrayrefs or they can be strings. + $fields->{'dependson'} = shift; + $fields->{'blocked'} = shift; + my $id = shift || 0; + + unless (defined($fields->{'dependson'}) || defined($fields->{'blocked'})) { + return; + } + + my $dbh = Bugzilla->dbh; + my %deps; + my %deptree; + my %sth; + $sth{dependson} + = $dbh->prepare('SELECT dependson FROM dependencies WHERE blocked = ?'); + $sth{blocked} + = $dbh->prepare('SELECT blocked FROM dependencies WHERE dependson = ?'); + + foreach my $pair (["blocked", "dependson"], ["dependson", "blocked"]) { + my ($me, $target) = @{$pair}; + $deptree{$target} = []; + $deps{$target} = []; + next unless $fields->{$target}; + + my %seen; + my $target_array + = ref($fields->{$target}) + ? $fields->{$target} + : [split(/[\s,]+/, $fields->{$target})]; + foreach my $i (@$target_array) { + if ($id == $i) { + ThrowUserError("dependency_loop_single"); + } + if (!exists $seen{$i}) { + push(@{$deptree{$target}}, $i); + $seen{$i} = 1; + } + } + + # populate $deps{$target} as first-level deps only. + # and find remainder of dependency tree in $deptree{$target} + @{$deps{$target}} = @{$deptree{$target}}; + my @stack = @{$deps{$target}}; + while (@stack) { + my $i = shift @stack; + my $dep_list = $dbh->selectcol_arrayref($sth{$target}, undef, $i); + foreach my $t (@$dep_list) { + + # ignore any _current_ dependencies involving this bug, + # as they will be overwritten with data from the form. + if ($t != $id && !exists $seen{$t}) { + push(@{$deptree{$target}}, $t); + push @stack, $t; + $seen{$t} = 1; } + } } + } - my @deps = @{$deptree{'dependson'}}; - my @blocks = @{$deptree{'blocked'}}; - my %union = (); - my %isect = (); - foreach my $b (@deps, @blocks) { $union{$b}++ && $isect{$b}++ } - my @isect = keys %isect; - if (scalar(@isect) > 0) { - ThrowUserError("dependency_loop_multi", {'deps' => \@isect}); - } - return %deps; + my @deps = @{$deptree{'dependson'}}; + my @blocks = @{$deptree{'blocked'}}; + my %union = (); + my %isect = (); + foreach my $b (@deps, @blocks) { $union{$b}++ && $isect{$b}++ } + my @isect = keys %isect; + if (scalar(@isect) > 0) { + ThrowUserError("dependency_loop_multi", {'deps' => \@isect}); + } + return %deps; } @@ -4584,51 +4798,52 @@ sub ValidateDependencies { ##################################################################### sub _create_cf_accessors { - my ($invocant) = @_; - my $class = ref($invocant) || $invocant; - return if Bugzilla->request_cache->{"${class}_cf_accessors_created"}; - - my $fields = Bugzilla->fields({ custom => 1 }); - foreach my $field (@$fields) { - my $accessor = $class->_accessor_for($field); - my $name = "${class}::" . $field->name; - { - no strict 'refs'; - next if defined *{$name}; - *{$name} = $accessor; - } + my ($invocant) = @_; + my $class = ref($invocant) || $invocant; + return if Bugzilla->request_cache->{"${class}_cf_accessors_created"}; + + my $fields = Bugzilla->fields({custom => 1}); + foreach my $field (@$fields) { + my $accessor = $class->_accessor_for($field); + my $name = "${class}::" . $field->name; + { + no strict 'refs'; + next if defined *{$name}; + *{$name} = $accessor; } + } - Bugzilla->request_cache->{"${class}_cf_accessors_created"} = 1; + Bugzilla->request_cache->{"${class}_cf_accessors_created"} = 1; } sub _accessor_for { - my ($class, $field) = @_; - if ($field->type == FIELD_TYPE_MULTI_SELECT) { - return $class->_multi_select_accessor($field->name); - } - return $class->_cf_accessor($field->name); + my ($class, $field) = @_; + if ($field->type == FIELD_TYPE_MULTI_SELECT) { + return $class->_multi_select_accessor($field->name); + } + return $class->_cf_accessor($field->name); } sub _cf_accessor { - my ($class, $field) = @_; - my $accessor = sub { - my ($self) = @_; - return $self->{$field}; - }; - return $accessor; + my ($class, $field) = @_; + my $accessor = sub { + my ($self) = @_; + return $self->{$field}; + }; + return $accessor; } sub _multi_select_accessor { - my ($class, $field) = @_; - my $accessor = sub { - my ($self) = @_; - $self->{$field} ||= Bugzilla->dbh->selectcol_arrayref( - "SELECT value FROM bug_$field WHERE bug_id = ? ORDER BY value", - undef, $self->id); - return $self->{$field}; - }; - return $accessor; + my ($class, $field) = @_; + my $accessor = sub { + my ($self) = @_; + $self->{$field} + ||= Bugzilla->dbh->selectcol_arrayref( + "SELECT value FROM bug_$field WHERE bug_id = ? ORDER BY value", + undef, $self->id); + return $self->{$field}; + }; + return $accessor; } 1; diff --git a/Bugzilla/BugMail.pm b/Bugzilla/BugMail.pm index 110a1ffaf..90eed0d8e 100644 --- a/Bugzilla/BugMail.pm +++ b/Bugzilla/BugMail.pm @@ -27,16 +27,17 @@ use Scalar::Util qw(blessed); use List::MoreUtils qw(uniq); use Storable qw(dclone); -use constant BIT_DIRECT => 1; -use constant BIT_WATCHING => 2; +use constant BIT_DIRECT => 1; +use constant BIT_WATCHING => 2; sub relationships { - my $ref = RELATIONSHIPS; - # Clone it so that we don't modify the constant; - my %relationships = %$ref; - Bugzilla::Hook::process('bugmail_relationships', - { relationships => \%relationships }); - return %relationships; + my $ref = RELATIONSHIPS; + + # Clone it so that we don't modify the constant; + my %relationships = %$ref; + Bugzilla::Hook::process('bugmail_relationships', + {relationships => \%relationships}); + return %relationships; } # args: bug_id, and an optional hash ref which may have keys for: @@ -46,452 +47,482 @@ sub relationships { # All the names are email addresses, not userids # values are scalars, except for cc, which is a list sub Send { - my ($id, $forced, $params) = @_; - $params ||= {}; - - my $dbh = Bugzilla->dbh; - my $bug = new Bugzilla::Bug($id); - - my $start = $bug->lastdiffed; - my $end = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); - - # Bugzilla::User objects of people in various roles. More than one person - # can 'have' a role, if the person in that role has changed, or people are - # watching. - my @assignees = ($bug->assigned_to); - my @qa_contacts = $bug->qa_contact || (); - - my @ccs = @{ $bug->cc_users }; - # Include the people passed in as being in particular roles. - # This can include people who used to hold those roles. - # At this point, we don't care if there are duplicates in these arrays. - my $changer = $forced->{'changer'}; - if ($forced->{'owner'}) { - push (@assignees, Bugzilla::User->check($forced->{'owner'})); - } - - if ($forced->{'qacontact'}) { - push (@qa_contacts, Bugzilla::User->check($forced->{'qacontact'})); - } - - if ($forced->{'cc'}) { - foreach my $cc (@{$forced->{'cc'}}) { - push(@ccs, Bugzilla::User->check($cc)); - } + my ($id, $forced, $params) = @_; + $params ||= {}; + + my $dbh = Bugzilla->dbh; + my $bug = new Bugzilla::Bug($id); + + my $start = $bug->lastdiffed; + my $end = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); + + # Bugzilla::User objects of people in various roles. More than one person + # can 'have' a role, if the person in that role has changed, or people are + # watching. + my @assignees = ($bug->assigned_to); + my @qa_contacts = $bug->qa_contact || (); + + my @ccs = @{$bug->cc_users}; + + # Include the people passed in as being in particular roles. + # This can include people who used to hold those roles. + # At this point, we don't care if there are duplicates in these arrays. + my $changer = $forced->{'changer'}; + if ($forced->{'owner'}) { + push(@assignees, Bugzilla::User->check($forced->{'owner'})); + } + + if ($forced->{'qacontact'}) { + push(@qa_contacts, Bugzilla::User->check($forced->{'qacontact'})); + } + + if ($forced->{'cc'}) { + foreach my $cc (@{$forced->{'cc'}}) { + push(@ccs, Bugzilla::User->check($cc)); } - my %user_cache = map { $_->id => $_ } (@assignees, @qa_contacts, @ccs); + } + my %user_cache = map { $_->id => $_ } (@assignees, @qa_contacts, @ccs); + + my @diffs; + if (!$start) { + @diffs = _get_new_bugmail_fields($bug); + } + + my $comments = []; + + if ($params->{dep_only}) { + push( + @diffs, + { + field_name => 'bug_status', + old => $params->{changes}->{bug_status}->[0], + new => $params->{changes}->{bug_status}->[1], + login_name => $changer->login, + who => $changer, + blocker => $params->{blocker} + }, + { + field_name => 'resolution', + old => $params->{changes}->{resolution}->[0], + new => $params->{changes}->{resolution}->[1], + login_name => $changer->login, + who => $changer, + blocker => $params->{blocker} + } + ); + } + else { + push(@diffs, _get_diffs($bug, $end, \%user_cache)); - my @diffs; - if (!$start) { - @diffs = _get_new_bugmail_fields($bug); - } + $comments = $bug->comments({after => $start, to => $end}); - my $comments = []; - - if ($params->{dep_only}) { - push(@diffs, { field_name => 'bug_status', - old => $params->{changes}->{bug_status}->[0], - new => $params->{changes}->{bug_status}->[1], - login_name => $changer->login, - who => $changer, - blocker => $params->{blocker} }, - { field_name => 'resolution', - old => $params->{changes}->{resolution}->[0], - new => $params->{changes}->{resolution}->[1], - login_name => $changer->login, - who => $changer, - blocker => $params->{blocker} }); - } - else { - push(@diffs, _get_diffs($bug, $end, \%user_cache)); + # Skip empty comments. + @$comments = grep { $_->type || $_->body =~ /\S/ } @$comments; - $comments = $bug->comments({ after => $start, to => $end }); - # Skip empty comments. - @$comments = grep { $_->type || $_->body =~ /\S/ } @$comments; + # If no changes have been made, there is no need to process further. + return {'sent' => []} unless scalar(@diffs) || scalar(@$comments); + } - # If no changes have been made, there is no need to process further. - return {'sent' => []} unless scalar(@diffs) || scalar(@$comments); - } + ########################################################################### + # Start of email filtering code + ########################################################################### - ########################################################################### - # Start of email filtering code - ########################################################################### - - # A user_id => roles hash to keep track of people. - my %recipients; - my %watching; - - # We also record bugs that are referenced - my @referenced_bug_ids = (); - - # Now we work out all the people involved with this bug, and note all of - # the relationships in a hash. The keys are userids, the values are an - # array of role constants. - - # CCs - $recipients{$_->id}->{+REL_CC} = BIT_DIRECT foreach (@ccs); - - # Reporter (there's only ever one) - $recipients{$bug->reporter->id}->{+REL_REPORTER} = BIT_DIRECT; - - # QA Contact - if (Bugzilla->params->{'useqacontact'}) { - foreach (@qa_contacts) { - # QA Contact can be blank; ignore it if so. - $recipients{$_->id}->{+REL_QA} = BIT_DIRECT if $_; - } - } + # A user_id => roles hash to keep track of people. + my %recipients; + my %watching; - # Assignee - $recipients{$_->id}->{+REL_ASSIGNEE} = BIT_DIRECT foreach (@assignees); - - # The last relevant set of people are those who are being removed from - # their roles in this change. We get their names out of the diffs. - foreach my $change (@diffs) { - if ($change->{old}) { - # You can't stop being the reporter, so we don't check that - # relationship here. - # Ignore people whose user account has been deleted or renamed. - if ($change->{field_name} eq 'cc') { - foreach my $cc_user (split(/[\s,]+/, $change->{old})) { - my $uid = login_to_id($cc_user); - $recipients{$uid}->{+REL_CC} = BIT_DIRECT if $uid; - } - } - elsif ($change->{field_name} eq 'qa_contact') { - my $uid = login_to_id($change->{old}); - $recipients{$uid}->{+REL_QA} = BIT_DIRECT if $uid; - } - elsif ($change->{field_name} eq 'assigned_to') { - my $uid = login_to_id($change->{old}); - $recipients{$uid}->{+REL_ASSIGNEE} = BIT_DIRECT if $uid; - } - } + # We also record bugs that are referenced + my @referenced_bug_ids = (); - 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} // ''); - } - } + # Now we work out all the people involved with this bug, and note all of + # the relationships in a hash. The keys are userids, the values are an + # array of role constants. + + # CCs + $recipients{$_->id}->{+REL_CC} = BIT_DIRECT foreach (@ccs); + + # Reporter (there's only ever one) + $recipients{$bug->reporter->id}->{+REL_REPORTER} = BIT_DIRECT; - my $referenced_bugs = scalar(@referenced_bug_ids) - ? Bugzilla::Bug->new_from_list([uniq @referenced_bug_ids]) - : []; + # QA Contact + if (Bugzilla->params->{'useqacontact'}) { + foreach (@qa_contacts) { - # Make sure %user_cache has every user in it so far referenced - foreach my $user_id (keys %recipients) { - $user_cache{$user_id} ||= new Bugzilla::User($user_id); + # QA Contact can be blank; ignore it if so. + $recipients{$_->id}->{+REL_QA} = BIT_DIRECT if $_; } - - Bugzilla::Hook::process('bugmail_recipients', - { bug => $bug, recipients => \%recipients, - users => \%user_cache, diffs => \@diffs }); - - # We should not assume %recipients to have any entries. - if (scalar keys %recipients) { - # Find all those user-watching anyone on the current list, who is not - # on it already themselves. - my $involved = join(",", keys %recipients); - - my $userwatchers = - $dbh->selectall_arrayref("SELECT watcher, watched FROM watch - WHERE watched IN ($involved)"); - - # Mark these people as having the role of the person they are watching - foreach my $watch (@$userwatchers) { - while (my ($role, $bits) = each %{$recipients{$watch->[1]}}) { - $recipients{$watch->[0]}->{$role} |= BIT_WATCHING - if $bits & BIT_DIRECT; - } - push(@{$watching{$watch->[0]}}, $watch->[1]); + } + + # Assignee + $recipients{$_->id}->{+REL_ASSIGNEE} = BIT_DIRECT foreach (@assignees); + + # The last relevant set of people are those who are being removed from + # their roles in this change. We get their names out of the diffs. + foreach my $change (@diffs) { + if ($change->{old}) { + + # You can't stop being the reporter, so we don't check that + # relationship here. + # Ignore people whose user account has been deleted or renamed. + if ($change->{field_name} eq 'cc') { + foreach my $cc_user (split(/[\s,]+/, $change->{old})) { + my $uid = login_to_id($cc_user); + $recipients{$uid}->{+REL_CC} = BIT_DIRECT if $uid; } + } + elsif ($change->{field_name} eq 'qa_contact') { + my $uid = login_to_id($change->{old}); + $recipients{$uid}->{+REL_QA} = BIT_DIRECT if $uid; + } + elsif ($change->{field_name} eq 'assigned_to') { + my $uid = login_to_id($change->{old}); + $recipients{$uid}->{+REL_ASSIGNEE} = BIT_DIRECT if $uid; + } } - # Global watcher - my @watchers = split(/[,\s]+/, Bugzilla->params->{'globalwatchers'}); - foreach (@watchers) { - my $watcher_id = login_to_id($_); - next unless $watcher_id; - $recipients{$watcher_id}->{+REL_GLOBAL_WATCHER} = BIT_DIRECT; + 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} // ''); } - - # We now have a complete set of all the users, and their relationships to - # the bug in question. However, we are not necessarily going to mail them - # all - there are preferences, permissions checks and all sorts to do yet. - my @sent; - - # The email client will display the Date: header in the desired timezone, - # so we can always use UTC here. - my $date = $params->{dep_only} ? $end : $bug->delta_ts; - $date = format_time($date, '%a, %d %b %Y %T %z', 'UTC'); - - foreach my $user_id (keys %recipients) { - my %rels_which_want; - my $user = $user_cache{$user_id} ||= new Bugzilla::User($user_id); - # Deleted users must be excluded. - next unless $user; - - # If email notifications are disabled for this account, or the bug - # is ignored, there is no need to do additional checks. - next if ($user->email_disabled || $user->is_bug_ignored($id)); - - if ($user->can_see_bug($id)) { - # Go through each role the user has and see if they want mail in - # that role. - foreach my $relationship (keys %{$recipients{$user_id}}) { - if ($user->wants_bug_mail($bug, - $relationship, - $start ? \@diffs : [], - $comments, - $params->{dep_only}, - $changer)) - { - $rels_which_want{$relationship} = - $recipients{$user_id}->{$relationship}; - } - } - } - - if (scalar(%rels_which_want)) { - # So the user exists, can see the bug, and wants mail in at least - # one role. But do we want to send it to them? - - # We shouldn't send mail if this is a dependency mail and the - # depending bug is not visible to the user. - # This is to avoid leaking the summary of a confidential bug. - my $dep_ok = 1; - if ($params->{dep_only}) { - $dep_ok = $user->can_see_bug($params->{blocker}->id) ? 1 : 0; - } - - # Email the user if the dep check passed. - if ($dep_ok) { - my $sent_mail = sendMail( - { to => $user, - bug => $bug, - comments => $comments, - date => $date, - changer => $changer, - watchers => exists $watching{$user_id} ? - $watching{$user_id} : undef, - diffs => \@diffs, - rels_which_want => \%rels_which_want, - dep_only => $params->{dep_only}, - referenced_bugs => $referenced_bugs, - }); - push(@sent, $user->login) if $sent_mail; - } - } + } + + my $referenced_bugs + = scalar(@referenced_bug_ids) + ? Bugzilla::Bug->new_from_list([uniq @referenced_bug_ids]) + : []; + + # Make sure %user_cache has every user in it so far referenced + foreach my $user_id (keys %recipients) { + $user_cache{$user_id} ||= new Bugzilla::User($user_id); + } + + Bugzilla::Hook::process( + 'bugmail_recipients', + { + bug => $bug, + recipients => \%recipients, + users => \%user_cache, + diffs => \@diffs } + ); - # When sending bugmail about a blocker being reopened or resolved, - # we say nothing about changes in the bug being blocked, so we must - # not update lastdiffed in this case. - if (!$params->{dep_only}) { - $dbh->do('UPDATE bugs SET lastdiffed = ? WHERE bug_id = ?', - undef, ($end, $id)); - $bug->{lastdiffed} = $end; - } + # We should not assume %recipients to have any entries. + if (scalar keys %recipients) { - return {'sent' => \@sent}; -} + # Find all those user-watching anyone on the current list, who is not + # on it already themselves. + my $involved = join(",", keys %recipients); -sub sendMail { - my $params = shift; - - my $user = $params->{to}; - my $bug = $params->{bug}; - my @send_comments = @{ $params->{comments} }; - my $date = $params->{date}; - my $changer = $params->{changer}; - my $watchingRef = $params->{watchers}; - my @diffs = @{ $params->{diffs} }; - my $relRef = $params->{rels_which_want}; - my $dep_only = $params->{dep_only}; - my $referenced_bugs = $params->{referenced_bugs}; - - # Only display changes the user is allowed see. - my @display_diffs; - - foreach my $diff (@diffs) { - my $add_diff = 0; - - if (grep { $_ eq $diff->{field_name} } TIMETRACKING_FIELDS) { - $add_diff = 1 if $user->is_timetracker; - } - elsif (!$diff->{isprivate} || $user->is_insider) { - $add_diff = 1; - } - push(@display_diffs, $diff) if $add_diff; - } + my $userwatchers = $dbh->selectall_arrayref( + "SELECT watcher, watched FROM watch + WHERE watched IN ($involved)" + ); - if (!$user->is_insider) { - @send_comments = grep { !$_->is_private } @send_comments; + # Mark these people as having the role of the person they are watching + foreach my $watch (@$userwatchers) { + while (my ($role, $bits) = each %{$recipients{$watch->[1]}}) { + $recipients{$watch->[0]}->{$role} |= BIT_WATCHING if $bits & BIT_DIRECT; + } + push(@{$watching{$watch->[0]}}, $watch->[1]); } - - if (!scalar(@display_diffs) && !scalar(@send_comments)) { - # Whoops, no differences! - return 0; + } + + # Global watcher + my @watchers = split(/[,\s]+/, Bugzilla->params->{'globalwatchers'}); + foreach (@watchers) { + my $watcher_id = login_to_id($_); + next unless $watcher_id; + $recipients{$watcher_id}->{+REL_GLOBAL_WATCHER} = BIT_DIRECT; + } + + # We now have a complete set of all the users, and their relationships to + # the bug in question. However, we are not necessarily going to mail them + # all - there are preferences, permissions checks and all sorts to do yet. + my @sent; + + # The email client will display the Date: header in the desired timezone, + # so we can always use UTC here. + my $date = $params->{dep_only} ? $end : $bug->delta_ts; + $date = format_time($date, '%a, %d %b %Y %T %z', 'UTC'); + + foreach my $user_id (keys %recipients) { + my %rels_which_want; + my $user = $user_cache{$user_id} ||= new Bugzilla::User($user_id); + + # Deleted users must be excluded. + next unless $user; + + # If email notifications are disabled for this account, or the bug + # is ignored, there is no need to do additional checks. + next if ($user->email_disabled || $user->is_bug_ignored($id)); + + if ($user->can_see_bug($id)) { + + # Go through each role the user has and see if they want mail in + # that role. + foreach my $relationship (keys %{$recipients{$user_id}}) { + if ($user->wants_bug_mail( + $bug, $relationship, $start ? \@diffs : [], + $comments, $params->{dep_only}, $changer + )) + { + $rels_which_want{$relationship} = $recipients{$user_id}->{$relationship}; + } + } } - my (@reasons, @reasons_watch); - while (my ($relationship, $bits) = each %{$relRef}) { - push(@reasons, $relationship) if ($bits & BIT_DIRECT); - push(@reasons_watch, $relationship) if ($bits & BIT_WATCHING); + if (scalar(%rels_which_want)) { + + # So the user exists, can see the bug, and wants mail in at least + # one role. But do we want to send it to them? + + # We shouldn't send mail if this is a dependency mail and the + # depending bug is not visible to the user. + # This is to avoid leaking the summary of a confidential bug. + my $dep_ok = 1; + if ($params->{dep_only}) { + $dep_ok = $user->can_see_bug($params->{blocker}->id) ? 1 : 0; + } + + # Email the user if the dep check passed. + if ($dep_ok) { + my $sent_mail = sendMail({ + to => $user, + bug => $bug, + comments => $comments, + date => $date, + changer => $changer, + watchers => exists $watching{$user_id} ? $watching{$user_id} : undef, + diffs => \@diffs, + rels_which_want => \%rels_which_want, + dep_only => $params->{dep_only}, + referenced_bugs => $referenced_bugs, + }); + push(@sent, $user->login) if $sent_mail; + } } + } - my %relationships = relationships(); - my @headerrel = map { $relationships{$_} } @reasons; - my @watchingrel = map { $relationships{$_} } @reasons_watch; - push(@headerrel, 'None') unless @headerrel; - push(@watchingrel, 'None') unless @watchingrel; - push @watchingrel, map { Bugzilla::User->new($_)->login } @$watchingRef; + # When sending bugmail about a blocker being reopened or resolved, + # we say nothing about changes in the bug being blocked, so we must + # not update lastdiffed in this case. + if (!$params->{dep_only}) { + $dbh->do('UPDATE bugs SET lastdiffed = ? WHERE bug_id = ?', undef, ($end, $id)); + $bug->{lastdiffed} = $end; + } - my @changedfields = uniq map { $_->{field_name} } @display_diffs; + return {'sent' => \@sent}; +} - # Add attachments.created to changedfields if one or more - # comments contain information about a new attachment - if (grep($_->type == CMT_ATTACHMENT_CREATED, @send_comments)) { - push(@changedfields, 'attachments.created'); +sub sendMail { + my $params = shift; + + my $user = $params->{to}; + my $bug = $params->{bug}; + my @send_comments = @{$params->{comments}}; + my $date = $params->{date}; + my $changer = $params->{changer}; + my $watchingRef = $params->{watchers}; + my @diffs = @{$params->{diffs}}; + my $relRef = $params->{rels_which_want}; + my $dep_only = $params->{dep_only}; + my $referenced_bugs = $params->{referenced_bugs}; + + # Only display changes the user is allowed see. + my @display_diffs; + + foreach my $diff (@diffs) { + my $add_diff = 0; + + if (grep { $_ eq $diff->{field_name} } TIMETRACKING_FIELDS) { + $add_diff = 1 if $user->is_timetracker; } - - my $bugmailtype = "changed"; - $bugmailtype = "new" if !$bug->lastdiffed; - $bugmailtype = "dep_changed" if $dep_only; - - my $vars = { - date => $date, - to_user => $user, - bug => $bug, - reasons => \@reasons, - reasons_watch => \@reasons_watch, - reasonsheader => join(" ", @headerrel), - reasonswatchheader => join(" ", @watchingrel), - changer => $changer, - diffs => \@display_diffs, - changedfields => \@changedfields, - referenced_bugs => $user->visible_bugs($referenced_bugs), - new_comments => \@send_comments, - threadingmarker => build_thread_marker($bug->id, $user->id, !$bug->lastdiffed), - bugmailtype => $bugmailtype, - }; - if (Bugzilla->params->{'use_mailer_queue'}) { - enqueue($vars); - } else { - MessageToMTA(_generate_bugmail($vars)); + elsif (!$diff->{isprivate} || $user->is_insider) { + $add_diff = 1; } - - return 1; + push(@display_diffs, $diff) if $add_diff; + } + + if (!$user->is_insider) { + @send_comments = grep { !$_->is_private } @send_comments; + } + + if (!scalar(@display_diffs) && !scalar(@send_comments)) { + + # Whoops, no differences! + return 0; + } + + my (@reasons, @reasons_watch); + while (my ($relationship, $bits) = each %{$relRef}) { + push(@reasons, $relationship) if ($bits & BIT_DIRECT); + push(@reasons_watch, $relationship) if ($bits & BIT_WATCHING); + } + + my %relationships = relationships(); + my @headerrel = map { $relationships{$_} } @reasons; + my @watchingrel = map { $relationships{$_} } @reasons_watch; + push(@headerrel, 'None') unless @headerrel; + push(@watchingrel, 'None') unless @watchingrel; + push @watchingrel, map { Bugzilla::User->new($_)->login } @$watchingRef; + + my @changedfields = uniq map { $_->{field_name} } @display_diffs; + + # Add attachments.created to changedfields if one or more + # comments contain information about a new attachment + if (grep($_->type == CMT_ATTACHMENT_CREATED, @send_comments)) { + push(@changedfields, 'attachments.created'); + } + + my $bugmailtype = "changed"; + $bugmailtype = "new" if !$bug->lastdiffed; + $bugmailtype = "dep_changed" if $dep_only; + + my $vars = { + date => $date, + to_user => $user, + bug => $bug, + reasons => \@reasons, + reasons_watch => \@reasons_watch, + reasonsheader => join(" ", @headerrel), + reasonswatchheader => join(" ", @watchingrel), + changer => $changer, + diffs => \@display_diffs, + changedfields => \@changedfields, + referenced_bugs => $user->visible_bugs($referenced_bugs), + new_comments => \@send_comments, + threadingmarker => build_thread_marker($bug->id, $user->id, !$bug->lastdiffed), + bugmailtype => $bugmailtype, + }; + if (Bugzilla->params->{'use_mailer_queue'}) { + enqueue($vars); + } + else { + MessageToMTA(_generate_bugmail($vars)); + } + + return 1; } sub enqueue { - my ($vars) = @_; - # we need to flatten all objects to a hash before pushing to the job queue. - # the hashes need to be inflated in the dequeue method. - $vars->{bug} = _flatten_object($vars->{bug}); - $vars->{to_user} = _flatten_object($vars->{to_user}); - $vars->{changer} = _flatten_object($vars->{changer}); - $vars->{new_comments} = [ map { _flatten_object($_) } @{ $vars->{new_comments} } ]; - foreach my $diff (@{ $vars->{diffs} }) { - $diff->{who} = _flatten_object($diff->{who}); - if (exists $diff->{blocker}) { - $diff->{blocker} = _flatten_object($diff->{blocker}); - } + my ($vars) = @_; + + # we need to flatten all objects to a hash before pushing to the job queue. + # the hashes need to be inflated in the dequeue method. + $vars->{bug} = _flatten_object($vars->{bug}); + $vars->{to_user} = _flatten_object($vars->{to_user}); + $vars->{changer} = _flatten_object($vars->{changer}); + $vars->{new_comments} = [map { _flatten_object($_) } @{$vars->{new_comments}}]; + foreach my $diff (@{$vars->{diffs}}) { + $diff->{who} = _flatten_object($diff->{who}); + if (exists $diff->{blocker}) { + $diff->{blocker} = _flatten_object($diff->{blocker}); } - Bugzilla->job_queue->insert('bug_mail', { vars => $vars }); + } + Bugzilla->job_queue->insert('bug_mail', {vars => $vars}); } sub dequeue { - my ($payload) = @_; - # clone the payload so we can modify it without impacting TheSchwartz's - # ability to process the job when we've finished - my $vars = dclone($payload); - # inflate objects - $vars->{bug} = Bugzilla::Bug->new_from_hash($vars->{bug}); - $vars->{to_user} = Bugzilla::User->new_from_hash($vars->{to_user}); - $vars->{changer} = Bugzilla::User->new_from_hash($vars->{changer}); - $vars->{new_comments} = [ map { Bugzilla::Comment->new_from_hash($_) } @{ $vars->{new_comments} } ]; - foreach my $diff (@{ $vars->{diffs} }) { - $diff->{who} = Bugzilla::User->new_from_hash($diff->{who}); - if (exists $diff->{blocker}) { - $diff->{blocker} = Bugzilla::Bug->new_from_hash($diff->{blocker}); - } + my ($payload) = @_; + + # clone the payload so we can modify it without impacting TheSchwartz's + # ability to process the job when we've finished + my $vars = dclone($payload); + + # inflate objects + $vars->{bug} = Bugzilla::Bug->new_from_hash($vars->{bug}); + $vars->{to_user} = Bugzilla::User->new_from_hash($vars->{to_user}); + $vars->{changer} = Bugzilla::User->new_from_hash($vars->{changer}); + $vars->{new_comments} + = [map { Bugzilla::Comment->new_from_hash($_) } @{$vars->{new_comments}}]; + foreach my $diff (@{$vars->{diffs}}) { + $diff->{who} = Bugzilla::User->new_from_hash($diff->{who}); + if (exists $diff->{blocker}) { + $diff->{blocker} = Bugzilla::Bug->new_from_hash($diff->{blocker}); } - # generate bugmail and send - MessageToMTA(_generate_bugmail($vars), 1); + } + + # generate bugmail and send + MessageToMTA(_generate_bugmail($vars), 1); } sub _flatten_object { - my ($object) = @_; - # nothing to do if it's already flattened - return $object unless blessed($object); - # the same objects are used for each recipient, so cache the flattened hash - my $cache = Bugzilla->request_cache->{bugmail_flat_objects} ||= {}; - my $key = blessed($object) . '-' . $object->id; - return $cache->{$key} ||= $object->flatten_to_hash; + my ($object) = @_; + + # nothing to do if it's already flattened + return $object unless blessed($object); + + # the same objects are used for each recipient, so cache the flattened hash + my $cache = Bugzilla->request_cache->{bugmail_flat_objects} ||= {}; + my $key = blessed($object) . '-' . $object->id; + return $cache->{$key} ||= $object->flatten_to_hash; } sub _generate_bugmail { - my ($vars) = @_; - my $user = $vars->{to_user}; - my $template = Bugzilla->template_inner($user->setting('lang')); - my ($msg_text, $msg_html, $msg_header); - state $use_utf8 = Bugzilla->params->{'utf8'}; - - $template->process("email/bugmail-header.txt.tmpl", $vars, \$msg_header) - || ThrowTemplateError($template->error()); - $template->process("email/bugmail.txt.tmpl", $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, - ) - ); - if ($user->setting('email_format') eq 'html') { - $template->process("email/bugmail.html.tmpl", $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, - ); - } - - 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; + my ($vars) = @_; + my $user = $vars->{to_user}; + my $template = Bugzilla->template_inner($user->setting('lang')); + my ($msg_text, $msg_html, $msg_header); + state $use_utf8 = Bugzilla->params->{'utf8'}; + + $template->process("email/bugmail-header.txt.tmpl", $vars, \$msg_header) + || ThrowTemplateError($template->error()); + $template->process("email/bugmail.txt.tmpl", $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, + )); + + if ($user->setting('email_format') eq 'html') { + $template->process("email/bugmail.html.tmpl", $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, + ); + } + + 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 _get_diffs { - my ($bug, $end, $user_cache) = @_; - my $dbh = Bugzilla->dbh; - - my @args = ($bug->id); - # If lastdiffed is NULL, then we don't limit the search on time. - my $when_restriction = ''; - if ($bug->lastdiffed) { - $when_restriction = ' AND bug_when > ? AND bug_when <= ?'; - push @args, ($bug->lastdiffed, $end); - } + my ($bug, $end, $user_cache) = @_; + my $dbh = Bugzilla->dbh; - my $diffs = $dbh->selectall_arrayref( - "SELECT fielddefs.name AS field_name, + my @args = ($bug->id); + + # If lastdiffed is NULL, then we don't limit the search on time. + my $when_restriction = ''; + if ($bug->lastdiffed) { + $when_restriction = ' AND bug_when > ? AND bug_when <= ?'; + push @args, ($bug->lastdiffed, $end); + } + + my $diffs = $dbh->selectall_arrayref( + "SELECT fielddefs.name AS field_name, bugs_activity.bug_when, bugs_activity.removed AS old, bugs_activity.added AS new, bugs_activity.attach_id, bugs_activity.comment_id, bugs_activity.who @@ -500,89 +531,92 @@ sub _get_diffs { ON fielddefs.id = bugs_activity.fieldid WHERE bugs_activity.bug_id = ? $when_restriction - ORDER BY bugs_activity.bug_when, bugs_activity.id", - {Slice=>{}}, @args); - - foreach my $diff (@$diffs) { - $user_cache->{$diff->{who}} ||= new Bugzilla::User($diff->{who}); - $diff->{who} = $user_cache->{$diff->{who}}; - if ($diff->{attach_id}) { - $diff->{isprivate} = $dbh->selectrow_array( - 'SELECT isprivate FROM attachments WHERE attach_id = ?', - undef, $diff->{attach_id}); - } - if ($diff->{field_name} eq 'longdescs.isprivate') { - my $comment = Bugzilla::Comment->new($diff->{comment_id}); - $diff->{num} = $comment->count; - $diff->{isprivate} = $diff->{new}; - } + ORDER BY bugs_activity.bug_when, bugs_activity.id", {Slice => {}}, @args + ); + + foreach my $diff (@$diffs) { + $user_cache->{$diff->{who}} ||= new Bugzilla::User($diff->{who}); + $diff->{who} = $user_cache->{$diff->{who}}; + if ($diff->{attach_id}) { + $diff->{isprivate} + = $dbh->selectrow_array( + 'SELECT isprivate FROM attachments WHERE attach_id = ?', + undef, $diff->{attach_id}); } - - 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; + if ($diff->{field_name} eq 'longdescs.isprivate') { + my $comment = Bugzilla::Comment->new($diff->{comment_id}); + $diff->{num} = $comment->count; + $diff->{isprivate} = $diff->{new}; + } + } + + 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; + return @changes; } sub _get_new_bugmail_fields { - my $bug = shift; - my @fields = @{ Bugzilla->fields({obsolete => 0, in_new_bugmail => 1}) }; - my @diffs; - my $params = Bugzilla->params; - - foreach my $field (@fields) { - my $name = $field->name; - my $value = $bug->$name; - - next if !$field->is_visible_on_bug($bug) - || ($name eq 'classification' && !$params->{'useclassification'}) - || ($name eq 'status_whiteboard' && !$params->{'usestatuswhiteboard'}) - || ($name eq 'qa_contact' && !$params->{'useqacontact'}) - || ($name eq 'target_milestone' && !$params->{'usetargetmilestone'}); - - if (ref $value eq 'ARRAY') { - $value = join(', ', @$value); - } - elsif (blessed($value) && $value->isa('Bugzilla::User')) { - $value = $value->login; - } - elsif (blessed($value) && $value->isa('Bugzilla::Object')) { - $value = $value->name; - } - elsif ($name eq 'estimated_time') { - # "0.00" (which is what we get from the DB) is true, - # so we explicitly do a numerical comparison with 0. - $value = 0 if $value == 0; - } - elsif ($name eq 'deadline') { - $value = time2str("%Y-%m-%d", str2time($value)) if $value; - } - - # If there isn't anything to show, don't include this header. - next unless $value; + my $bug = shift; + my @fields = @{Bugzilla->fields({obsolete => 0, in_new_bugmail => 1})}; + my @diffs; + my $params = Bugzilla->params; + + foreach my $field (@fields) { + my $name = $field->name; + my $value = $bug->$name; + + next + if !$field->is_visible_on_bug($bug) + || ($name eq 'classification' && !$params->{'useclassification'}) + || ($name eq 'status_whiteboard' && !$params->{'usestatuswhiteboard'}) + || ($name eq 'qa_contact' && !$params->{'useqacontact'}) + || ($name eq 'target_milestone' && !$params->{'usetargetmilestone'}); + + if (ref $value eq 'ARRAY') { + $value = join(', ', @$value); + } + elsif (blessed($value) && $value->isa('Bugzilla::User')) { + $value = $value->login; + } + elsif (blessed($value) && $value->isa('Bugzilla::Object')) { + $value = $value->name; + } + elsif ($name eq 'estimated_time') { - push(@diffs, { - field_name => $name, - who => $bug->reporter, - new => $value}); + # "0.00" (which is what we get from the DB) is true, + # so we explicitly do a numerical comparison with 0. + $value = 0 if $value == 0; } + elsif ($name eq 'deadline') { + $value = time2str("%Y-%m-%d", str2time($value)) if $value; + } + + # If there isn't anything to show, don't include this header. + next unless $value; + + push(@diffs, {field_name => $name, who => $bug->reporter, new => $value}); + } - return @diffs; + return @diffs; } 1; diff --git a/Bugzilla/BugUrl.pm b/Bugzilla/BugUrl.pm index 1d75fe8f1..255be8623 100644 --- a/Bugzilla/BugUrl.pm +++ b/Bugzilla/BugUrl.pm @@ -28,48 +28,49 @@ use URI::QueryParam; use constant DB_TABLE => 'bug_see_also'; use constant NAME_FIELD => 'value'; use constant LIST_ORDER => 'id'; + # See Also is tracked in bugs_activity. use constant AUDIT_CREATES => 0; use constant AUDIT_UPDATES => 0; use constant AUDIT_REMOVES => 0; use constant DB_COLUMNS => qw( - id - bug_id - value - class + id + bug_id + value + class ); # This must be strings with the names of the validations, # instead of coderefs, because subclasses override these # validators with their own. use constant VALIDATORS => { - value => '_check_value', - bug_id => '_check_bug_id', - class => \&_check_class, + value => '_check_value', + bug_id => '_check_bug_id', + class => \&_check_class, }; # This is the order we go through all of subclasses and # pick the first one that should handle the url. New # subclasses should be added at the end of the list. use constant SUB_CLASSES => qw( - Bugzilla::BugUrl::Bugzilla::Local - Bugzilla::BugUrl::Bugzilla - Bugzilla::BugUrl::Launchpad - Bugzilla::BugUrl::Google - Bugzilla::BugUrl::Debian - Bugzilla::BugUrl::JIRA - Bugzilla::BugUrl::Trac - Bugzilla::BugUrl::MantisBT - Bugzilla::BugUrl::SourceForge - Bugzilla::BugUrl::GitHub + Bugzilla::BugUrl::Bugzilla::Local + Bugzilla::BugUrl::Bugzilla + Bugzilla::BugUrl::Launchpad + Bugzilla::BugUrl::Google + Bugzilla::BugUrl::Debian + Bugzilla::BugUrl::JIRA + Bugzilla::BugUrl::Trac + Bugzilla::BugUrl::MantisBT + Bugzilla::BugUrl::SourceForge + Bugzilla::BugUrl::GitHub ); ############################### #### Accessors ###### ############################### -sub class { return $_[0]->{class} } +sub class { return $_[0]->{class} } sub bug_id { return $_[0]->{bug_id} } ############################### @@ -77,130 +78,127 @@ sub bug_id { return $_[0]->{bug_id} } ############################### sub new { - my $class = shift; - my $param = shift; - - if (ref $param) { - my $bug_id = $param->{bug_id}; - my $name = $param->{name} || $param->{value}; - if (!defined $bug_id) { - ThrowCodeError('bad_arg', - { argument => 'bug_id', - function => "${class}::new" }); - } - if (!defined $name) { - ThrowCodeError('bad_arg', - { argument => 'name', - function => "${class}::new" }); - } - - my $condition = 'bug_id = ? AND value = ?'; - my @values = ($bug_id, $name); - $param = { condition => $condition, values => \@values }; + my $class = shift; + my $param = shift; + + if (ref $param) { + my $bug_id = $param->{bug_id}; + my $name = $param->{name} || $param->{value}; + if (!defined $bug_id) { + ThrowCodeError('bad_arg', {argument => 'bug_id', function => "${class}::new"}); + } + if (!defined $name) { + ThrowCodeError('bad_arg', {argument => 'name', function => "${class}::new"}); } - unshift @_, $param; - return $class->SUPER::new(@_); + my $condition = 'bug_id = ? AND value = ?'; + my @values = ($bug_id, $name); + $param = {condition => $condition, values => \@values}; + } + + unshift @_, $param; + return $class->SUPER::new(@_); } sub _do_list_select { - my $class = shift; - my $objects = $class->SUPER::_do_list_select(@_); + my $class = shift; + my $objects = $class->SUPER::_do_list_select(@_); - foreach my $object (@$objects) { - eval "use " . $object->class; - # If the class cannot be loaded, then we build a generic object. - bless $object, ($@ ? 'Bugzilla::BugUrl' : $object->class); - } + foreach my $object (@$objects) { + eval "use " . $object->class; + + # If the class cannot be loaded, then we build a generic object. + bless $object, ($@ ? 'Bugzilla::BugUrl' : $object->class); + } - return $objects + return $objects; } # This is an abstract method. It must be overridden # in every subclass. sub should_handle { - my ($class, $input) = @_; - ThrowCodeError('unknown_method', - { method => "${class}::should_handle" }); + my ($class, $input) = @_; + ThrowCodeError('unknown_method', {method => "${class}::should_handle"}); } sub class_for { - my ($class, $value) = @_; - - my @sub_classes = $class->SUB_CLASSES; - Bugzilla::Hook::process("bug_url_sub_classes", - { sub_classes => \@sub_classes }); - - my $uri = URI->new($value); - foreach my $subclass (@sub_classes) { - eval "use $subclass"; - die $@ if $@; - return wantarray ? ($subclass, $uri) : $subclass - if $subclass->should_handle($uri); - } + my ($class, $value) = @_; - ThrowUserError('bug_url_invalid', { url => $value }); + my @sub_classes = $class->SUB_CLASSES; + Bugzilla::Hook::process("bug_url_sub_classes", {sub_classes => \@sub_classes}); + + my $uri = URI->new($value); + foreach my $subclass (@sub_classes) { + eval "use $subclass"; + die $@ if $@; + return wantarray ? ($subclass, $uri) : $subclass + if $subclass->should_handle($uri); + } + + ThrowUserError('bug_url_invalid', {url => $value}); } sub _check_class { - my ($class, $subclass) = @_; - eval "use $subclass"; die $@ if $@; - return $subclass; + my ($class, $subclass) = @_; + eval "use $subclass"; + die $@ if $@; + return $subclass; } sub _check_bug_id { - my ($class, $bug_id) = @_; + my ($class, $bug_id) = @_; - my $bug; - if (blessed $bug_id) { - # We got a bug object passed in, use it - $bug = $bug_id; - $bug->check_is_visible; - } - else { - # We got a bug id passed in, check it and get the bug object - $bug = Bugzilla::Bug->check({ id => $bug_id }); - } + my $bug; + if (blessed $bug_id) { + + # We got a bug object passed in, use it + $bug = $bug_id; + $bug->check_is_visible; + } + else { + # We got a bug id passed in, check it and get the bug object + $bug = Bugzilla::Bug->check({id => $bug_id}); + } - return $bug->id; + return $bug->id; } sub _check_value { - my ($class, $uri) = @_; - - my $value = $uri->as_string; - - if (!$value) { - ThrowCodeError('param_required', - { function => 'add_see_also', param => '$value' }); - } - - # We assume that the URL is an HTTP URL if there is no (something):// - # in front. - if (!$uri->scheme) { - # This works better than setting $uri->scheme('http'), because - # that creates URLs like "http:domain.com" and doesn't properly - # differentiate the path from the domain. - $uri = new URI("http://$value"); - } - elsif ($uri->scheme ne 'http' && $uri->scheme ne 'https') { - ThrowUserError('bug_url_invalid', { url => $value, reason => 'http' }); - } - - # This stops the following edge cases from being accepted: - # * show_bug.cgi?id=1 - # * /show_bug.cgi?id=1 - # * http:///show_bug.cgi?id=1 - if (!$uri->authority or $uri->path !~ m{/}) { - ThrowUserError('bug_url_invalid', - { url => $value, reason => 'path_only' }); - } - - if (length($uri->path) > MAX_BUG_URL_LENGTH) { - ThrowUserError('bug_url_too_long', { url => $uri->path }); - } - - return $uri; + my ($class, $uri) = @_; + + my $value = $uri->as_string; + + if (!$value) { + ThrowCodeError('param_required', + {function => 'add_see_also', param => '$value'}); + } + + # We assume that the URL is an HTTP URL if there is no (something):// + # in front. + if (!$uri->scheme) { + + # This works better than setting $uri->scheme('http'), because + # that creates URLs like "http:domain.com" and doesn't properly + # differentiate the path from the domain. + $uri = new URI("http://$value"); + } + elsif ($uri->scheme ne 'http' && $uri->scheme ne 'https') { + ThrowUserError('bug_url_invalid', {url => $value, reason => 'http'}); + } + + # This stops the following edge cases from being accepted: + # * show_bug.cgi?id=1 + # * /show_bug.cgi?id=1 + # * http:///show_bug.cgi?id=1 + if (!$uri->authority or $uri->path !~ m{/}) { + ThrowUserError('bug_url_invalid', {url => $value, reason => 'path_only'}); + } + + if (length($uri->path) > MAX_BUG_URL_LENGTH) { + ThrowUserError('bug_url_too_long', {url => $uri->path}); + } + + return $uri; } 1; diff --git a/Bugzilla/BugUrl/Bugzilla.pm b/Bugzilla/BugUrl/Bugzilla.pm index 402ff1509..9d036100f 100644 --- a/Bugzilla/BugUrl/Bugzilla.pm +++ b/Bugzilla/BugUrl/Bugzilla.pm @@ -21,37 +21,39 @@ use Bugzilla::Util; ############################### sub should_handle { - my ($class, $uri) = @_; - return ($uri->path =~ /show_bug\.cgi$/) ? 1 : 0; + my ($class, $uri) = @_; + return ($uri->path =~ /show_bug\.cgi$/) ? 1 : 0; } sub _check_value { - my ($class, $uri) = @_; - - $uri = $class->SUPER::_check_value($uri); - - my $bug_id = $uri->query_param('id'); - # We don't currently allow aliases, because we can't check to see - # if somebody's putting both an alias link and a numeric ID link. - # When we start validating the URL by accessing the other Bugzilla, - # we can allow aliases. - detaint_natural($bug_id); - if (!$bug_id) { - my $value = $uri->as_string; - ThrowUserError('bug_url_invalid', { url => $value, reason => 'id' }); - } - - # Make sure that "id" is the only query parameter. - $uri->query("id=$bug_id"); - # And remove any # part if there is one. - $uri->fragment(undef); - - return $uri; + my ($class, $uri) = @_; + + $uri = $class->SUPER::_check_value($uri); + + my $bug_id = $uri->query_param('id'); + + # We don't currently allow aliases, because we can't check to see + # if somebody's putting both an alias link and a numeric ID link. + # When we start validating the URL by accessing the other Bugzilla, + # we can allow aliases. + detaint_natural($bug_id); + if (!$bug_id) { + my $value = $uri->as_string; + ThrowUserError('bug_url_invalid', {url => $value, reason => 'id'}); + } + + # Make sure that "id" is the only query parameter. + $uri->query("id=$bug_id"); + + # And remove any # part if there is one. + $uri->fragment(undef); + + return $uri; } sub target_bug_id { - my ($self) = @_; - return new URI($self->name)->query_param('id'); + my ($self) = @_; + return new URI($self->name)->query_param('id'); } 1; diff --git a/Bugzilla/BugUrl/Bugzilla/Local.pm b/Bugzilla/BugUrl/Bugzilla/Local.pm index 7b9cb6a4f..3fe0fcb5d 100644 --- a/Bugzilla/BugUrl/Bugzilla/Local.pm +++ b/Bugzilla/BugUrl/Bugzilla/Local.pm @@ -20,77 +20,75 @@ use Bugzilla::Util; #### Initialization #### ############################### -use constant VALIDATOR_DEPENDENCIES => { - value => ['bug_id'], -}; +use constant VALIDATOR_DEPENDENCIES => {value => ['bug_id'],}; ############################### #### Methods #### ############################### sub ref_bug_url { - my $self = shift; - - if (!exists $self->{ref_bug_url}) { - my $ref_bug_id = new URI($self->name)->query_param('id'); - my $ref_bug = Bugzilla::Bug->check($ref_bug_id); - my $ref_value = $self->local_uri($self->bug_id); - $self->{ref_bug_url} = - new Bugzilla::BugUrl::Bugzilla::Local({ bug_id => $ref_bug->id, - value => $ref_value }); - } - return $self->{ref_bug_url}; + my $self = shift; + + if (!exists $self->{ref_bug_url}) { + my $ref_bug_id = new URI($self->name)->query_param('id'); + my $ref_bug = Bugzilla::Bug->check($ref_bug_id); + my $ref_value = $self->local_uri($self->bug_id); + $self->{ref_bug_url} = new Bugzilla::BugUrl::Bugzilla::Local( + {bug_id => $ref_bug->id, value => $ref_value}); + } + return $self->{ref_bug_url}; } sub should_handle { - my ($class, $uri) = @_; - - # Check if it is either a bug id number or an alias. - return 1 if $uri->as_string =~ m/^\w+$/; - - # Check if it is a local Bugzilla uri and call - # Bugzilla::BugUrl::Bugzilla to check if it's a valid Bugzilla - # see also url. - my $canonical_local = URI->new($class->local_uri)->canonical; - if ($canonical_local->authority eq $uri->canonical->authority - and $canonical_local->path eq $uri->canonical->path) - { - return $class->SUPER::should_handle($uri); - } - - return 0; + my ($class, $uri) = @_; + + # Check if it is either a bug id number or an alias. + return 1 if $uri->as_string =~ m/^\w+$/; + + # Check if it is a local Bugzilla uri and call + # Bugzilla::BugUrl::Bugzilla to check if it's a valid Bugzilla + # see also url. + my $canonical_local = URI->new($class->local_uri)->canonical; + if ( $canonical_local->authority eq $uri->canonical->authority + and $canonical_local->path eq $uri->canonical->path) + { + return $class->SUPER::should_handle($uri); + } + + return 0; } sub _check_value { - my ($class, $uri, undef, $params) = @_; - - # At this point we are going to treat any word as a - # bug id/alias to the local Bugzilla. - my $value = $uri->as_string; - if ($value =~ m/^\w+$/) { - $uri = new URI($class->local_uri($value)); - } else { - # It's not a word, then we have to check - # if it's a valid Bugzilla url. - $uri = $class->SUPER::_check_value($uri); - } - - my $ref_bug_id = $uri->query_param('id'); - my $ref_bug = Bugzilla::Bug->check($ref_bug_id); - my $self_bug_id = $params->{bug_id}; - $params->{ref_bug} = $ref_bug; - - if ($ref_bug->id == $self_bug_id) { - ThrowUserError('see_also_self_reference'); - } - - return $uri; + my ($class, $uri, undef, $params) = @_; + + # At this point we are going to treat any word as a + # bug id/alias to the local Bugzilla. + my $value = $uri->as_string; + if ($value =~ m/^\w+$/) { + $uri = new URI($class->local_uri($value)); + } + else { + # It's not a word, then we have to check + # if it's a valid Bugzilla url. + $uri = $class->SUPER::_check_value($uri); + } + + my $ref_bug_id = $uri->query_param('id'); + my $ref_bug = Bugzilla::Bug->check($ref_bug_id); + my $self_bug_id = $params->{bug_id}; + $params->{ref_bug} = $ref_bug; + + if ($ref_bug->id == $self_bug_id) { + ThrowUserError('see_also_self_reference'); + } + + return $uri; } sub local_uri { - my ($self, $bug_id) = @_; - $bug_id ||= ''; - return correct_urlbase() . "show_bug.cgi?id=$bug_id"; + my ($self, $bug_id) = @_; + $bug_id ||= ''; + return correct_urlbase() . "show_bug.cgi?id=$bug_id"; } 1; diff --git a/Bugzilla/BugUrl/Debian.pm b/Bugzilla/BugUrl/Debian.pm index 2b611aa57..f49f2b820 100644 --- a/Bugzilla/BugUrl/Debian.pm +++ b/Bugzilla/BugUrl/Debian.pm @@ -18,28 +18,30 @@ use parent qw(Bugzilla::BugUrl); ############################### sub should_handle { - my ($class, $uri) = @_; - - # Debian BTS URLs can look like various things: - # http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1234 - # http://bugs.debian.org/1234 - return (lc($uri->authority) eq 'bugs.debian.org' - and (($uri->path =~ /bugreport\.cgi$/ - and $uri->query_param('bug') =~ m|^\d+$|) - or $uri->path =~ m|^/\d+$|)) ? 1 : 0; + my ($class, $uri) = @_; + + # Debian BTS URLs can look like various things: + # http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1234 + # http://bugs.debian.org/1234 + return ( + lc($uri->authority) eq 'bugs.debian.org' + and + (($uri->path =~ /bugreport\.cgi$/ and $uri->query_param('bug') =~ m|^\d+$|) + or $uri->path =~ m|^/\d+$|) + ) ? 1 : 0; } sub _check_value { - my $class = shift; + my $class = shift; - my $uri = $class->SUPER::_check_value(@_); + my $uri = $class->SUPER::_check_value(@_); - # This is the shortest standard URL form for Debian BTS URLs, - # and so we reduce all URLs to this. - $uri->path =~ m|^/(\d+)$| || $uri->query_param('bug') =~ m|^(\d+)$|; - $uri = new URI("http://bugs.debian.org/$1"); + # This is the shortest standard URL form for Debian BTS URLs, + # and so we reduce all URLs to this. + $uri->path =~ m|^/(\d+)$| || $uri->query_param('bug') =~ m|^(\d+)$|; + $uri = new URI("http://bugs.debian.org/$1"); - return $uri; + return $uri; } 1; diff --git a/Bugzilla/BugUrl/GitHub.pm b/Bugzilla/BugUrl/GitHub.pm index f14f1d6b0..583837a60 100644 --- a/Bugzilla/BugUrl/GitHub.pm +++ b/Bugzilla/BugUrl/GitHub.pm @@ -18,25 +18,25 @@ use parent qw(Bugzilla::BugUrl); ############################### sub should_handle { - my ($class, $uri) = @_; - - # GitHub issue URLs have only one form: - # https://github.com/USER_OR_TEAM_OR_ORGANIZATION_NAME/REPOSITORY_NAME/issues/111 - # GitHub pull request URLs have only one form: - # https://github.com/USER_OR_TEAM_OR_ORGANIZATION_NAME/REPOSITORY_NAME/pull/111 - return (lc($uri->authority) eq 'github.com' - and $uri->path =~ m!^/[^/]+/[^/]+/(?:issues|pull)/\d+$!) ? 1 : 0; + my ($class, $uri) = @_; + +# GitHub issue URLs have only one form: +# https://github.com/USER_OR_TEAM_OR_ORGANIZATION_NAME/REPOSITORY_NAME/issues/111 +# GitHub pull request URLs have only one form: +# https://github.com/USER_OR_TEAM_OR_ORGANIZATION_NAME/REPOSITORY_NAME/pull/111 + return (lc($uri->authority) eq 'github.com' + and $uri->path =~ m!^/[^/]+/[^/]+/(?:issues|pull)/\d+$!) ? 1 : 0; } sub _check_value { - my ($class, $uri) = @_; + my ($class, $uri) = @_; - $uri = $class->SUPER::_check_value($uri); + $uri = $class->SUPER::_check_value($uri); - # GitHub HTTP URLs redirect to HTTPS, so just use the HTTPS scheme. - $uri->scheme('https'); + # GitHub HTTP URLs redirect to HTTPS, so just use the HTTPS scheme. + $uri->scheme('https'); - return $uri; + return $uri; } 1; diff --git a/Bugzilla/BugUrl/Google.pm b/Bugzilla/BugUrl/Google.pm index 71a9c46fb..106425302 100644 --- a/Bugzilla/BugUrl/Google.pm +++ b/Bugzilla/BugUrl/Google.pm @@ -18,27 +18,27 @@ use parent qw(Bugzilla::BugUrl); ############################### sub should_handle { - my ($class, $uri) = @_; + my ($class, $uri) = @_; - # Google Code URLs only have one form: - # http(s)://code.google.com/p/PROJECT_NAME/issues/detail?id=1234 - return (lc($uri->authority) eq 'code.google.com' - and $uri->path =~ m|^/p/[^/]+/issues/detail$| - and $uri->query_param('id') =~ /^\d+$/) ? 1 : 0; + # Google Code URLs only have one form: + # http(s)://code.google.com/p/PROJECT_NAME/issues/detail?id=1234 + return (lc($uri->authority) eq 'code.google.com' + and $uri->path =~ m|^/p/[^/]+/issues/detail$| + and $uri->query_param('id') =~ /^\d+$/) ? 1 : 0; } sub _check_value { - my ($class, $uri) = @_; - - $uri = $class->SUPER::_check_value($uri); + my ($class, $uri) = @_; - # While Google Code URLs can be either HTTP or HTTPS, - # always go with the HTTP scheme, as that's the default. - if ($uri->scheme eq 'https') { - $uri->scheme('http'); - } + $uri = $class->SUPER::_check_value($uri); - return $uri; + # While Google Code URLs can be either HTTP or HTTPS, + # always go with the HTTP scheme, as that's the default. + if ($uri->scheme eq 'https') { + $uri->scheme('http'); + } + + return $uri; } 1; diff --git a/Bugzilla/BugUrl/JIRA.pm b/Bugzilla/BugUrl/JIRA.pm index e9d2a2d2a..b42b1decc 100644 --- a/Bugzilla/BugUrl/JIRA.pm +++ b/Bugzilla/BugUrl/JIRA.pm @@ -18,25 +18,26 @@ use parent qw(Bugzilla::BugUrl); ############################### sub should_handle { - my ($class, $uri) = @_; + my ($class, $uri) = @_; - # JIRA URLs have only one basic form (but the jira is optional): - # https://issues.apache.org/jira/browse/KEY-1234 - # http://issues.example.com/browse/KEY-1234 - return ($uri->path =~ m|/browse/[A-Z][A-Z]+-\d+$|) ? 1 : 0; + # JIRA URLs have only one basic form (but the jira is optional): + # https://issues.apache.org/jira/browse/KEY-1234 + # http://issues.example.com/browse/KEY-1234 + return ($uri->path =~ m|/browse/[A-Z][A-Z]+-\d+$|) ? 1 : 0; } sub _check_value { - my $class = shift; + my $class = shift; - my $uri = $class->SUPER::_check_value(@_); + my $uri = $class->SUPER::_check_value(@_); - # Make sure there are no query parameters. - $uri->query(undef); - # And remove any # part if there is one. - $uri->fragment(undef); + # Make sure there are no query parameters. + $uri->query(undef); - return $uri; + # And remove any # part if there is one. + $uri->fragment(undef); + + return $uri; } 1; diff --git a/Bugzilla/BugUrl/Launchpad.pm b/Bugzilla/BugUrl/Launchpad.pm index 0362747a2..5be8088d1 100644 --- a/Bugzilla/BugUrl/Launchpad.pm +++ b/Bugzilla/BugUrl/Launchpad.pm @@ -18,27 +18,28 @@ use parent qw(Bugzilla::BugUrl); ############################### sub should_handle { - my ($class, $uri) = @_; - - # Launchpad bug URLs can look like various things: - # https://bugs.launchpad.net/ubuntu/+bug/1234 - # https://launchpad.net/bugs/1234 - # All variations end with either "/bugs/1234" or "/+bug/1234" - return ($uri->authority =~ /launchpad\.net$/ - and $uri->path =~ m|bugs?/\d+$|) ? 1 : 0; + my ($class, $uri) = @_; + + # Launchpad bug URLs can look like various things: + # https://bugs.launchpad.net/ubuntu/+bug/1234 + # https://launchpad.net/bugs/1234 + # All variations end with either "/bugs/1234" or "/+bug/1234" + return ($uri->authority =~ /launchpad\.net$/ and $uri->path =~ m|bugs?/\d+$|) + ? 1 + : 0; } sub _check_value { - my ($class, $uri) = @_; + my ($class, $uri) = @_; - $uri = $class->SUPER::_check_value($uri); + $uri = $class->SUPER::_check_value($uri); - # This is the shortest standard URL form for Launchpad bugs, - # and so we reduce all URLs to this. - $uri->path =~ m|bugs?/(\d+)$|; - $uri = new URI("https://launchpad.net/bugs/$1"); + # This is the shortest standard URL form for Launchpad bugs, + # and so we reduce all URLs to this. + $uri->path =~ m|bugs?/(\d+)$|; + $uri = new URI("https://launchpad.net/bugs/$1"); - return $uri; + return $uri; } 1; diff --git a/Bugzilla/BugUrl/MantisBT.pm b/Bugzilla/BugUrl/MantisBT.pm index 60d3b578e..742ae1a47 100644 --- a/Bugzilla/BugUrl/MantisBT.pm +++ b/Bugzilla/BugUrl/MantisBT.pm @@ -18,22 +18,22 @@ use parent qw(Bugzilla::BugUrl); ############################### sub should_handle { - my ($class, $uri) = @_; + my ($class, $uri) = @_; - # MantisBT URLs look like the following ('bugs' directory is optional): - # http://www.mantisbt.org/bugs/view.php?id=1234 - return ($uri->path_query =~ m|view\.php\?id=\d+$|) ? 1 : 0; + # MantisBT URLs look like the following ('bugs' directory is optional): + # http://www.mantisbt.org/bugs/view.php?id=1234 + return ($uri->path_query =~ m|view\.php\?id=\d+$|) ? 1 : 0; } sub _check_value { - my $class = shift; + my $class = shift; - my $uri = $class->SUPER::_check_value(@_); + my $uri = $class->SUPER::_check_value(@_); - # Remove any # part if there is one. - $uri->fragment(undef); + # Remove any # part if there is one. + $uri->fragment(undef); - return $uri; + return $uri; } 1; diff --git a/Bugzilla/BugUrl/SourceForge.pm b/Bugzilla/BugUrl/SourceForge.pm index acba0df28..ffdde42f4 100644 --- a/Bugzilla/BugUrl/SourceForge.pm +++ b/Bugzilla/BugUrl/SourceForge.pm @@ -18,27 +18,27 @@ use parent qw(Bugzilla::BugUrl); ############################### sub should_handle { - my ($class, $uri) = @_; - - # SourceForge tracker URLs have only one form: - # http://sourceforge.net/tracker/?func=detail&aid=111&group_id=111&atid=111 - return (lc($uri->authority) eq 'sourceforge.net' - and $uri->path =~ m|/tracker/| - and $uri->query_param('func') eq 'detail' - and $uri->query_param('aid') - and $uri->query_param('group_id') - and $uri->query_param('atid')) ? 1 : 0; + my ($class, $uri) = @_; + + # SourceForge tracker URLs have only one form: + # http://sourceforge.net/tracker/?func=detail&aid=111&group_id=111&atid=111 + return (lc($uri->authority) eq 'sourceforge.net' + and $uri->path =~ m|/tracker/| + and $uri->query_param('func') eq 'detail' + and $uri->query_param('aid') + and $uri->query_param('group_id') + and $uri->query_param('atid')) ? 1 : 0; } sub _check_value { - my $class = shift; + my $class = shift; - my $uri = $class->SUPER::_check_value(@_); + my $uri = $class->SUPER::_check_value(@_); - # Remove any # part if there is one. - $uri->fragment(undef); + # Remove any # part if there is one. + $uri->fragment(undef); - return $uri; + return $uri; } 1; diff --git a/Bugzilla/BugUrl/Trac.pm b/Bugzilla/BugUrl/Trac.pm index fe74abf33..22418a1df 100644 --- a/Bugzilla/BugUrl/Trac.pm +++ b/Bugzilla/BugUrl/Trac.pm @@ -18,25 +18,26 @@ use parent qw(Bugzilla::BugUrl); ############################### sub should_handle { - my ($class, $uri) = @_; + my ($class, $uri) = @_; - # Trac URLs can look like various things: - # http://dev.mutt.org/trac/ticket/1234 - # http://trac.roundcube.net/ticket/1484130 - return ($uri->path =~ m|/ticket/\d+$|) ? 1 : 0; + # Trac URLs can look like various things: + # http://dev.mutt.org/trac/ticket/1234 + # http://trac.roundcube.net/ticket/1484130 + return ($uri->path =~ m|/ticket/\d+$|) ? 1 : 0; } sub _check_value { - my $class = shift; + my $class = shift; - my $uri = $class->SUPER::_check_value(@_); + my $uri = $class->SUPER::_check_value(@_); - # Make sure there are no query parameters. - $uri->query(undef); - # And remove any # part if there is one. - $uri->fragment(undef); + # Make sure there are no query parameters. + $uri->query(undef); - return $uri; + # And remove any # part if there is one. + $uri->fragment(undef); + + return $uri; } 1; diff --git a/Bugzilla/BugUserLastVisit.pm b/Bugzilla/BugUserLastVisit.pm index d043b121a..d1c351959 100644 --- a/Bugzilla/BugUserLastVisit.pm +++ b/Bugzilla/BugUserLastVisit.pm @@ -25,25 +25,27 @@ use constant LIST_ORDER => 'id'; use constant NAME_FIELD => 'id'; # turn off auditing and exclude these objects from memcached -use constant { AUDIT_CREATES => 0, - AUDIT_UPDATES => 0, - AUDIT_REMOVES => 0, - USE_MEMCACHED => 0 }; +use constant { + AUDIT_CREATES => 0, + AUDIT_UPDATES => 0, + AUDIT_REMOVES => 0, + USE_MEMCACHED => 0 +}; ##################################################################### # Provide accessors for our columns ##################################################################### -sub id { return $_[0]->{id} } -sub bug_id { return $_[0]->{bug_id} } -sub user_id { return $_[0]->{user_id} } +sub id { return $_[0]->{id} } +sub bug_id { return $_[0]->{bug_id} } +sub user_id { return $_[0]->{user_id} } sub last_visit_ts { return $_[0]->{last_visit_ts} } sub user { - my $self = shift; + my $self = shift; - $self->{user} //= Bugzilla::User->new({ id => $self->user_id, cache => 1 }); - return $self->{user}; + $self->{user} //= Bugzilla::User->new({id => $self->user_id, cache => 1}); + return $self->{user}; } 1; diff --git a/Bugzilla/CGI.pm b/Bugzilla/CGI.pm index 9b1ff9235..a4319be07 100644 --- a/Bugzilla/CGI.pm +++ b/Bugzilla/CGI.pm @@ -22,280 +22,301 @@ use Bugzilla::Search::Recent; use File::Basename; sub _init_bz_cgi_globals { - my $invocant = shift; - # We need to disable output buffering - see bug 179174 - $| = 1; - - # Ignore SIGTERM and SIGPIPE - this prevents DB corruption. If the user closes - # their browser window while a script is running, the web server sends these - # signals, and we don't want to die half way through a write. - $SIG{TERM} = 'IGNORE'; - $SIG{PIPE} = 'IGNORE'; - - # We don't precompile any functions here, that's done specially in - # mod_perl code. - $invocant->_setup_symbols(qw(:no_xhtml :oldstyle_urls :private_tempfiles - :unique_headers)); + my $invocant = shift; + + # We need to disable output buffering - see bug 179174 + $| = 1; + + # Ignore SIGTERM and SIGPIPE - this prevents DB corruption. If the user closes + # their browser window while a script is running, the web server sends these + # signals, and we don't want to die half way through a write. + $SIG{TERM} = 'IGNORE'; + $SIG{PIPE} = 'IGNORE'; + + # We don't precompile any functions here, that's done specially in + # mod_perl code. + $invocant->_setup_symbols( + qw(:no_xhtml :oldstyle_urls :private_tempfiles + :unique_headers) + ); } BEGIN { __PACKAGE__->_init_bz_cgi_globals() if i_am_cgi(); } sub new { - my ($invocant, @args) = @_; - my $class = ref($invocant) || $invocant; - - # Under mod_perl, CGI's global variables get reset on each request, - # so we need to set them up again every time. - $class->_init_bz_cgi_globals() if $ENV{MOD_PERL}; - - my $self = $class->SUPER::new(@args); - - # Make sure our outgoing cookie list is empty on each invocation - $self->{Bugzilla_cookie_list} = []; - - # Path-Info is of no use for Bugzilla and interacts badly with IIS. - # Moreover, it causes unexpected behaviors, such as totally breaking - # the rendering of pages. - my $script = basename($0); - if (my $path_info = $self->path_info) { - my @whitelist = ("rest.cgi"); - Bugzilla::Hook::process('path_info_whitelist', { whitelist => \@whitelist }); - if (!grep($_ eq $script, @whitelist)) { - # IIS includes the full path to the script in PATH_INFO, - # so we have to extract the real PATH_INFO from it, - # else we will be redirected outside Bugzilla. - my $script_name = $self->script_name; - $path_info =~ s/^\Q$script_name\E//; - if ($script_name && $path_info) { - print $self->redirect($self->url(-path => 0, -query => 1)); - } - } - } - - # Send appropriate charset - $self->charset(Bugzilla->params->{'utf8'} ? 'UTF-8' : ''); - - # Redirect to urlbase/sslbase if we are not viewing an attachment. - if ($self->url_is_attachment_base and $script ne 'attachment.cgi') { - $self->redirect_to_urlbase(); - } - - # Check for errors - # All of the Bugzilla code wants to do this, so do it here instead of - # in each script - - my $err = $self->cgi_error; - - if ($err) { - # Note that this error block is only triggered by CGI.pm for malformed - # multipart requests, and so should never happen unless there is a - # browser bug. - - print $self->header(-status => $err); - - # ThrowCodeError wants to print the header, so it grabs Bugzilla->cgi - # which creates a new Bugzilla::CGI object, which fails again, which - # ends up here, and calls ThrowCodeError, and then recurses forever. - # So don't use it. - # In fact, we can't use templates at all, because we need a CGI object - # to determine the template lang as well as the current url (from the - # template) - # Since this is an internal error which indicates a severe browser bug, - # just die. - die "CGI parsing error: $err"; - } - - return $self; + my ($invocant, @args) = @_; + my $class = ref($invocant) || $invocant; + + # Under mod_perl, CGI's global variables get reset on each request, + # so we need to set them up again every time. + $class->_init_bz_cgi_globals() if $ENV{MOD_PERL}; + + my $self = $class->SUPER::new(@args); + + # Make sure our outgoing cookie list is empty on each invocation + $self->{Bugzilla_cookie_list} = []; + + # Path-Info is of no use for Bugzilla and interacts badly with IIS. + # Moreover, it causes unexpected behaviors, such as totally breaking + # the rendering of pages. + my $script = basename($0); + if (my $path_info = $self->path_info) { + my @whitelist = ("rest.cgi"); + Bugzilla::Hook::process('path_info_whitelist', {whitelist => \@whitelist}); + if (!grep($_ eq $script, @whitelist)) { + + # IIS includes the full path to the script in PATH_INFO, + # so we have to extract the real PATH_INFO from it, + # else we will be redirected outside Bugzilla. + my $script_name = $self->script_name; + $path_info =~ s/^\Q$script_name\E//; + if ($script_name && $path_info) { + print $self->redirect($self->url(-path => 0, -query => 1)); + } + } + } + + # Send appropriate charset + $self->charset(Bugzilla->params->{'utf8'} ? 'UTF-8' : ''); + + # Redirect to urlbase/sslbase if we are not viewing an attachment. + if ($self->url_is_attachment_base and $script ne 'attachment.cgi') { + $self->redirect_to_urlbase(); + } + + # Check for errors + # All of the Bugzilla code wants to do this, so do it here instead of + # in each script + + my $err = $self->cgi_error; + + if ($err) { + + # Note that this error block is only triggered by CGI.pm for malformed + # multipart requests, and so should never happen unless there is a + # browser bug. + + print $self->header(-status => $err); + + # ThrowCodeError wants to print the header, so it grabs Bugzilla->cgi + # which creates a new Bugzilla::CGI object, which fails again, which + # ends up here, and calls ThrowCodeError, and then recurses forever. + # So don't use it. + # In fact, we can't use templates at all, because we need a CGI object + # to determine the template lang as well as the current url (from the + # template) + # Since this is an internal error which indicates a severe browser bug, + # just die. + die "CGI parsing error: $err"; + } + + return $self; } # We want this sorted plus the ability to exclude certain params sub canonicalise_query { - my ($self, @exclude) = @_; + my ($self, @exclude) = @_; - # Reconstruct the URL by concatenating the sorted param=value pairs - my @parameters; - foreach my $key (sort($self->param())) { - # Leave this key out if it's in the exclude list - next if grep { $_ eq $key } @exclude; + # Reconstruct the URL by concatenating the sorted param=value pairs + my @parameters; + foreach my $key (sort($self->param())) { - # Remove the Boolean Charts for standard query.cgi fields - # They are listed in the query URL already - next if $key =~ /^(field|type|value)(-\d+){3}$/; + # Leave this key out if it's in the exclude list + next if grep { $_ eq $key } @exclude; - my $esc_key = url_quote($key); + # Remove the Boolean Charts for standard query.cgi fields + # They are listed in the query URL already + next if $key =~ /^(field|type|value)(-\d+){3}$/; - foreach my $value ($self->param($key)) { - # Omit params with an empty value - if (defined($value) && $value ne '') { - my $esc_value = url_quote($value); + my $esc_key = url_quote($key); - push(@parameters, "$esc_key=$esc_value"); - } - } - } + foreach my $value ($self->param($key)) { - return join("&", @parameters); -} + # Omit params with an empty value + if (defined($value) && $value ne '') { + my $esc_value = url_quote($value); -sub clean_search_url { - my $self = shift; - # Delete any empty URL parameter. - my @cgi_params = $self->param; - - foreach my $param (@cgi_params) { - if (defined $self->param($param) && $self->param($param) eq '') { - $self->delete($param); - $self->delete("${param}_type"); - } - - # Custom Search stuff is empty if it's "noop". We also keep around - # the old Boolean Chart syntax for backwards-compatibility. - if (($param =~ /\d-\d-\d/ || $param =~ /^[[:alpha:]]\d+$/) - && defined $self->param($param) && $self->param($param) eq 'noop') - { - $self->delete($param); - } - - # Any "join" for custom search that's an AND can be removed, because - # that's the default. - if (($param =~ /^j\d+$/ || $param eq 'j_top') - && $self->param($param) eq 'AND') - { - $self->delete($param); - } + push(@parameters, "$esc_key=$esc_value"); + } } + } - # Delete leftovers from the login form - $self->delete('Bugzilla_remember', 'GoAheadAndLogIn'); + return join("&", @parameters); +} - # Delete the token if we're not performing an action which needs it - unless ((defined $self->param('remtype') - && ($self->param('remtype') eq 'asdefault' - || $self->param('remtype') eq 'asnamed')) - || (defined $self->param('remaction') - && $self->param('remaction') eq 'forget')) - { - $self->delete("token"); - } +sub clean_search_url { + my $self = shift; - foreach my $num (1,2,3) { - # If there's no value in the email field, delete the related fields. - if (!$self->param("email$num")) { - foreach my $field (qw(type assigned_to reporter qa_contact cc longdesc)) { - $self->delete("email$field$num"); - } - } - } + # Delete any empty URL parameter. + my @cgi_params = $self->param; - # chfieldto is set to "Now" by default in query.cgi. But if none - # of the other chfield parameters are set, it's meaningless. - if (!defined $self->param('chfieldfrom') && !$self->param('chfield') - && !defined $self->param('chfieldvalue') && $self->param('chfieldto') - && lc($self->param('chfieldto')) eq 'now') - { - $self->delete('chfieldto'); + foreach my $param (@cgi_params) { + if (defined $self->param($param) && $self->param($param) eq '') { + $self->delete($param); + $self->delete("${param}_type"); } - # cmdtype "doit" is the default from query.cgi, but it's only meaningful - # if there's a remtype parameter. - if (defined $self->param('cmdtype') && $self->param('cmdtype') eq 'doit' - && !defined $self->param('remtype')) + # Custom Search stuff is empty if it's "noop". We also keep around + # the old Boolean Chart syntax for backwards-compatibility. + if ( ($param =~ /\d-\d-\d/ || $param =~ /^[[:alpha:]]\d+$/) + && defined $self->param($param) + && $self->param($param) eq 'noop') { - $self->delete('cmdtype'); + $self->delete($param); } - # "Reuse same sort as last time" is actually the default, so we don't - # need it in the URL. - if ($self->param('order') - && $self->param('order') eq 'Reuse same sort as last time') + # Any "join" for custom search that's an AND can be removed, because + # that's the default. + if (($param =~ /^j\d+$/ || $param eq 'j_top') && $self->param($param) eq 'AND') { - $self->delete('order'); - } + $self->delete($param); + } + } + + # Delete leftovers from the login form + $self->delete('Bugzilla_remember', 'GoAheadAndLogIn'); + + # Delete the token if we're not performing an action which needs it + unless ( + ( + defined $self->param('remtype') + && ( $self->param('remtype') eq 'asdefault' + || $self->param('remtype') eq 'asnamed') + ) + || (defined $self->param('remaction') && $self->param('remaction') eq 'forget') + ) + { + $self->delete("token"); + } + + foreach my $num (1, 2, 3) { + + # If there's no value in the email field, delete the related fields. + if (!$self->param("email$num")) { + foreach my $field (qw(type assigned_to reporter qa_contact cc longdesc)) { + $self->delete("email$field$num"); + } + } + } + + # chfieldto is set to "Now" by default in query.cgi. But if none + # of the other chfield parameters are set, it's meaningless. + if ( !defined $self->param('chfieldfrom') + && !$self->param('chfield') + && !defined $self->param('chfieldvalue') + && $self->param('chfieldto') + && lc($self->param('chfieldto')) eq 'now') + { + $self->delete('chfieldto'); + } + + # cmdtype "doit" is the default from query.cgi, but it's only meaningful + # if there's a remtype parameter. + if ( defined $self->param('cmdtype') + && $self->param('cmdtype') eq 'doit' + && !defined $self->param('remtype')) + { + $self->delete('cmdtype'); + } + + # "Reuse same sort as last time" is actually the default, so we don't + # need it in the URL. + if ( $self->param('order') + && $self->param('order') eq 'Reuse same sort as last time') + { + $self->delete('order'); + } + + # list_id is added in buglist.cgi after calling clean_search_url, + # and doesn't need to be saved in saved searches. + $self->delete('list_id'); + + # no_redirect is used internally by redirect_search_url(). + $self->delete('no_redirect'); + + # And now finally, if query_format is our only parameter, that + # really means we have no parameters, so we should delete query_format. + if ($self->param('query_format') && scalar($self->param()) == 1) { + $self->delete('query_format'); + } +} - # list_id is added in buglist.cgi after calling clean_search_url, - # and doesn't need to be saved in saved searches. - $self->delete('list_id'); +sub check_etag { + my ($self, $valid_etag) = @_; - # no_redirect is used internally by redirect_search_url(). - $self->delete('no_redirect'); + # ETag support. + my $if_none_match = $self->http('If-None-Match'); + return if !$if_none_match; - # And now finally, if query_format is our only parameter, that - # really means we have no parameters, so we should delete query_format. - if ($self->param('query_format') && scalar($self->param()) == 1) { - $self->delete('query_format'); - } -} + my @if_none = split(/[\s,]+/, $if_none_match); + foreach my $possible_etag (@if_none) { -sub check_etag { - my ($self, $valid_etag) = @_; - - # ETag support. - my $if_none_match = $self->http('If-None-Match'); - return if !$if_none_match; - - my @if_none = split(/[\s,]+/, $if_none_match); - foreach my $possible_etag (@if_none) { - # remove quotes from begin and end of the string - $possible_etag =~ s/^\"//g; - $possible_etag =~ s/\"$//g; - if ($possible_etag eq $valid_etag or $possible_etag eq '*') { - return 1; - } + # remove quotes from begin and end of the string + $possible_etag =~ s/^\"//g; + $possible_etag =~ s/\"$//g; + if ($possible_etag eq $valid_etag or $possible_etag eq '*') { + return 1; } + } - return 0; + return 0; } # Have to add the cookies in. sub multipart_start { - my $self = shift; - - my %args = @_; - - # CGI.pm::multipart_start doesn't honour its own charset information, so - # we do it ourselves here - if (defined $self->charset() && defined $args{-type}) { - # Remove any existing charset specifier - $args{-type} =~ s/;.*$//; - # and add the specified one - $args{-type} .= '; charset=' . $self->charset(); - } - - my $headers = $self->SUPER::multipart_start(%args); - # Eliminate the one extra CRLF at the end. - $headers =~ s/$CGI::CRLF$//; - # Add the cookies. We have to do it this way instead of - # passing them to multpart_start, because CGI.pm's multipart_start - # doesn't understand a '-cookie' argument pointing to an arrayref. - foreach my $cookie (@{$self->{Bugzilla_cookie_list}}) { - $headers .= "Set-Cookie: ${cookie}${CGI::CRLF}"; - } - $headers .= $CGI::CRLF; - $self->{_multipart_in_progress} = 1; - return $headers; + my $self = shift; + + my %args = @_; + + # CGI.pm::multipart_start doesn't honour its own charset information, so + # we do it ourselves here + if (defined $self->charset() && defined $args{-type}) { + + # Remove any existing charset specifier + $args{-type} =~ s/;.*$//; + + # and add the specified one + $args{-type} .= '; charset=' . $self->charset(); + } + + my $headers = $self->SUPER::multipart_start(%args); + + # Eliminate the one extra CRLF at the end. + $headers =~ s/$CGI::CRLF$//; + + # Add the cookies. We have to do it this way instead of + # passing them to multpart_start, because CGI.pm's multipart_start + # doesn't understand a '-cookie' argument pointing to an arrayref. + foreach my $cookie (@{$self->{Bugzilla_cookie_list}}) { + $headers .= "Set-Cookie: ${cookie}${CGI::CRLF}"; + } + $headers .= $CGI::CRLF; + $self->{_multipart_in_progress} = 1; + return $headers; } sub close_standby_message { - my ($self, $contenttype, $disp, $disp_prefix, $extension) = @_; - $self->set_dated_content_disp($disp, $disp_prefix, $extension); - - if ($self->{_multipart_in_progress}) { - print $self->multipart_end(); - print $self->multipart_start(-type => $contenttype); - } - elsif (!$self->{_header_done}) { - print $self->header($contenttype); - } + my ($self, $contenttype, $disp, $disp_prefix, $extension) = @_; + $self->set_dated_content_disp($disp, $disp_prefix, $extension); + + if ($self->{_multipart_in_progress}) { + print $self->multipart_end(); + print $self->multipart_start(-type => $contenttype); + } + 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{ + 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 @@ -309,12 +330,13 @@ sub _prevent_unsafe_response { # 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{ + 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 @@ -322,373 +344,386 @@ sub _prevent_unsafe_response { (?: / | $ ) }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; + }; + + 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 %headers; - my $user = Bugzilla->user; - - # If there's only one parameter, then it's a Content-Type. - if (scalar(@_) == 1) { - %headers = ('-type' => shift(@_)); - } - else { - %headers = @_; - } - $self->_prevent_unsafe_response(\%headers); - - if ($self->{'_content_disp'}) { - $headers{'-content_disposition'} = $self->{'_content_disp'}; - } - - if (!$user->id && $user->authorizer->can_login - && !$self->cookie('Bugzilla_login_request_cookie')) - { - my %args; - $args{'-secure'} = 1 if Bugzilla->params->{ssl_redirect}; + my $self = shift; + + my %headers; + my $user = Bugzilla->user; + + # If there's only one parameter, then it's a Content-Type. + if (scalar(@_) == 1) { + %headers = ('-type' => shift(@_)); + } + else { + %headers = @_; + } + $self->_prevent_unsafe_response(\%headers); + + if ($self->{'_content_disp'}) { + $headers{'-content_disposition'} = $self->{'_content_disp'}; + } + + 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 + ); + } - $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}})) { + $headers{'-cookie'} = $self->{Bugzilla_cookie_list}; + } - # Add the cookies in if we have any - if (scalar(@{$self->{Bugzilla_cookie_list}})) { - $headers{'-cookie'} = $self->{Bugzilla_cookie_list}; + # Add Strict-Transport-Security (STS) header if this response + # is over SSL and the strict_transport_security param is turned on. + if ( $self->https + && !$self->url_is_attachment_base + && Bugzilla->params->{'strict_transport_security'} ne 'off') + { + my $sts_opts = 'max-age=' . MAX_STS_AGE; + if (Bugzilla->params->{'strict_transport_security'} eq 'include_subdomains') { + $sts_opts .= '; includeSubDomains'; } - # Add Strict-Transport-Security (STS) header if this response - # is over SSL and the strict_transport_security param is turned on. - if ($self->https && !$self->url_is_attachment_base - && Bugzilla->params->{'strict_transport_security'} ne 'off') - { - my $sts_opts = 'max-age=' . MAX_STS_AGE; - if (Bugzilla->params->{'strict_transport_security'} - eq 'include_subdomains') - { - $sts_opts .= '; includeSubDomains'; - } - - $headers{'-strict_transport_security'} = $sts_opts; - } + $headers{'-strict_transport_security'} = $sts_opts; + } - # Add X-Frame-Options header to prevent framing and subsequent - # possible clickjacking problems. - unless ($self->url_is_attachment_base) { - $headers{'-x_frame_options'} = 'SAMEORIGIN'; - } + # Add X-Frame-Options header to prevent framing and subsequent + # possible clickjacking problems. + unless ($self->url_is_attachment_base) { + $headers{'-x_frame_options'} = 'SAMEORIGIN'; + } - # Add X-XSS-Protection header to prevent simple XSS attacks - # and enforce the blocking (rather than the rewriting) mode. - $headers{'-x_xss_protection'} = '1; mode=block'; + # Add X-XSS-Protection header to prevent simple XSS attacks + # and enforce the blocking (rather than the rewriting) mode. + $headers{'-x_xss_protection'} = '1; mode=block'; - # Add X-Content-Type-Options header to prevent browsers sniffing - # the MIME type away from the declared Content-Type. - $headers{'-x_content_type_options'} = 'nosniff'; + # Add X-Content-Type-Options header to prevent browsers sniffing + # the MIME type away from the declared Content-Type. + $headers{'-x_content_type_options'} = 'nosniff'; - Bugzilla::Hook::process('cgi_headers', - { cgi => $self, headers => \%headers } - ); - $self->{_header_done} = 1; + Bugzilla::Hook::process('cgi_headers', {cgi => $self, headers => \%headers}); + $self->{_header_done} = 1; - return $self->SUPER::header(%headers) || ""; + return $self->SUPER::header(%headers) || ""; } sub param { - my $self = shift; - local $CGI::LIST_CONTEXT_WARN = 0; - - # When we are just requesting the value of a parameter... - if (scalar(@_) == 1) { - my @result = $self->SUPER::param(@_); - - # Also look at the URL parameters, after we look at the POST - # parameters. This is to allow things like login-form submissions - # with URL parameters in the form's "target" attribute. - if (!scalar(@result) - && $self->request_method && $self->request_method eq 'POST') - { - @result = $self->url_param(@_); - } - - # Fix UTF-8-ness of input parameters. - if (Bugzilla->params->{'utf8'}) { - @result = map { _fix_utf8($_) } @result; - } - - return wantarray ? @result : $result[0]; - } - # And for various other functions in CGI.pm, we need to correctly - # return the URL parameters in addition to the POST parameters when - # asked for the list of parameters. - elsif (!scalar(@_) && $self->request_method - && $self->request_method eq 'POST') + my $self = shift; + local $CGI::LIST_CONTEXT_WARN = 0; + + # When we are just requesting the value of a parameter... + if (scalar(@_) == 1) { + my @result = $self->SUPER::param(@_); + + # Also look at the URL parameters, after we look at the POST + # parameters. This is to allow things like login-form submissions + # with URL parameters in the form's "target" attribute. + if ( !scalar(@result) + && $self->request_method + && $self->request_method eq 'POST') { - my @post_params = $self->SUPER::param; - my @url_params = $self->url_param; - my %params = map { $_ => 1 } (@post_params, @url_params); - return keys %params; + @result = $self->url_param(@_); } - return $self->SUPER::param(@_); + # Fix UTF-8-ness of input parameters. + if (Bugzilla->params->{'utf8'}) { + @result = map { _fix_utf8($_) } @result; + } + + return wantarray ? @result : $result[0]; + } + + # And for various other functions in CGI.pm, we need to correctly + # return the URL parameters in addition to the POST parameters when + # asked for the list of parameters. + elsif (!scalar(@_) && $self->request_method && $self->request_method eq 'POST') + { + my @post_params = $self->SUPER::param; + my @url_params = $self->url_param; + my %params = map { $_ => 1 } (@post_params, @url_params); + return keys %params; + } + + return $self->SUPER::param(@_); } sub url_param { - my $self = shift; - # Some servers fail to set the QUERY_STRING parameter, which - # causes undef issues - $ENV{'QUERY_STRING'} //= ''; - return $self->SUPER::url_param(@_); + my $self = shift; + + # Some servers fail to set the QUERY_STRING parameter, which + # causes undef issues + $ENV{'QUERY_STRING'} //= ''; + return $self->SUPER::url_param(@_); } sub _fix_utf8 { - my $input = shift; - # The is_utf8 is here in case CGI gets smart about utf8 someday. - utf8::decode($input) if defined $input && !ref $input && !utf8::is_utf8($input); - return $input; + my $input = shift; + + # The is_utf8 is here in case CGI gets smart about utf8 someday. + utf8::decode($input) if defined $input && !ref $input && !utf8::is_utf8($input); + return $input; } sub should_set { - my ($self, $param) = @_; - my $set = (defined $self->param($param) - or defined $self->param("defined_$param")) - ? 1 : 0; - return $set; + my ($self, $param) = @_; + my $set + = (defined $self->param($param) or defined $self->param("defined_$param")) + ? 1 + : 0; + return $set; } # The various parts of Bugzilla which create cookies don't want to have to # pass them around to all of the callers. Instead, store them locally here, # and then output as required from |header|. sub send_cookie { - my $self = shift; - - # Move the param list into a hash for easier handling. - my %paramhash; - my @paramlist; - my ($key, $value); - while ($key = shift) { - $value = shift; - $paramhash{$key} = $value; - } - - # Complain if -value is not given or empty (bug 268146). - if (!exists($paramhash{'-value'}) || !$paramhash{'-value'}) { - ThrowCodeError('cookies_need_value'); - } - - # Add the default path and the domain in. - $paramhash{'-path'} = Bugzilla->params->{'cookiepath'}; - $paramhash{'-domain'} = Bugzilla->params->{'cookiedomain'} - if Bugzilla->params->{'cookiedomain'}; - - # Move the param list back into an array for the call to cookie(). - foreach (keys(%paramhash)) { - unshift(@paramlist, $_ => $paramhash{$_}); - } - - push(@{$self->{'Bugzilla_cookie_list'}}, $self->cookie(@paramlist)); + my $self = shift; + + # Move the param list into a hash for easier handling. + my %paramhash; + my @paramlist; + my ($key, $value); + while ($key = shift) { + $value = shift; + $paramhash{$key} = $value; + } + + # Complain if -value is not given or empty (bug 268146). + if (!exists($paramhash{'-value'}) || !$paramhash{'-value'}) { + ThrowCodeError('cookies_need_value'); + } + + # Add the default path and the domain in. + $paramhash{'-path'} = Bugzilla->params->{'cookiepath'}; + $paramhash{'-domain'} = Bugzilla->params->{'cookiedomain'} + if Bugzilla->params->{'cookiedomain'}; + + # Move the param list back into an array for the call to cookie(). + foreach (keys(%paramhash)) { + unshift(@paramlist, $_ => $paramhash{$_}); + } + + push(@{$self->{'Bugzilla_cookie_list'}}, $self->cookie(@paramlist)); } # Cookies are removed by setting an expiry date in the past. # This method is a send_cookie wrapper doing exactly this. sub remove_cookie { - my $self = shift; - my ($cookiename) = (@_); - - # Expire the cookie, giving a non-empty dummy value (bug 268146). - $self->send_cookie('-name' => $cookiename, - '-expires' => 'Tue, 15-Sep-1998 21:49:00 GMT', - '-value' => 'X'); + my $self = shift; + my ($cookiename) = (@_); + + # Expire the cookie, giving a non-empty dummy value (bug 268146). + $self->send_cookie( + '-name' => $cookiename, + '-expires' => 'Tue, 15-Sep-1998 21:49:00 GMT', + '-value' => 'X' + ); } # This helps implement Bugzilla::Search::Recent, and also shortens search # URLs that get POSTed to buglist.cgi. sub redirect_search_url { - my $self = shift; - - # If there is no parameter, there is nothing to do. - return unless $self->param; - - # If we're retreiving an old list, we never need to redirect or - # do anything related to Bugzilla::Search::Recent. - return if $self->param('regetlastlist'); - - my $user = Bugzilla->user; - - if ($user->id) { - # There are two conditions that could happen here--we could get a URL - # with no list id, and we could get a URL with a list_id that isn't - # ours. - my $list_id = $self->param('list_id'); - if ($list_id) { - # If we have a valid list_id, no need to redirect or clean. - return if Bugzilla::Search::Recent->check_quietly( - { id => $list_id }); - } - } - elsif ($self->request_method ne 'POST') { - # Logged-out users who do a GET don't get a list_id, don't get - # their URLs cleaned, and don't get redirected. - return; - } + my $self = shift; - my $no_redirect = $self->param('no_redirect'); - $self->clean_search_url(); - - # Make sure we still have params still after cleaning otherwise we - # do not want to store a list_id for an empty search. - if ($user->id && $self->param) { - # Insert a placeholder Bugzilla::Search::Recent, so that we know what - # the id of the resulting search will be. This is then pulled out - # of the Referer header when viewing show_bug.cgi to know what - # bug list we came from. - my $recent_search = Bugzilla::Search::Recent->create_placeholder; - $self->param('list_id', $recent_search->id); - } + # If there is no parameter, there is nothing to do. + return unless $self->param; + + # If we're retreiving an old list, we never need to redirect or + # do anything related to Bugzilla::Search::Recent. + return if $self->param('regetlastlist'); + + my $user = Bugzilla->user; + + if ($user->id) { - # Browsers which support history.replaceState do not need to be - # redirected. We can fix the URL on the fly. - return if $no_redirect; + # There are two conditions that could happen here--we could get a URL + # with no list id, and we could get a URL with a list_id that isn't + # ours. + my $list_id = $self->param('list_id'); + if ($list_id) { - # 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 $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; + # If we have a valid list_id, no need to redirect or clean. + return if Bugzilla::Search::Recent->check_quietly({id => $list_id}); } + } + elsif ($self->request_method ne 'POST') { + + # Logged-out users who do a GET don't get a list_id, don't get + # their URLs cleaned, and don't get redirected. + return; + } + + my $no_redirect = $self->param('no_redirect'); + $self->clean_search_url(); + + # Make sure we still have params still after cleaning otherwise we + # do not want to store a list_id for an empty search. + if ($user->id && $self->param) { + + # Insert a placeholder Bugzilla::Search::Recent, so that we know what + # the id of the resulting search will be. This is then pulled out + # of the Referer header when viewing show_bug.cgi to know what + # bug list we came from. + my $recent_search = Bugzilla::Search::Recent->create_placeholder; + $self->param('list_id', $recent_search->id); + } + + # Browsers which support history.replaceState do not need to be + # redirected. We can fix the URL on the fly. + return if $no_redirect; + + # 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 $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; + } } sub redirect_to_https { - my $self = shift; - my $sslbase = Bugzilla->params->{'sslbase'}; - # If this is a POST, we don't want ?POSTDATA in the query string. - # We expect the client to re-POST, which may be a violation of - # the HTTP spec, but the only time we're expecting it often is - # in the WebService, and WebService clients usually handle this - # correctly. - $self->delete('POSTDATA'); - my $url = $sslbase . $self->url('-path_info' => 1, '-query' => 1, - '-relative' => 1); - - # XML-RPC clients (SOAP::Lite at least) require a 301 to redirect properly - # and do not work with 302. Our redirect really is permanent anyhow, so - # it doesn't hurt to make it a 301. - print $self->redirect(-location => $url, -status => 301); - - # When using XML-RPC with mod_perl, we need the headers sent immediately. - $self->r->rflush if $ENV{MOD_PERL}; - exit; + my $self = shift; + my $sslbase = Bugzilla->params->{'sslbase'}; + + # If this is a POST, we don't want ?POSTDATA in the query string. + # We expect the client to re-POST, which may be a violation of + # the HTTP spec, but the only time we're expecting it often is + # in the WebService, and WebService clients usually handle this + # correctly. + $self->delete('POSTDATA'); + my $url + = $sslbase . $self->url('-path_info' => 1, '-query' => 1, '-relative' => 1); + + # XML-RPC clients (SOAP::Lite at least) require a 301 to redirect properly + # and do not work with 302. Our redirect really is permanent anyhow, so + # it doesn't hurt to make it a 301. + print $self->redirect(-location => $url, -status => 301); + + # When using XML-RPC with mod_perl, we need the headers sent immediately. + $self->r->rflush if $ENV{MOD_PERL}; + exit; } # Redirect to the urlbase version of the current URL. sub redirect_to_urlbase { - my $self = shift; - my $path = $self->url('-path_info' => 1, '-query' => 1, '-relative' => 1); - print $self->redirect('-location' => correct_urlbase() . $path); - exit; + my $self = shift; + my $path = $self->url('-path_info' => 1, '-query' => 1, '-relative' => 1); + print $self->redirect('-location' => correct_urlbase() . $path); + exit; } sub url_is_attachment_base { - my ($self, $id) = @_; - return 0 if !use_attachbase() or !i_am_cgi(); - my $attach_base = Bugzilla->params->{'attachment_base'}; - # If we're passed an id, we only want one specific attachment base - # for a particular bug. If we're not passed an ID, we just want to - # know if our current URL matches the attachment_base *pattern*. - my $regex; - if ($id) { - $attach_base =~ s/\%bugid\%/$id/; - $regex = quotemeta($attach_base); - } - else { - # In this circumstance we run quotemeta first because we need to - # insert an active regex meta-character afterward. - $regex = quotemeta($attach_base); - $regex =~ s/\\\%bugid\\\%/\\d+/; - } - $regex = "^$regex"; - return ($self->url =~ $regex) ? 1 : 0; + my ($self, $id) = @_; + return 0 if !use_attachbase() or !i_am_cgi(); + my $attach_base = Bugzilla->params->{'attachment_base'}; + + # If we're passed an id, we only want one specific attachment base + # for a particular bug. If we're not passed an ID, we just want to + # know if our current URL matches the attachment_base *pattern*. + my $regex; + if ($id) { + $attach_base =~ s/\%bugid\%/$id/; + $regex = quotemeta($attach_base); + } + else { + # In this circumstance we run quotemeta first because we need to + # insert an active regex meta-character afterward. + $regex = quotemeta($attach_base); + $regex =~ s/\\\%bugid\\\%/\\d+/; + } + $regex = "^$regex"; + return ($self->url =~ $regex) ? 1 : 0; } sub set_dated_content_disp { - my ($self, $type, $prefix, $ext) = @_; + my ($self, $type, $prefix, $ext) = @_; - my @time = localtime(time()); - my $date = sprintf "%04d-%02d-%02d", 1900+$time[5], $time[4]+1, $time[3]; - my $filename = "$prefix-$date.$ext"; + my @time = localtime(time()); + my $date = sprintf "%04d-%02d-%02d", 1900 + $time[5], $time[4] + 1, $time[3]; + my $filename = "$prefix-$date.$ext"; - $filename =~ s/\s/_/g; # Remove whitespace to avoid HTTP header tampering - $filename =~ s/\\/_/g; # Remove backslashes as well - $filename =~ s/"/\\"/g; # escape quotes + $filename =~ s/\s/_/g; # Remove whitespace to avoid HTTP header tampering + $filename =~ s/\\/_/g; # Remove backslashes as well + $filename =~ s/"/\\"/g; # escape quotes - my $disposition = "$type; filename=\"$filename\""; + my $disposition = "$type; filename=\"$filename\""; - $self->{'_content_disp'} = $disposition; + $self->{'_content_disp'} = $disposition; } ########################## # Vars TIEHASH Interface # ########################## -# Fix the TIEHASH interface (scalar $cgi->Vars) to return and accept +# Fix the TIEHASH interface (scalar $cgi->Vars) to return and accept # arrayrefs. sub STORE { - my $self = shift; - my ($param, $value) = @_; - if (defined $value and ref $value eq 'ARRAY') { - return $self->param(-name => $param, -value => $value); - } - return $self->SUPER::STORE(@_); + my $self = shift; + my ($param, $value) = @_; + if (defined $value and ref $value eq 'ARRAY') { + return $self->param(-name => $param, -value => $value); + } + return $self->SUPER::STORE(@_); } sub FETCH { - my ($self, $param) = @_; - return $self if $param eq 'CGI'; # CGI.pm did this, so we do too. - my @result = $self->param($param); - return undef if !scalar(@result); - return $result[0] if scalar(@result) == 1; - return \@result; + my ($self, $param) = @_; + return $self if $param eq 'CGI'; # CGI.pm did this, so we do too. + my @result = $self->param($param); + return undef if !scalar(@result); + return $result[0] if scalar(@result) == 1; + return \@result; } -# For the Vars TIEHASH interface: the normal CGI.pm DELETE doesn't return +# For the Vars TIEHASH interface: the normal CGI.pm DELETE doesn't return # the value deleted, but Perl's "delete" expects that value. sub DELETE { - my ($self, $param) = @_; - my $value = $self->FETCH($param); - $self->delete($param); - return $value; + my ($self, $param) = @_; + my $value = $self->FETCH($param); + $self->delete($param); + return $value; } 1; diff --git a/Bugzilla/Chart.pm b/Bugzilla/Chart.pm index 3c69006aa..3aee1aafb 100644 --- a/Bugzilla/Chart.pm +++ b/Bugzilla/Chart.pm @@ -26,405 +26,424 @@ use Date::Parse; use List::Util qw(max); sub new { - my $invocant = shift; - my $class = ref($invocant) || $invocant; - - # Create a ref to an empty hash and bless it - my $self = {}; - bless($self, $class); - - if ($#_ == 0) { - # Construct from a CGI object. - $self->init($_[0]); - } - else { - die("CGI object not passed in - invalid number of args \($#_\)($_)"); - } + my $invocant = shift; + my $class = ref($invocant) || $invocant; + + # Create a ref to an empty hash and bless it + my $self = {}; + bless($self, $class); + + if ($#_ == 0) { - return $self; + # Construct from a CGI object. + $self->init($_[0]); + } + else { + die("CGI object not passed in - invalid number of args \($#_\)($_)"); + } + + return $self; } sub init { - my $self = shift; - my $cgi = shift; - - # The data structure is a list of lists (lines) of Series objects. - # There is a separate list for the labels. - # - # The URL encoding is: - # line0=67&line0=73&line1=81&line2=67... - # &label0=B+/+R+/+CONFIRMED&label1=... - # &select0=1&select3=1... - # &cumulate=1&datefrom=2002-02-03&dateto=2002-04-04&ctype=html... - # >=1&labelgt=Grand+Total - foreach my $param ($cgi->param()) { - # Store all the lines - if ($param =~ /^line(\d+)$/) { - foreach my $series_id ($cgi->param($param)) { - detaint_natural($series_id) - || ThrowCodeError("invalid_series_id"); - my $series = new Bugzilla::Series($series_id); - push(@{$self->{'lines'}[$1]}, $series) if $series; - } - } - - # Store all the labels - if ($param =~ /^label(\d+)$/) { - $self->{'labels'}[$1] = $cgi->param($param); - } - } - - # Store the miscellaneous metadata - $self->{'cumulate'} = $cgi->param('cumulate') ? 1 : 0; - $self->{'gt'} = $cgi->param('gt') ? 1 : 0; - $self->{'labelgt'} = $cgi->param('labelgt'); - $self->{'datefrom'} = $cgi->param('datefrom'); - $self->{'dateto'} = $cgi->param('dateto'); - - # If we are cumulating, a grand total makes no sense - $self->{'gt'} = 0 if $self->{'cumulate'}; - - # Make sure the dates are ones we are able to interpret - foreach my $date ('datefrom', 'dateto') { - if ($self->{$date}) { - $self->{$date} = str2time($self->{$date}) - || ThrowUserError("illegal_date", { date => $self->{$date}}); - } + my $self = shift; + my $cgi = shift; + + # The data structure is a list of lists (lines) of Series objects. + # There is a separate list for the labels. + # + # The URL encoding is: + # line0=67&line0=73&line1=81&line2=67... + # &label0=B+/+R+/+CONFIRMED&label1=... + # &select0=1&select3=1... + # &cumulate=1&datefrom=2002-02-03&dateto=2002-04-04&ctype=html... + # >=1&labelgt=Grand+Total + foreach my $param ($cgi->param()) { + + # Store all the lines + if ($param =~ /^line(\d+)$/) { + foreach my $series_id ($cgi->param($param)) { + detaint_natural($series_id) || ThrowCodeError("invalid_series_id"); + my $series = new Bugzilla::Series($series_id); + push(@{$self->{'lines'}[$1]}, $series) if $series; + } } - # datefrom can't be after dateto - if ($self->{'datefrom'} && $self->{'dateto'} && - $self->{'datefrom'} > $self->{'dateto'}) - { - ThrowUserError('misarranged_dates', { 'datefrom' => scalar $cgi->param('datefrom'), - 'dateto' => scalar $cgi->param('dateto') }); + # Store all the labels + if ($param =~ /^label(\d+)$/) { + $self->{'labels'}[$1] = $cgi->param($param); } + } + + # Store the miscellaneous metadata + $self->{'cumulate'} = $cgi->param('cumulate') ? 1 : 0; + $self->{'gt'} = $cgi->param('gt') ? 1 : 0; + $self->{'labelgt'} = $cgi->param('labelgt'); + $self->{'datefrom'} = $cgi->param('datefrom'); + $self->{'dateto'} = $cgi->param('dateto'); + + # If we are cumulating, a grand total makes no sense + $self->{'gt'} = 0 if $self->{'cumulate'}; + + # Make sure the dates are ones we are able to interpret + foreach my $date ('datefrom', 'dateto') { + if ($self->{$date}) { + $self->{$date} = str2time($self->{$date}) + || ThrowUserError("illegal_date", {date => $self->{$date}}); + } + } + + # datefrom can't be after dateto + if ( $self->{'datefrom'} + && $self->{'dateto'} + && $self->{'datefrom'} > $self->{'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. sub add { - my $self = shift; - my @series_ids = @_; - - # Get the current size of the series; required for adding Grand Total later - my $current_size = scalar($self->getSeriesIDs()); - - # Count the number of added series - my $added = 0; - # Create new Series and push them on to the list of lines. - # Note that new lines have no label; the display template is responsible - # for inventing something sensible. - foreach my $series_id (@series_ids) { - my $series = new Bugzilla::Series($series_id); - if ($series) { - push(@{$self->{'lines'}}, [$series]); - push(@{$self->{'labels'}}, ""); - $added++; - } + my $self = shift; + my @series_ids = @_; + + # Get the current size of the series; required for adding Grand Total later + my $current_size = scalar($self->getSeriesIDs()); + + # Count the number of added series + my $added = 0; + + # Create new Series and push them on to the list of lines. + # Note that new lines have no label; the display template is responsible + # for inventing something sensible. + foreach my $series_id (@series_ids) { + my $series = new Bugzilla::Series($series_id); + if ($series) { + push(@{$self->{'lines'}}, [$series]); + push(@{$self->{'labels'}}, ""); + $added++; } - - # If we are going from < 2 to >= 2 series, add the Grand Total line. - if (!$self->{'gt'}) { - if ($current_size < 2 && - $current_size + $added >= 2) - { - $self->{'gt'} = 1; - } + } + + # If we are going from < 2 to >= 2 series, add the Grand Total line. + if (!$self->{'gt'}) { + if ($current_size < 2 && $current_size + $added >= 2) { + $self->{'gt'} = 1; } + } } # Alter Chart so that the selections are removed from it. sub remove { - my $self = shift; - my @line_ids = @_; - - foreach my $line_id (@line_ids) { - if ($line_id == 65536) { - # Magic value - delete Grand Total. - $self->{'gt'} = 0; - } - else { - delete($self->{'lines'}->[$line_id]); - delete($self->{'labels'}->[$line_id]); - } + my $self = shift; + my @line_ids = @_; + + foreach my $line_id (@line_ids) { + if ($line_id == 65536) { + + # Magic value - delete Grand Total. + $self->{'gt'} = 0; + } + else { + delete($self->{'lines'}->[$line_id]); + delete($self->{'labels'}->[$line_id]); } + } } # Alter Chart so that the selections are summed. sub sum { - my $self = shift; - my @line_ids = @_; - - # We can't add the Grand Total to things. - @line_ids = grep(!/^65536$/, @line_ids); - - # We can't add less than two things. - return if scalar(@line_ids) < 2; - - my @series; - my $label = ""; - my $biggestlength = 0; - - # We rescue the Series objects of all the series involved in the sum. - foreach my $line_id (@line_ids) { - my @line = @{$self->{'lines'}->[$line_id]}; - - foreach my $series (@line) { - push(@series, $series); - } - - # We keep the label that labels the line with the most series. - if (scalar(@line) > $biggestlength) { - $biggestlength = scalar(@line); - $label = $self->{'labels'}->[$line_id]; - } + my $self = shift; + my @line_ids = @_; + + # We can't add the Grand Total to things. + @line_ids = grep(!/^65536$/, @line_ids); + + # We can't add less than two things. + return if scalar(@line_ids) < 2; + + my @series; + my $label = ""; + my $biggestlength = 0; + + # We rescue the Series objects of all the series involved in the sum. + foreach my $line_id (@line_ids) { + my @line = @{$self->{'lines'}->[$line_id]}; + + foreach my $series (@line) { + push(@series, $series); + } + + # We keep the label that labels the line with the most series. + if (scalar(@line) > $biggestlength) { + $biggestlength = scalar(@line); + $label = $self->{'labels'}->[$line_id]; } + } - $self->remove(@line_ids); + $self->remove(@line_ids); - push(@{$self->{'lines'}}, \@series); - push(@{$self->{'labels'}}, $label); + push(@{$self->{'lines'}}, \@series); + push(@{$self->{'labels'}}, $label); } sub data { - my $self = shift; - $self->{'_data'} ||= $self->readData(); - return $self->{'_data'}; + my $self = shift; + $self->{'_data'} ||= $self->readData(); + return $self->{'_data'}; } # Convert the Chart's data into a plottable form in $self->{'_data'}. sub readData { - my $self = shift; - my @data; - my @maxvals; - - # Note: you get a bad image if getSeriesIDs returns nothing - # We need to handle errors better. - my $series_ids = join(",", $self->getSeriesIDs()); - - return [] unless $series_ids; - - # Work out the date boundaries for our data. - my $dbh = Bugzilla->dbh; - - # The date used is the one given if it's in a sensible range; otherwise, - # it's the earliest or latest date in the database as appropriate. - my $datefrom = $dbh->selectrow_array("SELECT MIN(series_date) " . - "FROM series_data " . - "WHERE series_id IN ($series_ids)"); - $datefrom = str2time($datefrom); - - if ($self->{'datefrom'} && $self->{'datefrom'} > $datefrom) { - $datefrom = $self->{'datefrom'}; - } - - my $dateto = $dbh->selectrow_array("SELECT MAX(series_date) " . - "FROM series_data " . - "WHERE series_id IN ($series_ids)"); - $dateto = str2time($dateto); - - if ($self->{'dateto'} && $self->{'dateto'} < $dateto) { - $dateto = $self->{'dateto'}; - } - - # Convert UNIX times back to a date format usable for SQL queries. - my $sql_from = time2str('%Y-%m-%d', $datefrom); - my $sql_to = time2str('%Y-%m-%d', $dateto); - - # Prepare the query which retrieves the data for each series - my $query = "SELECT " . $dbh->sql_to_days('series_date') . " - " . - $dbh->sql_to_days('?') . ", series_value " . - "FROM series_data " . - "WHERE series_id = ? " . - "AND series_date >= ?"; - if ($dateto) { - $query .= " AND series_date <= ?"; - } - - my $sth = $dbh->prepare($query); - - my $gt_index = $self->{'gt'} ? scalar(@{$self->{'lines'}}) : undef; - my $line_index = 0; - - $maxvals[$gt_index] = 0 if $gt_index; - - my @datediff_total; - - foreach my $line (@{$self->{'lines'}}) { - # Even if we end up with no data, we need an empty arrayref to prevent - # errors in the PNG-generating code - $data[$line_index] = []; - $maxvals[$line_index] = 0; - - foreach my $series (@$line) { - - # Get the data for this series and add it on - if ($dateto) { - $sth->execute($sql_from, $series->{'series_id'}, $sql_from, $sql_to); - } - else { - $sth->execute($sql_from, $series->{'series_id'}, $sql_from); - } - my $points = $sth->fetchall_arrayref(); - - foreach my $point (@$points) { - my ($datediff, $value) = @$point; - $data[$line_index][$datediff] ||= 0; - $data[$line_index][$datediff] += $value; - if ($data[$line_index][$datediff] > $maxvals[$line_index]) { - $maxvals[$line_index] = $data[$line_index][$datediff]; - } - - $datediff_total[$datediff] += $value; - - # Add to the grand total, if we are doing that - if ($gt_index) { - $data[$gt_index][$datediff] ||= 0; - $data[$gt_index][$datediff] += $value; - if ($data[$gt_index][$datediff] > $maxvals[$gt_index]) { - $maxvals[$gt_index] = $data[$gt_index][$datediff]; - } - } - } + my $self = shift; + my @data; + my @maxvals; + + # Note: you get a bad image if getSeriesIDs returns nothing + # We need to handle errors better. + my $series_ids = join(",", $self->getSeriesIDs()); + + return [] unless $series_ids; + + # Work out the date boundaries for our data. + my $dbh = Bugzilla->dbh; + + # The date used is the one given if it's in a sensible range; otherwise, + # it's the earliest or latest date in the database as appropriate. + my $datefrom + = $dbh->selectrow_array("SELECT MIN(series_date) " + . "FROM series_data " + . "WHERE series_id IN ($series_ids)"); + $datefrom = str2time($datefrom); + + if ($self->{'datefrom'} && $self->{'datefrom'} > $datefrom) { + $datefrom = $self->{'datefrom'}; + } + + my $dateto + = $dbh->selectrow_array("SELECT MAX(series_date) " + . "FROM series_data " + . "WHERE series_id IN ($series_ids)"); + $dateto = str2time($dateto); + + if ($self->{'dateto'} && $self->{'dateto'} < $dateto) { + $dateto = $self->{'dateto'}; + } + + # Convert UNIX times back to a date format usable for SQL queries. + my $sql_from = time2str('%Y-%m-%d', $datefrom); + my $sql_to = time2str('%Y-%m-%d', $dateto); + + # Prepare the query which retrieves the data for each series + my $query + = "SELECT " + . $dbh->sql_to_days('series_date') . " - " + . $dbh->sql_to_days('?') + . ", series_value " + . "FROM series_data " + . "WHERE series_id = ? " + . "AND series_date >= ?"; + if ($dateto) { + $query .= " AND series_date <= ?"; + } + + my $sth = $dbh->prepare($query); + + my $gt_index = $self->{'gt'} ? scalar(@{$self->{'lines'}}) : undef; + my $line_index = 0; + + $maxvals[$gt_index] = 0 if $gt_index; + + my @datediff_total; + + foreach my $line (@{$self->{'lines'}}) { + + # Even if we end up with no data, we need an empty arrayref to prevent + # errors in the PNG-generating code + $data[$line_index] = []; + $maxvals[$line_index] = 0; + + foreach my $series (@$line) { + + # Get the data for this series and add it on + if ($dateto) { + $sth->execute($sql_from, $series->{'series_id'}, $sql_from, $sql_to); + } + else { + $sth->execute($sql_from, $series->{'series_id'}, $sql_from); + } + my $points = $sth->fetchall_arrayref(); + + foreach my $point (@$points) { + my ($datediff, $value) = @$point; + $data[$line_index][$datediff] ||= 0; + $data[$line_index][$datediff] += $value; + if ($data[$line_index][$datediff] > $maxvals[$line_index]) { + $maxvals[$line_index] = $data[$line_index][$datediff]; } - # We are done with the series making up this line, go to the next one - $line_index++; - } + $datediff_total[$datediff] += $value; - # calculate maximum y value - if ($self->{'cumulate'}) { - # Make sure we do not try to take the max of an array with undef values - my @processed_datediff; - while (@datediff_total) { - my $datediff = shift @datediff_total; - push @processed_datediff, $datediff if defined($datediff); + # Add to the grand total, if we are doing that + if ($gt_index) { + $data[$gt_index][$datediff] ||= 0; + $data[$gt_index][$datediff] += $value; + if ($data[$gt_index][$datediff] > $maxvals[$gt_index]) { + $maxvals[$gt_index] = $data[$gt_index][$datediff]; + } } - $self->{'y_max_value'} = max(@processed_datediff); - } - else { - $self->{'y_max_value'} = max(@maxvals); - } - $self->{'y_max_value'} |= 1; # For log() - - # Align the max y value: - # For one- or two-digit numbers, increase y_max_value until divisible by 8 - # For larger numbers, see the comments below to figure out what's going on - if ($self->{'y_max_value'} < 100) { - do { - ++$self->{'y_max_value'}; - } while ($self->{'y_max_value'} % 8 != 0); - } - else { - # First, get the # of digits in the y_max_value - my $num_digits = 1+int(log($self->{'y_max_value'})/log(10)); - - # We want to zero out all but the top 2 digits - my $mask_length = $num_digits - 2; - $self->{'y_max_value'} /= 10**$mask_length; - $self->{'y_max_value'} = int($self->{'y_max_value'}); - $self->{'y_max_value'} *= 10**$mask_length; - - # Add 10^$mask_length to the max value - # Continue to increase until it's divisible by 8 * 10^($mask_length-1) - # (Throwing in the -1 keeps at least the smallest digit at zero) - do { - $self->{'y_max_value'} += 10**$mask_length; - } while ($self->{'y_max_value'} % (8*(10**($mask_length-1))) != 0); + } } - - # Add the x-axis labels into the data structure - my $date_progression = generateDateProgression($datefrom, $dateto); - unshift(@data, $date_progression); + # We are done with the series making up this line, go to the next one + $line_index++; + } - if ($self->{'gt'}) { - # Add Grand Total to label list - push(@{$self->{'labels'}}, $self->{'labelgt'}); + # calculate maximum y value + if ($self->{'cumulate'}) { - $data[$gt_index] ||= []; + # Make sure we do not try to take the max of an array with undef values + my @processed_datediff; + while (@datediff_total) { + my $datediff = shift @datediff_total; + push @processed_datediff, $datediff if defined($datediff); } - - return \@data; + $self->{'y_max_value'} = max(@processed_datediff); + } + else { + $self->{'y_max_value'} = max(@maxvals); + } + $self->{'y_max_value'} |= 1; # For log() + + # Align the max y value: + # For one- or two-digit numbers, increase y_max_value until divisible by 8 + # For larger numbers, see the comments below to figure out what's going on + if ($self->{'y_max_value'} < 100) { + do { + ++$self->{'y_max_value'}; + } while ($self->{'y_max_value'} % 8 != 0); + } + else { + # First, get the # of digits in the y_max_value + my $num_digits = 1 + int(log($self->{'y_max_value'}) / log(10)); + + # We want to zero out all but the top 2 digits + my $mask_length = $num_digits - 2; + $self->{'y_max_value'} /= 10**$mask_length; + $self->{'y_max_value'} = int($self->{'y_max_value'}); + $self->{'y_max_value'} *= 10**$mask_length; + + # Add 10^$mask_length to the max value + # Continue to increase until it's divisible by 8 * 10^($mask_length-1) + # (Throwing in the -1 keeps at least the smallest digit at zero) + do { + $self->{'y_max_value'} += 10**$mask_length; + } while ($self->{'y_max_value'} % (8 * (10**($mask_length - 1))) != 0); + } + + + # Add the x-axis labels into the data structure + my $date_progression = generateDateProgression($datefrom, $dateto); + unshift(@data, $date_progression); + + if ($self->{'gt'}) { + + # Add Grand Total to label list + push(@{$self->{'labels'}}, $self->{'labelgt'}); + + $data[$gt_index] ||= []; + } + + return \@data; } # Flatten the data structure into a list of series_ids sub getSeriesIDs { - my $self = shift; - my @series_ids; + my $self = shift; + my @series_ids; - foreach my $line (@{$self->{'lines'}}) { - foreach my $series (@$line) { - push(@series_ids, $series->{'series_id'}); - } + foreach my $line (@{$self->{'lines'}}) { + foreach my $series (@$line) { + push(@series_ids, $series->{'series_id'}); } + } - return @series_ids; + return @series_ids; } # Class method to get the data necessary to populate the "select series" # widgets on various pages. sub getVisibleSeries { - my %cats; - - my $grouplist = Bugzilla->user->groups_as_string; - - # Get all visible series - my $dbh = Bugzilla->dbh; - my $serieses = $dbh->selectall_arrayref("SELECT cc1.name, cc2.name, " . - "series.name, series.series_id " . - "FROM series " . - "INNER JOIN series_categories AS cc1 " . - " ON series.category = cc1.id " . - "INNER JOIN series_categories AS cc2 " . - " ON series.subcategory = cc2.id " . - "LEFT JOIN category_group_map AS cgm " . - " ON series.category = cgm.category_id " . - " AND cgm.group_id NOT IN($grouplist) " . - "WHERE creator = ? OR (is_public = 1 AND cgm.category_id IS NULL) " . - $dbh->sql_group_by('series.series_id', 'cc1.name, cc2.name, ' . - 'series.name'), - undef, Bugzilla->user->id); - foreach my $series (@$serieses) { - my ($cat, $subcat, $name, $series_id) = @$series; - $cats{$cat}{$subcat}{$name} = $series_id; - } - - return \%cats; + my %cats; + + my $grouplist = Bugzilla->user->groups_as_string; + + # Get all visible series + my $dbh = Bugzilla->dbh; + my $serieses = $dbh->selectall_arrayref( + "SELECT cc1.name, cc2.name, " + . "series.name, series.series_id " + . "FROM series " + . "INNER JOIN series_categories AS cc1 " + . " ON series.category = cc1.id " + . "INNER JOIN series_categories AS cc2 " + . " ON series.subcategory = cc2.id " + . "LEFT JOIN category_group_map AS cgm " + . " ON series.category = cgm.category_id " + . " AND cgm.group_id NOT IN($grouplist) " + . "WHERE creator = ? OR (is_public = 1 AND cgm.category_id IS NULL) " + . $dbh->sql_group_by( + 'series.series_id', 'cc1.name, cc2.name, ' . 'series.name' + ), + undef, + Bugzilla->user->id + ); + foreach my $series (@$serieses) { + my ($cat, $subcat, $name, $series_id) = @$series; + $cats{$cat}{$subcat}{$name} = $series_id; + } + + return \%cats; } sub generateDateProgression { - my ($datefrom, $dateto) = @_; - my @progression; - - $dateto = $dateto || time(); - my $oneday = 60 * 60 * 24; - - # When the from and to dates are converted by str2time(), you end up with - # a time figure representing midnight at the beginning of that day. We - # adjust the times by 1/3 and 2/3 of a day respectively to prevent - # edge conditions in time2str(). - $datefrom += $oneday / 3; - $dateto += (2 * $oneday) / 3; - - while ($datefrom < $dateto) { - push (@progression, time2str("%Y-%m-%d", $datefrom)); - $datefrom += $oneday; - } + my ($datefrom, $dateto) = @_; + my @progression; + + $dateto = $dateto || time(); + my $oneday = 60 * 60 * 24; + + # When the from and to dates are converted by str2time(), you end up with + # a time figure representing midnight at the beginning of that day. We + # adjust the times by 1/3 and 2/3 of a day respectively to prevent + # edge conditions in time2str(). + $datefrom += $oneday / 3; + $dateto += (2 * $oneday) / 3; - return \@progression; + while ($datefrom < $dateto) { + push(@progression, time2str("%Y-%m-%d", $datefrom)); + $datefrom += $oneday; + } + + return \@progression; } sub dump { - my $self = shift; - - # 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 "
"; + my $self = shift; + + # 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 "
"; } 1; diff --git a/Bugzilla/Classification.pm b/Bugzilla/Classification.pm index 09f71baaf..1ea86f592 100644 --- a/Bugzilla/Classification.pm +++ b/Bugzilla/Classification.pm @@ -26,26 +26,26 @@ use parent qw(Bugzilla::Field::ChoiceInterface Bugzilla::Object Exporter); use constant IS_CONFIG => 1; -use constant DB_TABLE => 'classifications'; +use constant DB_TABLE => 'classifications'; use constant LIST_ORDER => 'sortkey, name'; use constant DB_COLUMNS => qw( - id - name - description - sortkey + id + name + description + sortkey ); use constant UPDATE_COLUMNS => qw( - name - description - sortkey + name + description + sortkey ); use constant VALIDATORS => { - name => \&_check_name, - description => \&_check_description, - sortkey => \&_check_sortkey, + name => \&_check_name, + description => \&_check_description, + sortkey => \&_check_sortkey, }; ############################### @@ -53,29 +53,31 @@ use constant VALIDATORS => { ############################### sub remove_from_db { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - ThrowUserError("classification_not_deletable") if ($self->id == 1); + ThrowUserError("classification_not_deletable") if ($self->id == 1); - $dbh->bz_start_transaction(); + $dbh->bz_start_transaction(); - # Reclassify products to the default classification, if needed. - my $product_ids = $dbh->selectcol_arrayref( - 'SELECT id FROM products WHERE classification_id = ?', undef, $self->id); - - if (@$product_ids) { - $dbh->do('UPDATE products SET classification_id = 1 WHERE ' - . $dbh->sql_in('id', $product_ids)); - foreach my $id (@$product_ids) { - Bugzilla->memcached->clear({ table => 'products', id => $id }); - } - Bugzilla->memcached->clear_config(); + # Reclassify products to the default classification, if needed. + my $product_ids + = $dbh->selectcol_arrayref( + 'SELECT id FROM products WHERE classification_id = ?', + undef, $self->id); + + if (@$product_ids) { + $dbh->do('UPDATE products SET classification_id = 1 WHERE ' + . $dbh->sql_in('id', $product_ids)); + foreach my $id (@$product_ids) { + Bugzilla->memcached->clear({table => 'products', id => $id}); } + Bugzilla->memcached->clear_config(); + } - $self->SUPER::remove_from_db(); + $self->SUPER::remove_from_db(); - $dbh->bz_commit_transaction(); + $dbh->bz_commit_transaction(); } @@ -84,38 +86,41 @@ sub remove_from_db { ############################### sub _check_name { - my ($invocant, $name) = @_; - - $name = trim($name); - $name || ThrowUserError('classification_not_specified'); - - if (length($name) > MAX_CLASSIFICATION_SIZE) { - ThrowUserError('classification_name_too_long', {'name' => $name}); - } - - my $classification = new Bugzilla::Classification({name => $name}); - if ($classification && (!ref $invocant || $classification->id != $invocant->id)) { - ThrowUserError("classification_already_exists", { name => $classification->name }); - } - return $name; + my ($invocant, $name) = @_; + + $name = trim($name); + $name || ThrowUserError('classification_not_specified'); + + if (length($name) > MAX_CLASSIFICATION_SIZE) { + ThrowUserError('classification_name_too_long', {'name' => $name}); + } + + my $classification = new Bugzilla::Classification({name => $name}); + if ($classification && (!ref $invocant || $classification->id != $invocant->id)) + { + ThrowUserError("classification_already_exists", + {name => $classification->name}); + } + return $name; } sub _check_description { - my ($invocant, $description) = @_; + my ($invocant, $description) = @_; - $description = trim($description || ''); - return $description; + $description = trim($description || ''); + return $description; } sub _check_sortkey { - my ($invocant, $sortkey) = @_; - - $sortkey ||= 0; - my $stored_sortkey = $sortkey; - if (!detaint_natural($sortkey) || $sortkey > MAX_SMALLINT) { - ThrowUserError('classification_invalid_sortkey', { 'sortkey' => $stored_sortkey }); - } - return $sortkey; + my ($invocant, $sortkey) = @_; + + $sortkey ||= 0; + my $stored_sortkey = $sortkey; + if (!detaint_natural($sortkey) || $sortkey > MAX_SMALLINT) { + ThrowUserError('classification_invalid_sortkey', + {'sortkey' => $stored_sortkey}); + } + return $sortkey; } ##################################### @@ -124,41 +129,45 @@ sub _check_sortkey { use constant FIELD_NAME => 'classification'; use constant is_default => 0; -use constant is_active => 1; +use constant is_active => 1; ############################### #### Methods #### ############################### -sub set_name { $_[0]->set('name', $_[1]); } +sub set_name { $_[0]->set('name', $_[1]); } sub set_description { $_[0]->set('description', $_[1]); } -sub set_sortkey { $_[0]->set('sortkey', $_[1]); } +sub set_sortkey { $_[0]->set('sortkey', $_[1]); } sub product_count { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - if (!defined $self->{'product_count'}) { - $self->{'product_count'} = $dbh->selectrow_array(q{ + if (!defined $self->{'product_count'}) { + $self->{'product_count'} = $dbh->selectrow_array( + q{ SELECT COUNT(*) FROM products - WHERE classification_id = ?}, undef, $self->id) || 0; - } - return $self->{'product_count'}; + WHERE classification_id = ?}, undef, $self->id + ) || 0; + } + return $self->{'product_count'}; } sub products { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - if (!$self->{'products'}) { - my $product_ids = $dbh->selectcol_arrayref(q{ + if (!$self->{'products'}) { + my $product_ids = $dbh->selectcol_arrayref( + q{ SELECT id FROM products WHERE classification_id = ? - ORDER BY name}, undef, $self->id); + ORDER BY name}, undef, $self->id + ); - $self->{'products'} = Bugzilla::Product->new_from_list($product_ids); - } - return $self->{'products'}; + $self->{'products'} = Bugzilla::Product->new_from_list($product_ids); + } + return $self->{'products'}; } ############################### @@ -166,7 +175,7 @@ sub products { ############################### sub description { return $_[0]->{'description'}; } -sub sortkey { return $_[0]->{'sortkey'}; } +sub sortkey { return $_[0]->{'sortkey'}; } ############################### @@ -177,27 +186,32 @@ sub sortkey { return $_[0]->{'sortkey'}; } # in global/choose-product.html.tmpl. sub sort_products_by_classification { - my $products = shift; - my $list; - - if (Bugzilla->params->{'useclassification'}) { - my $class = {}; - # Get all classifications with at least one product. - foreach my $product (@$products) { - $class->{$product->classification_id}->{'object'} ||= - new Bugzilla::Classification($product->classification_id); - # Nice way to group products per classification, without querying - # the DB again. - push(@{$class->{$product->classification_id}->{'products'}}, $product); - } - $list = [sort {$a->{'object'}->sortkey <=> $b->{'object'}->sortkey - || lc($a->{'object'}->name) cmp lc($b->{'object'}->name)} - (values %$class)]; - } - else { - $list = [{object => undef, products => $products}]; + my $products = shift; + my $list; + + if (Bugzilla->params->{'useclassification'}) { + my $class = {}; + + # Get all classifications with at least one product. + foreach my $product (@$products) { + $class->{$product->classification_id}->{'object'} + ||= new Bugzilla::Classification($product->classification_id); + + # Nice way to group products per classification, without querying + # the DB again. + push(@{$class->{$product->classification_id}->{'products'}}, $product); } - return $list; + $list = [ + sort { + $a->{'object'}->sortkey <=> $b->{'object'}->sortkey + || lc($a->{'object'}->name) cmp lc($b->{'object'}->name) + } (values %$class) + ]; + } + else { + $list = [{object => undef, products => $products}]; + } + return $list; } 1; diff --git a/Bugzilla/Comment.pm b/Bugzilla/Comment.pm index b036907d7..02a044ff5 100644 --- a/Bugzilla/Comment.pm +++ b/Bugzilla/Comment.pm @@ -33,47 +33,48 @@ use constant AUDIT_CREATES => 0; use constant AUDIT_UPDATES => 0; use constant DB_COLUMNS => qw( - comment_id - bug_id - who - bug_when - work_time - thetext - isprivate - already_wrapped - type - extra_data + comment_id + bug_id + who + bug_when + work_time + thetext + isprivate + already_wrapped + type + extra_data ); use constant UPDATE_COLUMNS => qw( - isprivate - type - extra_data + isprivate + type + extra_data ); use constant DB_TABLE => 'longdescs'; use constant ID_FIELD => 'comment_id'; + # In some rare cases, two comments can have identical timestamps. If # this happens, we want to be sure that the comment added later shows up # later in the sequence. use constant LIST_ORDER => 'bug_when, comment_id'; use constant VALIDATORS => { - bug_id => \&_check_bug_id, - who => \&_check_who, - bug_when => \&_check_bug_when, - work_time => \&_check_work_time, - thetext => \&_check_thetext, - isprivate => \&_check_isprivate, - extra_data => \&_check_extra_data, - type => \&_check_type, + bug_id => \&_check_bug_id, + who => \&_check_who, + bug_when => \&_check_bug_when, + work_time => \&_check_work_time, + thetext => \&_check_thetext, + isprivate => \&_check_isprivate, + extra_data => \&_check_extra_data, + type => \&_check_type, }; use constant VALIDATOR_DEPENDENCIES => { - extra_data => ['type'], - bug_id => ['who'], - work_time => ['who', 'bug_id'], - isprivate => ['who'], + extra_data => ['type'], + bug_id => ['who'], + work_time => ['who', 'bug_id'], + isprivate => ['who'], }; ######################### @@ -81,95 +82,100 @@ use constant VALIDATOR_DEPENDENCIES => { ######################### sub update { - my $self = shift; - my ($changes, $old_comment) = $self->SUPER::update(@_); - - if (exists $changes->{'thetext'} || exists $changes->{'isprivate'}) { - $self->bug->_sync_fulltext( update_comments => 1); - } - - my @old_tags = @{ $old_comment->tags }; - my @new_tags = @{ $self->tags }; - my ($removed_tags, $added_tags) = diff_arrays(\@old_tags, \@new_tags); - - if (@$removed_tags || @$added_tags) { - my $dbh = Bugzilla->dbh; - my $when = $dbh->selectrow_array("SELECT LOCALTIMESTAMP(0)"); - my $sth_delete = $dbh->prepare( - "DELETE FROM longdescs_tags WHERE comment_id = ? AND tag = ?" - ); - my $sth_insert = $dbh->prepare( - "INSERT INTO longdescs_tags(comment_id, tag) VALUES (?, ?)" - ); - my $sth_activity = $dbh->prepare( - "INSERT INTO longdescs_tags_activity + my $self = shift; + my ($changes, $old_comment) = $self->SUPER::update(@_); + + if (exists $changes->{'thetext'} || exists $changes->{'isprivate'}) { + $self->bug->_sync_fulltext(update_comments => 1); + } + + my @old_tags = @{$old_comment->tags}; + my @new_tags = @{$self->tags}; + my ($removed_tags, $added_tags) = diff_arrays(\@old_tags, \@new_tags); + + if (@$removed_tags || @$added_tags) { + my $dbh = Bugzilla->dbh; + my $when = $dbh->selectrow_array("SELECT LOCALTIMESTAMP(0)"); + my $sth_delete = $dbh->prepare( + "DELETE FROM longdescs_tags WHERE comment_id = ? AND tag = ?"); + my $sth_insert + = $dbh->prepare("INSERT INTO longdescs_tags(comment_id, tag) VALUES (?, ?)"); + my $sth_activity = $dbh->prepare( + "INSERT INTO longdescs_tags_activity (bug_id, comment_id, who, bug_when, added, removed) VALUES (?, ?, ?, ?, ?, ?)" - ); - - foreach my $tag (@$removed_tags) { - my $weighted = Bugzilla::Comment::TagWeights->new({ name => $tag }); - if ($weighted) { - if ($weighted->weight == 1) { - $weighted->remove_from_db(); - } else { - $weighted->set_weight($weighted->weight - 1); - $weighted->update(); - } - } - trick_taint($tag); - $sth_delete->execute($self->id, $tag); - $sth_activity->execute( - $self->bug_id, $self->id, Bugzilla->user->id, $when, '', $tag); - } + ); - foreach my $tag (@$added_tags) { - my $weighted = Bugzilla::Comment::TagWeights->new({ name => $tag }); - if ($weighted) { - $weighted->set_weight($weighted->weight + 1); - $weighted->update(); - } else { - Bugzilla::Comment::TagWeights->create({ tag => $tag, weight => 1 }); - } - trick_taint($tag); - $sth_insert->execute($self->id, $tag); - $sth_activity->execute( - $self->bug_id, $self->id, Bugzilla->user->id, $when, $tag, ''); + foreach my $tag (@$removed_tags) { + my $weighted = Bugzilla::Comment::TagWeights->new({name => $tag}); + if ($weighted) { + if ($weighted->weight == 1) { + $weighted->remove_from_db(); } + else { + $weighted->set_weight($weighted->weight - 1); + $weighted->update(); + } + } + trick_taint($tag); + $sth_delete->execute($self->id, $tag); + $sth_activity->execute($self->bug_id, $self->id, Bugzilla->user->id, $when, '', + $tag); } - return $changes; + foreach my $tag (@$added_tags) { + my $weighted = Bugzilla::Comment::TagWeights->new({name => $tag}); + if ($weighted) { + $weighted->set_weight($weighted->weight + 1); + $weighted->update(); + } + else { + Bugzilla::Comment::TagWeights->create({tag => $tag, weight => 1}); + } + trick_taint($tag); + $sth_insert->execute($self->id, $tag); + $sth_activity->execute($self->bug_id, $self->id, Bugzilla->user->id, $when, + $tag, ''); + } + } + + return $changes; } # Speeds up displays of comment lists by loading all author objects and tags at # once for a whole list. sub preload { - my ($class, $comments) = @_; - # Author - my %user_ids = map { $_->{who} => 1 } @$comments; - my $users = Bugzilla::User->new_from_list([keys %user_ids]); - my %user_map = map { $_->id => $_ } @$users; - foreach my $comment (@$comments) { - $comment->{author} = $user_map{$comment->{who}}; - } - # Tags - if (Bugzilla->params->{'comment_taggers_group'}) { - my $dbh = Bugzilla->dbh; - my @comment_ids = map { $_->id } @$comments; - my %comment_map = map { $_->id => $_ } @$comments; - my $rows = $dbh->selectall_arrayref( - "SELECT comment_id, " . $dbh->sql_group_concat('tag', "','") . " + my ($class, $comments) = @_; + + # Author + my %user_ids = map { $_->{who} => 1 } @$comments; + my $users = Bugzilla::User->new_from_list([keys %user_ids]); + my %user_map = map { $_->id => $_ } @$users; + foreach my $comment (@$comments) { + $comment->{author} = $user_map{$comment->{who}}; + } + + # Tags + if (Bugzilla->params->{'comment_taggers_group'}) { + my $dbh = Bugzilla->dbh; + my @comment_ids = map { $_->id } @$comments; + my %comment_map = map { $_->id => $_ } @$comments; + my $rows = $dbh->selectall_arrayref( + "SELECT comment_id, " . $dbh->sql_group_concat('tag', "','") . " FROM longdescs_tags - WHERE " . $dbh->sql_in('comment_id', \@comment_ids) . ' ' . - $dbh->sql_group_by('comment_id')); - foreach my $row (@$rows) { - $comment_map{$row->[0]}->{tags} = [ split(/,/, $row->[1]) ]; - } - # Also sets the 'tags' attribute for comments which have no entry - # in the longdescs_tags table, else calling $comment->tags will - # trigger another SQL query again. - $comment_map{$_}->{tags} ||= [] foreach @comment_ids; + WHERE " + . $dbh->sql_in('comment_id', \@comment_ids) . ' ' + . $dbh->sql_group_by('comment_id') + ); + foreach my $row (@$rows) { + $comment_map{$row->[0]}->{tags} = [split(/,/, $row->[1])]; } + + # Also sets the 'tags' attribute for comments which have no entry + # in the longdescs_tags table, else calling $comment->tags will + # trigger another SQL query again. + $comment_map{$_}->{tags} ||= [] foreach @comment_ids; + } } ############################### @@ -177,130 +183,132 @@ sub preload { ############################### sub already_wrapped { return $_[0]->{'already_wrapped'}; } -sub body { return $_[0]->{'thetext'}; } -sub bug_id { return $_[0]->{'bug_id'}; } -sub creation_ts { return $_[0]->{'bug_when'}; } -sub is_private { return $_[0]->{'isprivate'}; } -sub work_time { - # Work time is returned as a string (see bug 607909) - return 0 if $_[0]->{'work_time'} + 0 == 0; - return $_[0]->{'work_time'}; +sub body { return $_[0]->{'thetext'}; } +sub bug_id { return $_[0]->{'bug_id'}; } +sub creation_ts { return $_[0]->{'bug_when'}; } +sub is_private { return $_[0]->{'isprivate'}; } + +sub work_time { + + # Work time is returned as a string (see bug 607909) + return 0 if $_[0]->{'work_time'} + 0 == 0; + return $_[0]->{'work_time'}; } -sub type { return $_[0]->{'type'}; } -sub extra_data { return $_[0]->{'extra_data'} } +sub type { return $_[0]->{'type'}; } +sub extra_data { return $_[0]->{'extra_data'} } sub tags { - my ($self) = @_; - state $comment_taggers_group = Bugzilla->params->{'comment_taggers_group'}; - return [] unless $comment_taggers_group; - $self->{'tags'} ||= Bugzilla->dbh->selectcol_arrayref( - "SELECT tag + my ($self) = @_; + state $comment_taggers_group = Bugzilla->params->{'comment_taggers_group'}; + return [] unless $comment_taggers_group; + $self->{'tags'} ||= Bugzilla->dbh->selectcol_arrayref( + "SELECT tag FROM longdescs_tags WHERE comment_id = ? - ORDER BY tag", - undef, $self->id); - return $self->{'tags'}; + ORDER BY tag", undef, $self->id + ); + return $self->{'tags'}; } sub collapsed { - my ($self) = @_; - state $comment_taggers_group = Bugzilla->params->{'comment_taggers_group'}; - return 0 unless $comment_taggers_group; - return $self->{collapsed} if exists $self->{collapsed}; - - state $collapsed_comment_tags = Bugzilla->params->{'collapsed_comment_tags'}; - $self->{collapsed} = 0; - Bugzilla->request_cache->{comment_tags_collapsed} - ||= [ split(/\s*,\s*/, $collapsed_comment_tags) ]; - my @collapsed_tags = @{ Bugzilla->request_cache->{comment_tags_collapsed} }; - foreach my $my_tag (@{ $self->tags }) { - $my_tag = lc($my_tag); - foreach my $collapsed_tag (@collapsed_tags) { - if ($my_tag eq lc($collapsed_tag)) { - $self->{collapsed} = 1; - last; - } - } - last if $self->{collapsed}; + my ($self) = @_; + state $comment_taggers_group = Bugzilla->params->{'comment_taggers_group'}; + return 0 unless $comment_taggers_group; + return $self->{collapsed} if exists $self->{collapsed}; + + state $collapsed_comment_tags = Bugzilla->params->{'collapsed_comment_tags'}; + $self->{collapsed} = 0; + Bugzilla->request_cache->{comment_tags_collapsed} + ||= [split(/\s*,\s*/, $collapsed_comment_tags)]; + my @collapsed_tags = @{Bugzilla->request_cache->{comment_tags_collapsed}}; + foreach my $my_tag (@{$self->tags}) { + $my_tag = lc($my_tag); + foreach my $collapsed_tag (@collapsed_tags) { + if ($my_tag eq lc($collapsed_tag)) { + $self->{collapsed} = 1; + last; + } } - return $self->{collapsed}; + last if $self->{collapsed}; + } + return $self->{collapsed}; } sub bug { - my $self = shift; - require Bugzilla::Bug; - $self->{bug} ||= new Bugzilla::Bug($self->bug_id); - return $self->{bug}; + my $self = shift; + require Bugzilla::Bug; + $self->{bug} ||= new Bugzilla::Bug($self->bug_id); + return $self->{bug}; } sub is_about_attachment { - my ($self) = @_; - return 1 if ($self->type == CMT_ATTACHMENT_CREATED - or $self->type == CMT_ATTACHMENT_UPDATED); - return 0; + my ($self) = @_; + return 1 + if ($self->type == CMT_ATTACHMENT_CREATED + or $self->type == CMT_ATTACHMENT_UPDATED); + return 0; } sub attachment { - my ($self) = @_; - return undef if not $self->is_about_attachment; - $self->{attachment} ||= - new Bugzilla::Attachment({ id => $self->extra_data, cache => 1 }); - return $self->{attachment}; + my ($self) = @_; + return undef if not $self->is_about_attachment; + $self->{attachment} + ||= new Bugzilla::Attachment({id => $self->extra_data, cache => 1}); + return $self->{attachment}; } -sub author { - my $self = shift; - $self->{'author'} - ||= new Bugzilla::User({ id => $self->{'who'}, cache => 1 }); - return $self->{'author'}; +sub author { + my $self = shift; + $self->{'author'} ||= new Bugzilla::User({id => $self->{'who'}, cache => 1}); + return $self->{'author'}; } sub body_full { - my ($self, $params) = @_; - $params ||= {}; - my $template = Bugzilla->template_inner; - my $body; - if ($self->type) { - $template->process("bug/format_comment.txt.tmpl", - { comment => $self, %$params }, \$body) - || ThrowTemplateError($template->error()); - $body =~ s/^X//; - } - else { - $body = $self->body; - } - if ($params->{wrap} and !$self->already_wrapped) { - $body = wrap_comment($body); - } - return $body; + my ($self, $params) = @_; + $params ||= {}; + my $template = Bugzilla->template_inner; + my $body; + if ($self->type) { + $template->process("bug/format_comment.txt.tmpl", {comment => $self, %$params}, + \$body) + || ThrowTemplateError($template->error()); + $body =~ s/^X//; + } + else { + $body = $self->body; + } + if ($params->{wrap} and !$self->already_wrapped) { + $body = wrap_comment($body); + } + return $body; } ############ # Mutators # ############ -sub set_is_private { $_[0]->set('isprivate', $_[1]); } -sub set_type { $_[0]->set('type', $_[1]); } -sub set_extra_data { $_[0]->set('extra_data', $_[1]); } +sub set_is_private { $_[0]->set('isprivate', $_[1]); } +sub set_type { $_[0]->set('type', $_[1]); } +sub set_extra_data { $_[0]->set('extra_data', $_[1]); } sub add_tag { - my ($self, $tag) = @_; - $tag = $self->_check_tag($tag); + my ($self, $tag) = @_; + $tag = $self->_check_tag($tag); - my $tags = $self->tags; - return if grep { lc($tag) eq lc($_) } @$tags; - push @$tags, $tag; - $self->{'tags'} = [ sort @$tags ]; + my $tags = $self->tags; + return if grep { lc($tag) eq lc($_) } @$tags; + push @$tags, $tag; + $self->{'tags'} = [sort @$tags]; } sub remove_tag { - my ($self, $tag) = @_; - $tag = $self->_check_tag($tag); + my ($self, $tag) = @_; + $tag = $self->_check_tag($tag); - my $tags = $self->tags; - my $index = first { lc($tags->[$_]) eq lc($tag) } 0..scalar(@$tags) - 1; - return unless defined $index; - splice(@$tags, $index, 1); + my $tags = $self->tags; + my $index = first { lc($tags->[$_]) eq lc($tag) } 0 .. scalar(@$tags) - 1; + return unless defined $index; + splice(@$tags, $index, 1); } ############## @@ -308,180 +316,180 @@ sub remove_tag { ############## sub run_create_validators { - my $self = shift; - my $params = $self->SUPER::run_create_validators(@_); - # Sometimes this run_create_validators is called with parameters that - # skip bug_id validation, so it might not exist in the resulting hash. - if (defined $params->{bug_id}) { - $params->{bug_id} = $params->{bug_id}->id; - } - return $params; + my $self = shift; + my $params = $self->SUPER::run_create_validators(@_); + + # Sometimes this run_create_validators is called with parameters that + # skip bug_id validation, so it might not exist in the resulting hash. + if (defined $params->{bug_id}) { + $params->{bug_id} = $params->{bug_id}->id; + } + return $params; } sub _check_extra_data { - my ($invocant, $extra_data, undef, $params) = @_; - my $type = blessed($invocant) ? $invocant->type : $params->{type}; + my ($invocant, $extra_data, undef, $params) = @_; + my $type = blessed($invocant) ? $invocant->type : $params->{type}; - if ($type == CMT_NORMAL) { - if (defined $extra_data) { - ThrowCodeError('comment_extra_data_not_allowed', - { type => $type, extra_data => $extra_data }); - } + if ($type == CMT_NORMAL) { + if (defined $extra_data) { + ThrowCodeError('comment_extra_data_not_allowed', + {type => $type, extra_data => $extra_data}); + } + } + else { + if (!defined $extra_data) { + ThrowCodeError('comment_extra_data_required', {type => $type}); + } + elsif ($type == CMT_ATTACHMENT_CREATED or $type == CMT_ATTACHMENT_UPDATED) { + my $attachment = Bugzilla::Attachment->check({id => $extra_data}); + $extra_data = $attachment->id; } else { - if (!defined $extra_data) { - ThrowCodeError('comment_extra_data_required', { type => $type }); - } - elsif ($type == CMT_ATTACHMENT_CREATED - or $type == CMT_ATTACHMENT_UPDATED) - { - my $attachment = Bugzilla::Attachment->check({ - id => $extra_data }); - $extra_data = $attachment->id; - } - else { - my $original = $extra_data; - detaint_natural($extra_data) - or ThrowCodeError('comment_extra_data_not_numeric', - { type => $type, extra_data => $original }); - } + my $original = $extra_data; + detaint_natural($extra_data) + or ThrowCodeError('comment_extra_data_not_numeric', + {type => $type, extra_data => $original}); } + } - return $extra_data; + return $extra_data; } sub _check_type { - my ($invocant, $type) = @_; - $type ||= CMT_NORMAL; - my $original = $type; - detaint_natural($type) - or ThrowCodeError('comment_type_invalid', { type => $original }); - return $type; + my ($invocant, $type) = @_; + $type ||= CMT_NORMAL; + my $original = $type; + detaint_natural($type) + or ThrowCodeError('comment_type_invalid', {type => $original}); + return $type; } sub _check_bug_id { - my ($invocant, $bug_id) = @_; - - ThrowCodeError('param_required', {function => 'Bugzilla::Comment->create', - param => 'bug_id'}) unless $bug_id; - - my $bug; - if (blessed $bug_id) { - # We got a bug object passed in, use it - $bug = $bug_id; - $bug->check_is_visible; - } - else { - # We got a bug id passed in, check it and get the bug object - $bug = Bugzilla::Bug->check({ id => $bug_id }); - } - - # Make sure the user can edit the product - Bugzilla->user->can_edit_product($bug->{product_id}); - - # Make sure the user can comment - my $privs; - $bug->check_can_change_field('longdesc', 0, 1, \$privs) - || ThrowUserError('illegal_change', - { field => 'longdesc', privs => $privs }); - return $bug; + my ($invocant, $bug_id) = @_; + + ThrowCodeError('param_required', + {function => 'Bugzilla::Comment->create', param => 'bug_id'}) + unless $bug_id; + + my $bug; + if (blessed $bug_id) { + + # We got a bug object passed in, use it + $bug = $bug_id; + $bug->check_is_visible; + } + else { + # We got a bug id passed in, check it and get the bug object + $bug = Bugzilla::Bug->check({id => $bug_id}); + } + + # Make sure the user can edit the product + Bugzilla->user->can_edit_product($bug->{product_id}); + + # Make sure the user can comment + my $privs; + $bug->check_can_change_field('longdesc', 0, 1, \$privs) + || ThrowUserError('illegal_change', {field => 'longdesc', privs => $privs}); + return $bug; } sub _check_who { - my ($invocant, $who) = @_; - Bugzilla->login(LOGIN_REQUIRED); - return Bugzilla->user->id; + my ($invocant, $who) = @_; + Bugzilla->login(LOGIN_REQUIRED); + return Bugzilla->user->id; } sub _check_bug_when { - my ($invocant, $when) = @_; + my ($invocant, $when) = @_; - # Make sure the timestamp is defined, default to a timestamp from the db - if (!defined $when) { - $when = Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); - } + # Make sure the timestamp is defined, default to a timestamp from the db + if (!defined $when) { + $when = Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); + } - # Make sure the timestamp parses - if (!datetime_from($when)) { - ThrowCodeError('invalid_timestamp', { timestamp => $when }); - } + # Make sure the timestamp parses + if (!datetime_from($when)) { + ThrowCodeError('invalid_timestamp', {timestamp => $when}); + } - return $when; + return $when; } sub _check_work_time { - my ($invocant, $value_in, $field, $params) = @_; - - # Call down to Bugzilla::Object, letting it know negative - # values are ok - my $time = $invocant->check_time($value_in, $field, $params, 1); - my $privs; - $params->{bug_id}->check_can_change_field('work_time', 0, $time, \$privs) - || ThrowUserError('illegal_change', - { field => 'work_time', privs => $privs }); - return $time; + my ($invocant, $value_in, $field, $params) = @_; + + # Call down to Bugzilla::Object, letting it know negative + # values are ok + my $time = $invocant->check_time($value_in, $field, $params, 1); + my $privs; + $params->{bug_id}->check_can_change_field('work_time', 0, $time, \$privs) + || ThrowUserError('illegal_change', {field => 'work_time', privs => $privs}); + return $time; } sub _check_thetext { - my ($invocant, $thetext) = @_; - - ThrowCodeError('param_required',{function => 'Bugzilla::Comment->create', - param => 'thetext'}) unless defined $thetext; - - # Remove any trailing whitespace. Leading whitespace could be - # a valid part of the comment. - $thetext =~ s/\s*$//s; - $thetext =~ s/\r\n?/\n/g; # Get rid of \r. - - # Characters above U+FFFF cannot be stored by MySQL older than 5.5.3 as they - # require the new utf8mb4 character set. Other DB servers are handling them - # without any problem. So we need to replace these characters if we use MySQL, - # else the comment is truncated. - # XXX - Once we use utf8mb4 for comments, this hack for MySQL can go away. - state $is_mysql = Bugzilla->dbh->isa('Bugzilla::DB::Mysql') ? 1 : 0; - if ($is_mysql) { - # Perl 5.13.8 and older complain about non-characters. - no warnings 'utf8'; - $thetext =~ s/([\x{10000}-\x{10FFFF}])/"\x{FDD0}[" . uc(sprintf('U+%04x', ord($1))) . "]\x{FDD1}"/eg; - } - - ThrowUserError('comment_too_long') if length($thetext) > MAX_COMMENT_LENGTH; - return $thetext; + my ($invocant, $thetext) = @_; + + ThrowCodeError('param_required', + {function => 'Bugzilla::Comment->create', param => 'thetext'}) + unless defined $thetext; + + # Remove any trailing whitespace. Leading whitespace could be + # a valid part of the comment. + $thetext =~ s/\s*$//s; + $thetext =~ s/\r\n?/\n/g; # Get rid of \r. + + # Characters above U+FFFF cannot be stored by MySQL older than 5.5.3 as they + # require the new utf8mb4 character set. Other DB servers are handling them + # without any problem. So we need to replace these characters if we use MySQL, + # else the comment is truncated. + # XXX - Once we use utf8mb4 for comments, this hack for MySQL can go away. + state $is_mysql = Bugzilla->dbh->isa('Bugzilla::DB::Mysql') ? 1 : 0; + if ($is_mysql) { + + # Perl 5.13.8 and older complain about non-characters. + no warnings 'utf8'; + $thetext + =~ s/([\x{10000}-\x{10FFFF}])/"\x{FDD0}[" . uc(sprintf('U+%04x', ord($1))) . "]\x{FDD1}"/eg; + } + + ThrowUserError('comment_too_long') if length($thetext) > MAX_COMMENT_LENGTH; + return $thetext; } sub _check_isprivate { - my ($invocant, $isprivate) = @_; - if ($isprivate && !Bugzilla->user->is_insider) { - ThrowUserError('user_not_insider'); - } - return $isprivate ? 1 : 0; + my ($invocant, $isprivate) = @_; + if ($isprivate && !Bugzilla->user->is_insider) { + ThrowUserError('user_not_insider'); + } + return $isprivate ? 1 : 0; } sub _check_tag { - my ($invocant, $tag) = @_; - length($tag) < MIN_COMMENT_TAG_LENGTH - and ThrowUserError('comment_tag_too_short', { tag => $tag }); - length($tag) > MAX_COMMENT_TAG_LENGTH - and ThrowUserError('comment_tag_too_long', { tag => $tag }); - $tag =~ /^[\w\d\._-]+$/ - or ThrowUserError('comment_tag_invalid', { tag => $tag }); - return $tag; + my ($invocant, $tag) = @_; + length($tag) < MIN_COMMENT_TAG_LENGTH + and ThrowUserError('comment_tag_too_short', {tag => $tag}); + length($tag) > MAX_COMMENT_TAG_LENGTH + and ThrowUserError('comment_tag_too_long', {tag => $tag}); + $tag =~ /^[\w\d\._-]+$/ or ThrowUserError('comment_tag_invalid', {tag => $tag}); + return $tag; } sub count { - my ($self) = @_; + my ($self) = @_; - return $self->{'count'} if defined $self->{'count'}; + return $self->{'count'} if defined $self->{'count'}; - my $dbh = Bugzilla->dbh; - ($self->{'count'}) = $dbh->selectrow_array( - "SELECT COUNT(*) + my $dbh = Bugzilla->dbh; + ($self->{'count'}) = $dbh->selectrow_array( + "SELECT COUNT(*) FROM longdescs WHERE bug_id = ? - AND bug_when <= ?", - undef, $self->bug_id, $self->creation_ts); + AND bug_when <= ?", undef, $self->bug_id, $self->creation_ts + ); - return --$self->{'count'}; + return --$self->{'count'}; } 1; diff --git a/Bugzilla/Comment/TagWeights.pm b/Bugzilla/Comment/TagWeights.pm index 7dba53e34..5355cad7f 100644 --- a/Bugzilla/Comment/TagWeights.pm +++ b/Bugzilla/Comment/TagWeights.pm @@ -21,20 +21,20 @@ use constant AUDIT_UPDATES => 0; use constant AUDIT_REMOVES => 0; use constant DB_COLUMNS => qw( - id - tag - weight + id + tag + weight ); use constant UPDATE_COLUMNS => qw( - weight + weight ); use constant DB_TABLE => 'longdescs_tags_weights'; use constant ID_FIELD => 'id'; use constant NAME_FIELD => 'tag'; use constant LIST_ORDER => 'weight DESC'; -use constant VALIDATORS => { }; +use constant VALIDATORS => {}; # There's no gain to caching these objects use constant USE_MEMCACHED => 0; diff --git a/Bugzilla/Component.pm b/Bugzilla/Component.pm index d5a6ece5d..3cdee9d63 100644 --- a/Bugzilla/Component.pm +++ b/Bugzilla/Component.pm @@ -27,150 +27,147 @@ use Scalar::Util qw(blessed); ############################### use constant DB_TABLE => 'components'; + # This is mostly for the editfields.cgi case where ->get_all is called. use constant LIST_ORDER => 'product_id, name'; use constant DB_COLUMNS => qw( - id - name - product_id - initialowner - initialqacontact - description - isactive + id + name + product_id + initialowner + initialqacontact + description + isactive ); use constant UPDATE_COLUMNS => qw( - name - initialowner - initialqacontact - description - isactive + name + initialowner + initialqacontact + description + isactive ); -use constant REQUIRED_FIELD_MAP => { - product_id => 'product', -}; +use constant REQUIRED_FIELD_MAP => {product_id => 'product',}; use constant VALIDATORS => { - create_series => \&Bugzilla::Object::check_boolean, - product => \&_check_product, - initialowner => \&_check_initialowner, - initialqacontact => \&_check_initialqacontact, - description => \&_check_description, - initial_cc => \&_check_cc_list, - name => \&_check_name, - isactive => \&Bugzilla::Object::check_boolean, + create_series => \&Bugzilla::Object::check_boolean, + product => \&_check_product, + initialowner => \&_check_initialowner, + initialqacontact => \&_check_initialqacontact, + description => \&_check_description, + initial_cc => \&_check_cc_list, + name => \&_check_name, + isactive => \&Bugzilla::Object::check_boolean, }; -use constant VALIDATOR_DEPENDENCIES => { - name => ['product'], -}; +use constant VALIDATOR_DEPENDENCIES => {name => ['product'],}; ############################### sub new { - my $class = shift; - my $param = shift; - my $dbh = Bugzilla->dbh; - - my $product; - if (ref $param and !defined $param->{id}) { - $product = $param->{product}; - my $name = $param->{name}; - if (!defined $product) { - ThrowCodeError('bad_arg', - {argument => 'product', - function => "${class}::new"}); - } - if (!defined $name) { - ThrowCodeError('bad_arg', - {argument => 'name', - function => "${class}::new"}); - } - - my $condition = 'product_id = ? AND name = ?'; - my @values = ($product->id, $name); - $param = { condition => $condition, values => \@values }; + my $class = shift; + my $param = shift; + my $dbh = Bugzilla->dbh; + + my $product; + if (ref $param and !defined $param->{id}) { + $product = $param->{product}; + my $name = $param->{name}; + if (!defined $product) { + ThrowCodeError('bad_arg', {argument => 'product', function => "${class}::new"}); } + if (!defined $name) { + ThrowCodeError('bad_arg', {argument => 'name', function => "${class}::new"}); + } + + my $condition = 'product_id = ? AND name = ?'; + my @values = ($product->id, $name); + $param = {condition => $condition, values => \@values}; + } - unshift @_, $param; - my $component = $class->SUPER::new(@_); - # Add the product object as attribute only if the component exists. - $component->{product} = $product if ($component && $product); - return $component; + unshift @_, $param; + my $component = $class->SUPER::new(@_); + + # Add the product object as attribute only if the component exists. + $component->{product} = $product if ($component && $product); + return $component; } sub create { - my $class = shift; - my $dbh = Bugzilla->dbh; + my $class = shift; + my $dbh = Bugzilla->dbh; - $dbh->bz_start_transaction(); + $dbh->bz_start_transaction(); - $class->check_required_create_fields(@_); - my $params = $class->run_create_validators(@_); - my $cc_list = delete $params->{initial_cc}; - my $create_series = delete $params->{create_series}; - my $product = delete $params->{product}; - $params->{product_id} = $product->id; + $class->check_required_create_fields(@_); + my $params = $class->run_create_validators(@_); + my $cc_list = delete $params->{initial_cc}; + my $create_series = delete $params->{create_series}; + my $product = delete $params->{product}; + $params->{product_id} = $product->id; - my $component = $class->insert_create_data($params); - $component->{product} = $product; + my $component = $class->insert_create_data($params); + $component->{product} = $product; - # We still have to fill the component_cc table. - $component->_update_cc_list($cc_list) if $cc_list; + # We still have to fill the component_cc table. + $component->_update_cc_list($cc_list) if $cc_list; - # Create series for the new component. - $component->_create_series() if $create_series; + # Create series for the new component. + $component->_create_series() if $create_series; - $dbh->bz_commit_transaction(); - return $component; + $dbh->bz_commit_transaction(); + return $component; } sub update { - my $self = shift; - my $changes = $self->SUPER::update(@_); - - # Update the component_cc table if necessary. - if (defined $self->{cc_ids}) { - my $diff = $self->_update_cc_list($self->{cc_ids}); - $changes->{cc_list} = $diff if defined $diff; - } - return $changes; + my $self = shift; + my $changes = $self->SUPER::update(@_); + + # Update the component_cc table if necessary. + if (defined $self->{cc_ids}) { + my $diff = $self->_update_cc_list($self->{cc_ids}); + $changes->{cc_list} = $diff if defined $diff; + } + return $changes; } sub remove_from_db { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - $self->_check_if_controller(); # From ChoiceInterface + $self->_check_if_controller(); # From ChoiceInterface - $dbh->bz_start_transaction(); + $dbh->bz_start_transaction(); - # Products must have at least one component. - my @components = @{ $self->product->components }; - if (scalar(@components) == 1) { - ThrowUserError('component_is_last', { comp => $self }); - } + # Products must have at least one component. + my @components = @{$self->product->components}; + if (scalar(@components) == 1) { + ThrowUserError('component_is_last', {comp => $self}); + } + + if ($self->bug_count) { + if (Bugzilla->params->{'allowbugdeletion'}) { + require Bugzilla::Bug; + foreach my $bug_id (@{$self->bug_ids}) { - if ($self->bug_count) { - if (Bugzilla->params->{'allowbugdeletion'}) { - require Bugzilla::Bug; - foreach my $bug_id (@{$self->bug_ids}) { - # Note: We allow admins to delete bugs even if they can't - # see them, as long as they can see the product. - my $bug = new Bugzilla::Bug($bug_id); - $bug->remove_from_db(); - } - } else { - ThrowUserError('component_has_bugs', {nb => $self->bug_count}); - } + # Note: We allow admins to delete bugs even if they can't + # see them, as long as they can see the product. + my $bug = new Bugzilla::Bug($bug_id); + $bug->remove_from_db(); + } } - # Update the list of components in the product object. - $self->product->{components} = [grep { $_->id != $self->id } @components]; - $self->SUPER::remove_from_db(); + else { + 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(); + $dbh->bz_commit_transaction(); } ################################ @@ -178,69 +175,70 @@ sub remove_from_db { ################################ sub _check_name { - my ($invocant, $name, undef, $params) = @_; - my $product = blessed($invocant) ? $invocant->product : $params->{product}; - - $name = trim($name); - $name || ThrowUserError('component_blank_name'); - - if (length($name) > MAX_COMPONENT_SIZE) { - ThrowUserError('component_name_too_long', {'name' => $name}); - } - - my $component = new Bugzilla::Component({product => $product, name => $name}); - if ($component && (!ref $invocant || $component->id != $invocant->id)) { - ThrowUserError('component_already_exists', { name => $component->name, - product => $product }); - } - return $name; + my ($invocant, $name, undef, $params) = @_; + my $product = blessed($invocant) ? $invocant->product : $params->{product}; + + $name = trim($name); + $name || ThrowUserError('component_blank_name'); + + if (length($name) > MAX_COMPONENT_SIZE) { + ThrowUserError('component_name_too_long', {'name' => $name}); + } + + my $component = new Bugzilla::Component({product => $product, name => $name}); + if ($component && (!ref $invocant || $component->id != $invocant->id)) { + ThrowUserError('component_already_exists', + {name => $component->name, product => $product}); + } + return $name; } sub _check_description { - my ($invocant, $description) = @_; + my ($invocant, $description) = @_; - $description = trim($description); - $description || ThrowUserError('component_blank_description'); - return $description; + $description = trim($description); + $description || ThrowUserError('component_blank_description'); + return $description; } sub _check_initialowner { - my ($invocant, $owner) = @_; + my ($invocant, $owner) = @_; - $owner || ThrowUserError('component_need_initialowner'); - my $owner_id = Bugzilla::User->check($owner)->id; - return $owner_id; + $owner || ThrowUserError('component_need_initialowner'); + my $owner_id = Bugzilla::User->check($owner)->id; + return $owner_id; } sub _check_initialqacontact { - my ($invocant, $qa_contact) = @_; - - my $qa_contact_id; - if (Bugzilla->params->{'useqacontact'}) { - $qa_contact_id = Bugzilla::User->check($qa_contact)->id if $qa_contact; - } - elsif (ref $invocant) { - $qa_contact_id = $invocant->{initialqacontact}; - } - return $qa_contact_id; + my ($invocant, $qa_contact) = @_; + + my $qa_contact_id; + if (Bugzilla->params->{'useqacontact'}) { + $qa_contact_id = Bugzilla::User->check($qa_contact)->id if $qa_contact; + } + elsif (ref $invocant) { + $qa_contact_id = $invocant->{initialqacontact}; + } + return $qa_contact_id; } sub _check_product { - my ($invocant, $product) = @_; - $product || ThrowCodeError('param_required', - { function => "$invocant->create", param => 'product' }); - return Bugzilla->user->check_can_admin_product($product->name); + my ($invocant, $product) = @_; + $product + || ThrowCodeError('param_required', + {function => "$invocant->create", param => 'product'}); + return Bugzilla->user->check_can_admin_product($product->name); } sub _check_cc_list { - my ($invocant, $cc_list) = @_; - - my %cc_ids; - foreach my $cc (@$cc_list) { - my $id = login_to_id($cc, THROW_ERROR); - $cc_ids{$id} = 1; - } - return [keys %cc_ids]; + my ($invocant, $cc_list) = @_; + + my %cc_ids; + foreach my $cc (@$cc_list) { + my $id = login_to_id($cc, THROW_ERROR); + $cc_ids{$id} = 1; + } + return [keys %cc_ids]; } ############################### @@ -248,156 +246,176 @@ sub _check_cc_list { ############################### sub _update_cc_list { - my ($self, $cc_list) = @_; - my $dbh = Bugzilla->dbh; + my ($self, $cc_list) = @_; + my $dbh = Bugzilla->dbh; - my $old_cc_list = - $dbh->selectcol_arrayref('SELECT user_id FROM component_cc - WHERE component_id = ?', undef, $self->id); + my $old_cc_list = $dbh->selectcol_arrayref( + 'SELECT user_id FROM component_cc + WHERE component_id = ?', undef, $self->id + ); - my ($removed, $added) = diff_arrays($old_cc_list, $cc_list); - my $diff; - if (scalar @$removed || scalar @$added) { - $diff = [join(', ', @$removed), join(', ', @$added)]; - } + my ($removed, $added) = diff_arrays($old_cc_list, $cc_list); + my $diff; + if (scalar @$removed || scalar @$added) { + $diff = [join(', ', @$removed), join(', ', @$added)]; + } - $dbh->do('DELETE FROM component_cc WHERE component_id = ?', undef, $self->id); + $dbh->do('DELETE FROM component_cc WHERE component_id = ?', undef, $self->id); - my $sth = $dbh->prepare('INSERT INTO component_cc - (user_id, component_id) VALUES (?, ?)'); - $sth->execute($_, $self->id) foreach (@$cc_list); + my $sth = $dbh->prepare( + 'INSERT INTO component_cc + (user_id, component_id) VALUES (?, ?)' + ); + $sth->execute($_, $self->id) foreach (@$cc_list); - return $diff; + return $diff; } sub _create_series { - my $self = shift; - - # Insert default charting queries for this product. - # If they aren't using charting, this won't do any harm. - my $prodcomp = "&product=" . url_quote($self->product->name) . - "&component=" . url_quote($self->name); - - my $open_query = 'field0-0-0=resolution&type0-0-0=notregexp&value0-0-0=.' . - $prodcomp; - my $nonopen_query = 'field0-0-0=resolution&type0-0-0=regexp&value0-0-0=.' . - $prodcomp; - - my @series = ([get_text('series_all_open'), $open_query], - [get_text('series_all_closed'), $nonopen_query]); - - foreach my $sdata (@series) { - my $series = new Bugzilla::Series(undef, $self->product->name, - $self->name, $sdata->[0], - Bugzilla->user->id, 1, $sdata->[1], 1); - $series->writeToDatabase(); - } + my $self = shift; + + # Insert default charting queries for this product. + # If they aren't using charting, this won't do any harm. + my $prodcomp + = "&product=" + . url_quote($self->product->name) + . "&component=" + . url_quote($self->name); + + my $open_query + = 'field0-0-0=resolution&type0-0-0=notregexp&value0-0-0=.' . $prodcomp; + my $nonopen_query + = 'field0-0-0=resolution&type0-0-0=regexp&value0-0-0=.' . $prodcomp; + + my @series = ( + [get_text('series_all_open'), $open_query], + [get_text('series_all_closed'), $nonopen_query] + ); + + foreach my $sdata (@series) { + my $series + = new Bugzilla::Series(undef, $self->product->name, $self->name, $sdata->[0], + Bugzilla->user->id, 1, $sdata->[1], 1); + $series->writeToDatabase(); + } } -sub set_name { $_[0]->set('name', $_[1]); } +sub set_name { $_[0]->set('name', $_[1]); } sub set_description { $_[0]->set('description', $_[1]); } -sub set_is_active { $_[0]->set('isactive', $_[1]); } +sub set_is_active { $_[0]->set('isactive', $_[1]); } + sub set_default_assignee { - my ($self, $owner) = @_; + my ($self, $owner) = @_; + + $self->set('initialowner', $owner); - $self->set('initialowner', $owner); - # Reset the default owner object. - delete $self->{default_assignee}; + # Reset the default owner object. + delete $self->{default_assignee}; } + sub set_default_qa_contact { - my ($self, $qa_contact) = @_; + my ($self, $qa_contact) = @_; + + $self->set('initialqacontact', $qa_contact); - $self->set('initialqacontact', $qa_contact); - # Reset the default QA contact object. - delete $self->{default_qa_contact}; + # Reset the default QA contact object. + delete $self->{default_qa_contact}; } + sub set_cc_list { - my ($self, $cc_list) = @_; + my ($self, $cc_list) = @_; + + $self->{cc_ids} = $self->_check_cc_list($cc_list); - $self->{cc_ids} = $self->_check_cc_list($cc_list); - # Reset the list of CC user objects. - delete $self->{initial_cc}; + # Reset the list of CC user objects. + delete $self->{initial_cc}; } sub bug_count { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - if (!defined $self->{'bug_count'}) { - $self->{'bug_count'} = $dbh->selectrow_array(q{ + if (!defined $self->{'bug_count'}) { + $self->{'bug_count'} = $dbh->selectrow_array( + q{ SELECT COUNT(*) FROM bugs - WHERE component_id = ?}, undef, $self->id) || 0; - } - return $self->{'bug_count'}; + WHERE component_id = ?}, undef, $self->id + ) || 0; + } + return $self->{'bug_count'}; } sub bug_ids { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - if (!defined $self->{'bugs_ids'}) { - $self->{'bugs_ids'} = $dbh->selectcol_arrayref(q{ + if (!defined $self->{'bugs_ids'}) { + $self->{'bugs_ids'} = $dbh->selectcol_arrayref( + q{ SELECT bug_id FROM bugs - WHERE component_id = ?}, undef, $self->id); - } - return $self->{'bugs_ids'}; + WHERE component_id = ?}, undef, $self->id + ); + } + return $self->{'bugs_ids'}; } sub default_assignee { - my $self = shift; + my $self = shift; - return $self->{'default_assignee'} - ||= new Bugzilla::User({ id => $self->{'initialowner'}, cache => 1 }); + return $self->{'default_assignee'} + ||= new Bugzilla::User({id => $self->{'initialowner'}, cache => 1}); } sub default_qa_contact { - my $self = shift; + my $self = shift; - return unless $self->{'initialqacontact'}; - return $self->{'default_qa_contact'} - ||= new Bugzilla::User({id => $self->{'initialqacontact'}, cache => 1 }); + return unless $self->{'initialqacontact'}; + return $self->{'default_qa_contact'} + ||= new Bugzilla::User({id => $self->{'initialqacontact'}, cache => 1}); } sub flag_types { - my $self = shift; - - if (!defined $self->{'flag_types'}) { - my $flagtypes = Bugzilla::FlagType::match({ product_id => $self->product_id, - component_id => $self->id }); - - $self->{'flag_types'} = {}; - $self->{'flag_types'}->{'bug'} = - [grep { $_->target_type eq 'bug' } @$flagtypes]; - $self->{'flag_types'}->{'attachment'} = - [grep { $_->target_type eq 'attachment' } @$flagtypes]; - } - return $self->{'flag_types'}; + my $self = shift; + + if (!defined $self->{'flag_types'}) { + my $flagtypes = Bugzilla::FlagType::match( + {product_id => $self->product_id, component_id => $self->id}); + + $self->{'flag_types'} = {}; + $self->{'flag_types'}->{'bug'} + = [grep { $_->target_type eq 'bug' } @$flagtypes]; + $self->{'flag_types'}->{'attachment'} + = [grep { $_->target_type eq 'attachment' } @$flagtypes]; + } + return $self->{'flag_types'}; } sub initial_cc { - my $self = shift; - my $dbh = Bugzilla->dbh; - - if (!defined $self->{'initial_cc'}) { - # If set_cc_list() has been called but data are not yet written - # into the DB, we want the new values defined by it. - my $cc_ids = $self->{cc_ids} - || $dbh->selectcol_arrayref('SELECT user_id FROM component_cc - WHERE component_id = ?', - undef, $self->id); - - $self->{'initial_cc'} = Bugzilla::User->new_from_list($cc_ids); - } - return $self->{'initial_cc'}; + my $self = shift; + my $dbh = Bugzilla->dbh; + + if (!defined $self->{'initial_cc'}) { + + # If set_cc_list() has been called but data are not yet written + # into the DB, we want the new values defined by it. + my $cc_ids = $self->{cc_ids} || $dbh->selectcol_arrayref( + 'SELECT user_id FROM component_cc + WHERE component_id = ?', undef, + $self->id + ); + + $self->{'initial_cc'} = Bugzilla::User->new_from_list($cc_ids); + } + return $self->{'initial_cc'}; } sub product { - my $self = shift; - if (!defined $self->{'product'}) { - require Bugzilla::Product; # We cannot |use| it. - $self->{'product'} = new Bugzilla::Product($self->product_id); - } - return $self->{'product'}; + my $self = shift; + if (!defined $self->{'product'}) { + require Bugzilla::Product; # We cannot |use| it. + $self->{'product'} = new Bugzilla::Product($self->product_id); + } + return $self->{'product'}; } ############################### @@ -405,8 +423,8 @@ sub product { ############################### sub description { return $_[0]->{'description'}; } -sub product_id { return $_[0]->{'product_id'}; } -sub is_active { return $_[0]->{'isactive'}; } +sub product_id { return $_[0]->{'product_id'}; } +sub is_active { return $_[0]->{'isactive'}; } ############################################## # Implement Bugzilla::Field::ChoiceInterface # @@ -416,11 +434,11 @@ use constant FIELD_NAME => 'component'; use constant is_default => 0; sub is_set_on_bug { - my ($self, $bug) = @_; - 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; + my ($self, $bug) = @_; + 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; } ############################### diff --git a/Bugzilla/Config.pm b/Bugzilla/Config.pm index 458616701..1aa944985 100644 --- a/Bugzilla/Config.pm +++ b/Bugzilla/Config.pm @@ -25,316 +25,323 @@ use File::Basename; # Don't export localvars by default - people should have to explicitly # ask for it, as a (probably futile) attempt to stop code using it # when it shouldn't -%Bugzilla::Config::EXPORT_TAGS = - ( - admin => [qw(update_params SetParam write_params)], - ); +%Bugzilla::Config::EXPORT_TAGS + = (admin => [qw(update_params SetParam write_params)],); Exporter::export_ok_tags('admin'); # INITIALISATION CODE # Perl throws a warning if we use bz_locations() directly after do. our %params; + # Load in the param definitions sub _load_params { - my $panels = param_panels(); - my %hook_panels; - foreach my $panel (keys %$panels) { - my $module = $panels->{$panel}; - eval("require $module") || die $@; - my @new_param_list = $module->get_param_list(); - $hook_panels{lc($panel)} = { params => \@new_param_list }; - } - # This hook is also called in editparams.cgi. This call here is required - # to make SetParam work. - Bugzilla::Hook::process('config_modify_panels', - { panels => \%hook_panels }); - - foreach my $panel (keys %hook_panels) { - foreach my $item (@{$hook_panels{$panel}->{params}}) { - $params{$item->{'name'}} = $item; - } + my $panels = param_panels(); + my %hook_panels; + foreach my $panel (keys %$panels) { + my $module = $panels->{$panel}; + eval("require $module") || die $@; + my @new_param_list = $module->get_param_list(); + $hook_panels{lc($panel)} = {params => \@new_param_list}; + } + + # This hook is also called in editparams.cgi. This call here is required + # to make SetParam work. + Bugzilla::Hook::process('config_modify_panels', {panels => \%hook_panels}); + + foreach my $panel (keys %hook_panels) { + foreach my $item (@{$hook_panels{$panel}->{params}}) { + $params{$item->{'name'}} = $item; } + } } + # END INIT CODE # Subroutines go here sub param_panels { - my $param_panels = {}; - my $libpath = bz_locations()->{'libpath'}; - foreach my $item ((glob "$libpath/Bugzilla/Config/*.pm")) { - $item =~ m#/([^/]+)\.pm$#; - my $module = $1; - $param_panels->{$module} = "Bugzilla::Config::$module" unless $module eq 'Common'; - } - # Now check for any hooked params - Bugzilla::Hook::process('config_add_panels', - { panel_modules => $param_panels }); - return $param_panels; + my $param_panels = {}; + my $libpath = bz_locations()->{'libpath'}; + foreach my $item ((glob "$libpath/Bugzilla/Config/*.pm")) { + $item =~ m#/([^/]+)\.pm$#; + my $module = $1; + $param_panels->{$module} = "Bugzilla::Config::$module" + unless $module eq 'Common'; + } + + # Now check for any hooked params + Bugzilla::Hook::process('config_add_panels', {panel_modules => $param_panels}); + return $param_panels; } sub SetParam { - my ($name, $value) = @_; + my ($name, $value) = @_; - _load_params unless %params; - die "Unknown param $name" unless (exists $params{$name}); + _load_params unless %params; + die "Unknown param $name" unless (exists $params{$name}); - my $entry = $params{$name}; + my $entry = $params{$name}; - # sanity check the value + # sanity check the value - # XXX - This runs the checks. Which would be good, except that - # check_shadowdb creates the database as a side effect, and so the - # checker fails the second time around... - if ($name ne 'shadowdb' && exists $entry->{'checker'}) { - my $err = $entry->{'checker'}->($value, $entry); - die "Param $name is not valid: $err" unless $err eq ''; - } + # XXX - This runs the checks. Which would be good, except that + # check_shadowdb creates the database as a side effect, and so the + # checker fails the second time around... + if ($name ne 'shadowdb' && exists $entry->{'checker'}) { + my $err = $entry->{'checker'}->($value, $entry); + die "Param $name is not valid: $err" unless $err eq ''; + } - Bugzilla->params->{$name} = $value; + Bugzilla->params->{$name} = $value; } sub update_params { - my ($params) = @_; - my $answer = Bugzilla->installation_answers; - my $datadir = bz_locations()->{'datadir'}; - my $param; - - # If the old data/params file using Data::Dumper output still exists, - # read it. It will be deleted once the parameters are stored in the new - # data/params.json file. - my $old_file = "$datadir/params"; - - if (-e $old_file) { - require Safe; - my $s = new Safe; - - $s->rdo($old_file); - die "Error reading $old_file: $!" if $!; - die "Error evaluating $old_file: $@" if $@; - - # Now read the param back out from the sandbox. - $param = \%{ $s->varglob('param') }; + my ($params) = @_; + my $answer = Bugzilla->installation_answers; + my $datadir = bz_locations()->{'datadir'}; + my $param; + + # If the old data/params file using Data::Dumper output still exists, + # read it. It will be deleted once the parameters are stored in the new + # data/params.json file. + my $old_file = "$datadir/params"; + + if (-e $old_file) { + require Safe; + my $s = new Safe; + + $s->rdo($old_file); + die "Error reading $old_file: $!" if $!; + die "Error evaluating $old_file: $@" if $@; + + # Now read the param back out from the sandbox. + $param = \%{$s->varglob('param')}; + } + else { + # Rename params.js to params.json if checksetup.pl + # was executed with an earlier version of this change + rename "$old_file.js", "$old_file.json" + if -e "$old_file.js" && !-e "$old_file.json"; + + # Read the new data/params.json file. + $param = read_param_file(); + } + + my %new_params; + + # If we didn't return any param values, then this is a new installation. + my $new_install = !(keys %$param); + + # --- UPDATE OLD PARAMS --- + + # Change from usebrowserinfo to defaultplatform/defaultopsys combo + if (exists $param->{'usebrowserinfo'}) { + if (!$param->{'usebrowserinfo'}) { + if (!exists $param->{'defaultplatform'}) { + $new_params{'defaultplatform'} = 'Other'; + } + if (!exists $param->{'defaultopsys'}) { + $new_params{'defaultopsys'} = 'Other'; + } } - else { - # Rename params.js to params.json if checksetup.pl - # was executed with an earlier version of this change - rename "$old_file.js", "$old_file.json" - if -e "$old_file.js" && !-e "$old_file.json"; - - # Read the new data/params.json file. - $param = read_param_file(); + } + + # Change from a boolean for quips to multi-state + if (exists $param->{'usequip'} && !exists $param->{'enablequips'}) { + $new_params{'enablequips'} = $param->{'usequip'} ? 'on' : 'off'; + } + + # Change from old product groups to controls for group_control_map + # 2002-10-14 bug 147275 bugreport@peshkin.net + if (exists $param->{'usebuggroups'} && !exists $param->{'makeproductgroups'}) { + $new_params{'makeproductgroups'} = $param->{'usebuggroups'}; + } + + # Modularise auth code + if (exists $param->{'useLDAP'} && !exists $param->{'loginmethod'}) { + $new_params{'loginmethod'} = $param->{'useLDAP'} ? "LDAP" : "DB"; + } + + # set verify method to whatever loginmethod was + if (exists $param->{'loginmethod'} && !exists $param->{'user_verify_class'}) { + $new_params{'user_verify_class'} = $param->{'loginmethod'}; + } + + # Remove quip-display control from parameters + # and give it to users via User Settings (Bug 41972) + if (exists $param->{'enablequips'} + && !exists $param->{'quip_list_entry_control'}) + { + my $new_value; + ($param->{'enablequips'} eq 'on') && do { $new_value = 'open'; }; + ($param->{'enablequips'} eq 'approved') && do { $new_value = 'moderated'; }; + ($param->{'enablequips'} eq 'frozen') && do { $new_value = 'closed'; }; + ($param->{'enablequips'} eq 'off') && do { $new_value = 'closed'; }; + $new_params{'quip_list_entry_control'} = $new_value; + } + + # Old mail_delivery_method choices contained no uppercase characters + my $mta = $param->{'mail_delivery_method'}; + if ($mta) { + if ($mta !~ /[A-Z]/) { + my %translation = ( + 'sendmail' => 'Sendmail', + 'smtp' => 'SMTP', + 'qmail' => 'Qmail', + 'testfile' => 'Test', + 'none' => 'None' + ); + $param->{'mail_delivery_method'} = $translation{$mta}; } - my %new_params; - - # If we didn't return any param values, then this is a new installation. - my $new_install = !(keys %$param); - - # --- UPDATE OLD PARAMS --- - - # Change from usebrowserinfo to defaultplatform/defaultopsys combo - if (exists $param->{'usebrowserinfo'}) { - if (!$param->{'usebrowserinfo'}) { - if (!exists $param->{'defaultplatform'}) { - $new_params{'defaultplatform'} = 'Other'; - } - if (!exists $param->{'defaultopsys'}) { - $new_params{'defaultopsys'} = 'Other'; - } - } + # This will force the parameter to be reset to its default value. + delete $param->{'mail_delivery_method'} + if $param->{'mail_delivery_method'} eq 'Qmail'; + } + + # Convert the old "ssl" parameter to the new "ssl_redirect" parameter. + # Both "authenticated sessions" and "always" turn on "ssl_redirect" + # when upgrading. + if (exists $param->{'ssl'} and $param->{'ssl'} ne 'never') { + $new_params{'ssl_redirect'} = 1; + } + +# "specific_search_allow_empty_words" has been renamed to "search_allow_no_criteria". + if (exists $param->{'specific_search_allow_empty_words'}) { + $new_params{'search_allow_no_criteria'} + = $param->{'specific_search_allow_empty_words'}; + } + + # --- DEFAULTS FOR NEW PARAMS --- + + _load_params unless %params; + foreach my $name (keys %params) { + my $item = $params{$name}; + unless (exists $param->{$name}) { + print "New parameter: $name\n" unless $new_install; + if (exists $new_params{$name}) { + $param->{$name} = $new_params{$name}; + } + elsif (exists $answer->{$name}) { + $param->{$name} = $answer->{$name}; + } + else { + $param->{$name} = $item->{'default'}; + } } + } - # Change from a boolean for quips to multi-state - if (exists $param->{'usequip'} && !exists $param->{'enablequips'}) { - $new_params{'enablequips'} = $param->{'usequip'} ? 'on' : 'off'; - } + $param->{'utf8'} = 1 if $new_install; - # Change from old product groups to controls for group_control_map - # 2002-10-14 bug 147275 bugreport@peshkin.net - if (exists $param->{'usebuggroups'} && - !exists $param->{'makeproductgroups'}) - { - $new_params{'makeproductgroups'} = $param->{'usebuggroups'}; - } + # Bug 452525: OR based groups are on by default for new installations + $param->{'or_groups'} = 1 if $new_install; - # Modularise auth code - if (exists $param->{'useLDAP'} && !exists $param->{'loginmethod'}) { - $new_params{'loginmethod'} = $param->{'useLDAP'} ? "LDAP" : "DB"; - } + # --- REMOVE OLD PARAMS --- - # set verify method to whatever loginmethod was - if (exists $param->{'loginmethod'} - && !exists $param->{'user_verify_class'}) - { - $new_params{'user_verify_class'} = $param->{'loginmethod'}; - } + my %oldparams; - # Remove quip-display control from parameters - # and give it to users via User Settings (Bug 41972) - if ( exists $param->{'enablequips'} - && !exists $param->{'quip_list_entry_control'}) - { - my $new_value; - ($param->{'enablequips'} eq 'on') && do {$new_value = 'open';}; - ($param->{'enablequips'} eq 'approved') && do {$new_value = 'moderated';}; - ($param->{'enablequips'} eq 'frozen') && do {$new_value = 'closed';}; - ($param->{'enablequips'} eq 'off') && do {$new_value = 'closed';}; - $new_params{'quip_list_entry_control'} = $new_value; + # Remove any old params + foreach my $item (keys %$param) { + if (!exists $params{$item}) { + $oldparams{$item} = delete $param->{$item}; } - - # Old mail_delivery_method choices contained no uppercase characters - my $mta = $param->{'mail_delivery_method'}; - if ($mta) { - if ($mta !~ /[A-Z]/) { - my %translation = ( - 'sendmail' => 'Sendmail', - 'smtp' => 'SMTP', - 'qmail' => 'Qmail', - 'testfile' => 'Test', - 'none' => 'None'); - $param->{'mail_delivery_method'} = $translation{$mta}; - } - # This will force the parameter to be reset to its default value. - delete $param->{'mail_delivery_method'} if $param->{'mail_delivery_method'} eq 'Qmail'; + } + + # Write any old parameters to old-params.txt + my $old_param_file = "$datadir/old-params.txt"; + if (scalar(keys %oldparams)) { + my $op_file = new IO::File($old_param_file, '>>', 0600) + || die "Couldn't create $old_param_file: $!"; + + print "The following parameters are no longer used in Bugzilla,", + " and so have been\nmoved from your parameters file into", + " $old_param_file:\n"; + + my $comma = ""; + foreach my $item (keys %oldparams) { + print $op_file "\n\n$item:\n" . $oldparams{$item} . "\n"; + print "${comma}$item"; + $comma = ", "; } + print "\n"; + $op_file->close; + } - # Convert the old "ssl" parameter to the new "ssl_redirect" parameter. - # Both "authenticated sessions" and "always" turn on "ssl_redirect" - # when upgrading. - if (exists $param->{'ssl'} and $param->{'ssl'} ne 'never') { - $new_params{'ssl_redirect'} = 1; - } + write_params($param); - # "specific_search_allow_empty_words" has been renamed to "search_allow_no_criteria". - if (exists $param->{'specific_search_allow_empty_words'}) { - $new_params{'search_allow_no_criteria'} = $param->{'specific_search_allow_empty_words'}; - } + if (-e $old_file) { + unlink $old_file; + say "$old_file has been converted into $old_file.json, using the JSON format."; + } - # --- DEFAULTS FOR NEW PARAMS --- - - _load_params unless %params; - foreach my $name (keys %params) { - my $item = $params{$name}; - unless (exists $param->{$name}) { - print "New parameter: $name\n" unless $new_install; - if (exists $new_params{$name}) { - $param->{$name} = $new_params{$name}; - } - elsif (exists $answer->{$name}) { - $param->{$name} = $answer->{$name}; - } - else { - $param->{$name} = $item->{'default'}; - } - } - } - - $param->{'utf8'} = 1 if $new_install; - - # Bug 452525: OR based groups are on by default for new installations - $param->{'or_groups'} = 1 if $new_install; - - # --- REMOVE OLD PARAMS --- - - my %oldparams; - # Remove any old params - foreach my $item (keys %$param) { - if (!exists $params{$item}) { - $oldparams{$item} = delete $param->{$item}; - } - } - - # Write any old parameters to old-params.txt - my $old_param_file = "$datadir/old-params.txt"; - if (scalar(keys %oldparams)) { - my $op_file = new IO::File($old_param_file, '>>', 0600) - || die "Couldn't create $old_param_file: $!"; - - print "The following parameters are no longer used in Bugzilla,", - " and so have been\nmoved from your parameters file into", - " $old_param_file:\n"; - - my $comma = ""; - foreach my $item (keys %oldparams) { - print $op_file "\n\n$item:\n" . $oldparams{$item} . "\n"; - print "${comma}$item"; - $comma = ", "; - } - print "\n"; - $op_file->close; - } - - write_params($param); - - if (-e $old_file) { - unlink $old_file; - say "$old_file has been converted into $old_file.json, using the JSON format."; - } - - # Return deleted params and values so that checksetup.pl has a chance - # to convert old params to new data. - return %oldparams; + # Return deleted params and values so that checksetup.pl has a chance + # to convert old params to new data. + return %oldparams; } sub write_params { - my ($param_data) = @_; - $param_data ||= Bugzilla->params; - my $param_file = bz_locations()->{'datadir'} . '/params.json'; + my ($param_data) = @_; + $param_data ||= Bugzilla->params; + my $param_file = bz_locations()->{'datadir'} . '/params.json'; - my $json_data = JSON::XS->new->canonical->pretty->encode($param_data); - write_text($param_file, $json_data); + my $json_data = JSON::XS->new->canonical->pretty->encode($param_data); + write_text($param_file, $json_data); - # It's not common to edit parameters and loading - # Bugzilla::Install::Filesystem is slow. - require Bugzilla::Install::Filesystem; - Bugzilla::Install::Filesystem::fix_file_permissions($param_file); + # It's not common to edit parameters and loading + # Bugzilla::Install::Filesystem is slow. + require Bugzilla::Install::Filesystem; + Bugzilla::Install::Filesystem::fix_file_permissions($param_file); - # And now we have to reset the params cache so that Bugzilla will re-read - # them. - delete Bugzilla->request_cache->{params}; + # And now we have to reset the params cache so that Bugzilla will re-read + # them. + delete Bugzilla->request_cache->{params}; } sub read_param_file { - my %params; - my $file = bz_locations()->{'datadir'} . '/params.json'; - - if (-e $file) { - 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 - # to all users in its error message, so we have to eval'uate it. - %params = eval { %{JSON::XS->new->decode($data)} }; - if ($@) { - my $error_msg = (basename($0) eq 'checksetup.pl') ? - $@ : 'run checksetup.pl to see the details.'; - die "Error parsing $file: $error_msg"; - } - # JSON::XS doesn't detaint data for us. - foreach my $key (keys %params) { - if (ref($params{$key}) eq "ARRAY") { - foreach my $item (@{$params{$key}}) { - trick_taint($item); - } - } else { - trick_taint($params{$key}) if defined $params{$key}; - } - } + my %params; + my $file = bz_locations()->{'datadir'} . '/params.json'; + + if (-e $file) { + 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 + # to all users in its error message, so we have to eval'uate it. + %params = eval { %{JSON::XS->new->decode($data)} }; + if ($@) { + my $error_msg + = (basename($0) eq 'checksetup.pl') + ? $@ + : 'run checksetup.pl to see the details.'; + die "Error parsing $file: $error_msg"; } - elsif ($ENV{'SERVER_SOFTWARE'}) { - # We're in a CGI, but the params file doesn't exist. We can't - # Template Toolkit, or even install_string, since checksetup - # might not have thrown an error. Bugzilla::CGI->new - # hasn't even been called yet, so we manually use CGI::Carp here - # so that the user sees the error. - require CGI::Carp; - CGI::Carp->import('fatalsToBrowser'); - die "The $file file does not exist." - . ' You probably need to run checksetup.pl.', + + # JSON::XS doesn't detaint data for us. + foreach my $key (keys %params) { + if (ref($params{$key}) eq "ARRAY") { + foreach my $item (@{$params{$key}}) { + trick_taint($item); + } + } + else { + trick_taint($params{$key}) if defined $params{$key}; + } } - return \%params; + } + elsif ($ENV{'SERVER_SOFTWARE'}) { + + # We're in a CGI, but the params file doesn't exist. We can't + # Template Toolkit, or even install_string, since checksetup + # might not have thrown an error. Bugzilla::CGI->new + # hasn't even been called yet, so we manually use CGI::Carp here + # so that the user sees the error. + require CGI::Carp; + CGI::Carp->import('fatalsToBrowser'); + die "The $file file does not exist." + . ' You probably need to run checksetup.pl.',; + } + return \%params; } 1; diff --git a/Bugzilla/Config/Admin.pm b/Bugzilla/Config/Admin.pm index 41d929298..fe19d7cf0 100644 --- a/Bugzilla/Config/Admin.pm +++ b/Bugzilla/Config/Admin.pm @@ -16,32 +16,21 @@ use Bugzilla::Config::Common; our $sortkey = 200; sub get_param_list { - my $class = shift; + my $class = shift; my @param_list = ( - { - name => 'allowbugdeletion', - type => 'b', - default => 0 - }, - - { - name => 'allowemailchange', - type => 'b', - default => 1 - }, - - { - name => 'allowuserdeletion', - type => 'b', - default => 0 - }, - - { - name => 'last_visit_keep_days', - type => 't', - default => 10, - checker => \&check_numeric - }); + {name => 'allowbugdeletion', type => 'b', default => 0}, + + {name => 'allowemailchange', type => 'b', default => 1}, + + {name => 'allowuserdeletion', type => 'b', default => 0}, + + { + name => 'last_visit_keep_days', + type => 't', + default => 10, + checker => \&check_numeric + } + ); return @param_list; } diff --git a/Bugzilla/Config/Advanced.pm b/Bugzilla/Config/Advanced.pm index 8356c3361..043a892d7 100644 --- a/Bugzilla/Config/Advanced.pm +++ b/Bugzilla/Config/Advanced.pm @@ -16,31 +16,18 @@ use Bugzilla::Config::Common; our $sortkey = 1700; use constant get_param_list => ( - { - name => 'cookiedomain', - type => 't', - default => '' - }, + {name => 'cookiedomain', type => 't', default => ''}, - { - name => 'inbound_proxies', - type => 't', - default => '', - checker => \&check_ip - }, + {name => 'inbound_proxies', type => 't', default => '', checker => \&check_ip}, - { - name => 'proxy_url', - type => 't', - default => '' - }, + {name => 'proxy_url', type => 't', default => ''}, { - name => 'strict_transport_security', - type => 's', - choices => ['off', 'this_domain_only', 'include_subdomains'], - default => 'off', - checker => \&check_multi + name => 'strict_transport_security', + type => 's', + choices => ['off', 'this_domain_only', 'include_subdomains'], + default => 'off', + checker => \&check_multi }, ); diff --git a/Bugzilla/Config/Attachment.pm b/Bugzilla/Config/Attachment.pm index 580ec46d9..0cf4b768a 100644 --- a/Bugzilla/Config/Attachment.pm +++ b/Bugzilla/Config/Attachment.pm @@ -16,48 +16,41 @@ use Bugzilla::Config::Common; our $sortkey = 400; sub get_param_list { - my $class = shift; + my $class = shift; my @param_list = ( - { - name => 'allow_attachment_display', - type => 'b', - default => 0 - }, - - { - name => 'attachment_base', - type => 't', - default => '', - checker => \&check_urlbase - }, - - { - name => 'allow_attachment_deletion', - type => 'b', - default => 0 - }, - - { - name => 'maxattachmentsize', - type => 't', - default => '1000', - checker => \&check_maxattachmentsize - }, - - # The maximum size (in bytes) for patches and non-patch attachments. - # The default limit is 1000KB, which is 24KB less than mysql's default - # maximum packet size (which determines how much data can be sent in a - # single mysql packet and thus how much data can be inserted into the - # database) to provide breathing space for the data in other fields of - # the attachment record as well as any mysql packet overhead (I don't - # know of any, but I suspect there may be some.) - - { - name => 'maxlocalattachment', - type => 't', - default => '0', - checker => \&check_numeric - } ); + {name => 'allow_attachment_display', type => 'b', default => 0}, + + { + name => 'attachment_base', + type => 't', + default => '', + checker => \&check_urlbase + }, + + {name => 'allow_attachment_deletion', type => 'b', default => 0}, + + { + name => 'maxattachmentsize', + type => 't', + default => '1000', + checker => \&check_maxattachmentsize + }, + + # The maximum size (in bytes) for patches and non-patch attachments. + # The default limit is 1000KB, which is 24KB less than mysql's default + # maximum packet size (which determines how much data can be sent in a + # single mysql packet and thus how much data can be inserted into the + # database) to provide breathing space for the data in other fields of + # the attachment record as well as any mysql packet overhead (I don't + # know of any, but I suspect there may be some.) + + { + name => 'maxlocalattachment', + type => 't', + default => '0', + checker => \&check_numeric + } + ); return @param_list; } diff --git a/Bugzilla/Config/Auth.pm b/Bugzilla/Config/Auth.pm index 78d719b15..09e81339f 100644 --- a/Bugzilla/Config/Auth.pm +++ b/Bugzilla/Config/Auth.pm @@ -16,111 +16,85 @@ use Bugzilla::Config::Common; our $sortkey = 300; sub get_param_list { - my $class = shift; + my $class = shift; my @param_list = ( - { - name => 'auth_env_id', - type => 't', - default => '', - }, - - { - name => 'auth_env_email', - type => 't', - default => '', - }, - - { - name => 'auth_env_realname', - type => 't', - default => '', - }, - - # XXX in the future: - # - # user_verify_class and user_info_class should have choices gathered from - # whatever sits in their respective directories - # - # rather than comma-separated lists, these two should eventually become - # arrays, but that requires alterations to editparams first - - { - name => 'user_info_class', - type => 's', - choices => [ 'CGI', 'Env', 'Env,CGI' ], - default => 'CGI', - checker => \&check_multi - }, - - { - name => 'user_verify_class', - type => 'o', - choices => [ 'DB', 'RADIUS', 'LDAP' ], - default => 'DB', - checker => \&check_user_verify_class - }, - - { - name => 'rememberlogin', - type => 's', - choices => ['on', 'defaulton', 'defaultoff', 'off'], - default => 'on', - checker => \&check_multi - }, - - { - name => 'requirelogin', - type => 'b', - default => '0' - }, - - { - name => 'webservice_email_filter', - type => 'b', - default => 0 - }, - - { - name => 'emailregexp', - type => 't', - default => q:^[\\w\\.\\+\\-=']+@[\\w\\.\\-]+\\.[\\w\\-]+$:, - checker => \&check_regexp - }, - - { - name => 'emailregexpdesc', - type => 'l', - default => 'A legal address must contain exactly one \'@\', and at least ' . - 'one \'.\' after the @.' - }, - - { - name => 'emailsuffix', - type => 't', - default => '' - }, - - { - name => 'createemailregexp', - type => 't', - default => q:.*:, - checker => \&check_regexp - }, - - { - name => 'password_complexity', - type => 's', - choices => [ 'no_constraints', 'mixed_letters', 'letters_numbers', - 'letters_numbers_specialchars' ], - default => 'no_constraints', - checker => \&check_multi - }, - - { - name => 'password_check_on_login', - type => 'b', - default => '1' - }, + {name => 'auth_env_id', type => 't', default => '',}, + + {name => 'auth_env_email', type => 't', default => '',}, + + {name => 'auth_env_realname', type => 't', default => '',}, + + # XXX in the future: + # + # user_verify_class and user_info_class should have choices gathered from + # whatever sits in their respective directories + # + # rather than comma-separated lists, these two should eventually become + # arrays, but that requires alterations to editparams first + + { + name => 'user_info_class', + type => 's', + choices => ['CGI', 'Env', 'Env,CGI'], + default => 'CGI', + checker => \&check_multi + }, + + { + name => 'user_verify_class', + type => 'o', + choices => ['DB', 'RADIUS', 'LDAP'], + default => 'DB', + checker => \&check_user_verify_class + }, + + { + name => 'rememberlogin', + type => 's', + choices => ['on', 'defaulton', 'defaultoff', 'off'], + default => 'on', + checker => \&check_multi + }, + + {name => 'requirelogin', type => 'b', default => '0'}, + + {name => 'webservice_email_filter', type => 'b', default => 0}, + + { + name => 'emailregexp', + type => 't', + default => q:^[\\w\\.\\+\\-=']+@[\\w\\.\\-]+\\.[\\w\\-]+$:, + checker => \&check_regexp + }, + + { + name => 'emailregexpdesc', + type => 'l', + default => 'A legal address must contain exactly one \'@\', and at least ' + . 'one \'.\' after the @.' + }, + + {name => 'emailsuffix', type => 't', default => ''}, + + { + name => 'createemailregexp', + type => 't', + default => q:.*:, + checker => \&check_regexp + }, + + { + name => 'password_complexity', + type => 's', + choices => [ + 'no_constraints', 'mixed_letters', + 'letters_numbers', 'letters_numbers_specialchars' + ], + default => 'no_constraints', + checker => \&check_multi + }, + + {name => 'password_check_on_login', type => 'b', default => '1'}, ); return @param_list; } diff --git a/Bugzilla/Config/BugChange.pm b/Bugzilla/Config/BugChange.pm index 0acdc0ce4..ad1cafefc 100644 --- a/Bugzilla/Config/BugChange.pm +++ b/Bugzilla/Config/BugChange.pm @@ -26,55 +26,33 @@ sub get_param_list { # and bug_status.is_open is not yet defined (hence the eval), so we use # the bug statuses above as they are still hardcoded. eval { - my @current_closed_states = map {$_->name} closed_bug_statuses(); - # If no closed state was found, use the default list above. - @closed_bug_statuses = @current_closed_states if scalar(@current_closed_states); + my @current_closed_states = map { $_->name } closed_bug_statuses(); + + # If no closed state was found, use the default list above. + @closed_bug_statuses = @current_closed_states if scalar(@current_closed_states); }; my @param_list = ( - { - name => 'duplicate_or_move_bug_status', - type => 's', - choices => \@closed_bug_statuses, - default => $closed_bug_statuses[0], - checker => \&check_bug_status - }, - - { - name => 'letsubmitterchoosepriority', - type => 'b', - default => 1 - }, - - { - name => 'letsubmitterchoosemilestone', - type => 'b', - default => 1 - }, - - { - name => 'musthavemilestoneonaccept', - type => 'b', - default => 0 - }, - - { - name => 'commentonchange_resolution', - type => 'b', - default => 0 - }, - - { - name => 'commentonduplicate', - type => 'b', - default => 0 - }, - - { - name => 'noresolveonopenblockers', - type => 'b', - default => 0, - } ); + { + name => 'duplicate_or_move_bug_status', + type => 's', + choices => \@closed_bug_statuses, + default => $closed_bug_statuses[0], + checker => \&check_bug_status + }, + + {name => 'letsubmitterchoosepriority', type => 'b', default => 1}, + + {name => 'letsubmitterchoosemilestone', type => 'b', default => 1}, + + {name => 'musthavemilestoneonaccept', type => 'b', default => 0}, + + {name => 'commentonchange_resolution', type => 'b', default => 0}, + + {name => 'commentonduplicate', type => 'b', default => 0}, + + {name => 'noresolveonopenblockers', type => 'b', default => 0,} + ); return @param_list; } diff --git a/Bugzilla/Config/BugFields.pm b/Bugzilla/Config/BugFields.pm index ef2faa64b..1659dc66a 100644 --- a/Bugzilla/Config/BugFields.pm +++ b/Bugzilla/Config/BugFields.pm @@ -25,73 +25,50 @@ sub get_param_list { my @legal_OS = @{get_legal_field_values('op_sys')}; my @param_list = ( - { - name => 'useclassification', - type => 'b', - default => 0 - }, - - { - name => 'usetargetmilestone', - type => 'b', - default => 0 - }, - - { - name => 'useqacontact', - type => 'b', - default => 0 - }, - - { - name => 'usestatuswhiteboard', - type => 'b', - default => 0 - }, - - { - name => 'use_see_also', - type => 'b', - default => 1 - }, - - { - name => 'defaultpriority', - type => 's', - choices => \@legal_priorities, - default => $legal_priorities[-1], - checker => \&check_priority - }, - - { - name => 'defaultseverity', - type => 's', - choices => \@legal_severities, - default => $legal_severities[-1], - checker => \&check_severity - }, - - { - name => 'defaultplatform', - type => 's', - choices => ['', @legal_platforms], - default => '', - checker => \&check_platform - }, - - { - name => 'defaultopsys', - type => 's', - choices => ['', @legal_OS], - default => '', - checker => \&check_opsys - }, - - { - name => 'collapsed_comment_tags', - type => 't', - default => 'obsolete, spam', - }); + {name => 'useclassification', type => 'b', default => 0}, + + {name => 'usetargetmilestone', type => 'b', default => 0}, + + {name => 'useqacontact', type => 'b', default => 0}, + + {name => 'usestatuswhiteboard', type => 'b', default => 0}, + + {name => 'use_see_also', type => 'b', default => 1}, + + { + name => 'defaultpriority', + type => 's', + choices => \@legal_priorities, + default => $legal_priorities[-1], + checker => \&check_priority + }, + + { + name => 'defaultseverity', + type => 's', + choices => \@legal_severities, + default => $legal_severities[-1], + checker => \&check_severity + }, + + { + name => 'defaultplatform', + type => 's', + choices => ['', @legal_platforms], + default => '', + checker => \&check_platform + }, + + { + name => 'defaultopsys', + type => 's', + choices => ['', @legal_OS], + default => '', + checker => \&check_opsys + }, + + {name => 'collapsed_comment_tags', type => 't', default => 'obsolete, spam',} + ); return @param_list; } diff --git a/Bugzilla/Config/Common.pm b/Bugzilla/Config/Common.pm index bd9b0bf84..756dbb0dd 100644 --- a/Bugzilla/Config/Common.pm +++ b/Bugzilla/Config/Common.pm @@ -21,392 +21,406 @@ use Bugzilla::Group; use Bugzilla::Status; use parent qw(Exporter); -@Bugzilla::Config::Common::EXPORT = - 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_font_file - check_mail_delivery_method check_notification check_utf8 - check_bug_status check_smtp_auth check_theschwartz_available - check_maxattachmentsize check_email check_smtp_ssl - check_comment_taggers_group check_smtp_server +@Bugzilla::Config::Common::EXPORT + = 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_font_file + check_mail_delivery_method check_notification check_utf8 + check_bug_status check_smtp_auth check_theschwartz_available + check_maxattachmentsize check_email check_smtp_ssl + check_comment_taggers_group check_smtp_server ); # Checking functions for the various values sub check_multi { - my ($value, $param) = (@_); + my ($value, $param) = (@_); - if ($param->{'type'} eq "s") { - unless (scalar(grep {$_ eq $value} (@{$param->{'choices'}}))) { - return "Invalid choice '$value' for single-select list param '$param->{'name'}'"; - } - - return ""; + if ($param->{'type'} eq "s") { + unless (scalar(grep { $_ eq $value } (@{$param->{'choices'}}))) { + return + "Invalid choice '$value' for single-select list param '$param->{'name'}'"; } - elsif ($param->{'type'} eq 'm' || $param->{'type'} eq 'o') { - if (ref($value) ne "ARRAY") { - $value = [split(',', $value)] - } - foreach my $chkParam (@$value) { - unless (scalar(grep {$_ eq $chkParam} (@{$param->{'choices'}}))) { - return "Invalid choice '$chkParam' for multi-select list param '$param->{'name'}'"; - } - } - - return ""; + + return ""; + } + elsif ($param->{'type'} eq 'm' || $param->{'type'} eq 'o') { + if (ref($value) ne "ARRAY") { + $value = [split(',', $value)]; } - else { - return "Invalid param type '$param->{'type'}' for check_multi(); " . - "contact your Bugzilla administrator"; + foreach my $chkParam (@$value) { + unless (scalar(grep { $_ eq $chkParam } (@{$param->{'choices'}}))) { + return + "Invalid choice '$chkParam' for multi-select list param '$param->{'name'}'"; + } } + + return ""; + } + else { + return "Invalid param type '$param->{'type'}' for check_multi(); " + . "contact your Bugzilla administrator"; + } } sub check_numeric { - my ($value) = (@_); - if ($value !~ /^[0-9]+$/) { - return "must be a numeric value"; - } - return ""; + my ($value) = (@_); + if ($value !~ /^[0-9]+$/) { + return "must be a numeric value"; + } + return ""; } sub check_regexp { - my ($value) = (@_); - eval { qr/$value/ }; - return $@; + my ($value) = (@_); + eval {qr/$value/}; + return $@; } sub check_email { - my ($value) = @_; - if ($value !~ $Email::Address::mailbox) { - return "must be a valid email address."; - } - return ""; + my ($value) = @_; + if ($value !~ $Email::Address::mailbox) { + return "must be a valid email address."; + } + return ""; } sub check_sslbase { - my $url = shift; - if ($url ne '') { - if ($url !~ m#^https://([^/]+).*/$#) { - return "must be a legal URL, that starts with https and ends with a slash."; - } - my $host = $1; - # Fall back to port 443 if for some reason getservbyname() fails. - my $port = getservbyname('https', 'tcp') || 443; - if ($host =~ /^(.+):(\d+)$/) { - $host = $1; - $port = $2; - } - local *SOCK; - my $proto = getprotobyname('tcp'); - socket(SOCK, PF_INET, SOCK_STREAM, $proto); - my $iaddr = inet_aton($host) || return "The host $host cannot be resolved"; - my $sin = sockaddr_in($port, $iaddr); - if (!connect(SOCK, $sin)) { - return "Failed to connect to $host:$port ($!); unable to enable SSL"; - } - close(SOCK); - } - return ""; + my $url = shift; + if ($url ne '') { + if ($url !~ m#^https://([^/]+).*/$#) { + return "must be a legal URL, that starts with https and ends with a slash."; + } + my $host = $1; + + # Fall back to port 443 if for some reason getservbyname() fails. + my $port = getservbyname('https', 'tcp') || 443; + if ($host =~ /^(.+):(\d+)$/) { + $host = $1; + $port = $2; + } + local *SOCK; + my $proto = getprotobyname('tcp'); + socket(SOCK, PF_INET, SOCK_STREAM, $proto); + my $iaddr = inet_aton($host) || return "The host $host cannot be resolved"; + my $sin = sockaddr_in($port, $iaddr); + if (!connect(SOCK, $sin)) { + return "Failed to connect to $host:$port ($!); unable to enable SSL"; + } + close(SOCK); + } + return ""; } sub check_ip { - my $inbound_proxies = shift; - my @proxies = split(/[\s,]+/, $inbound_proxies); - foreach my $proxy (@proxies) { - validate_ip($proxy) || return "$proxy is not a valid IPv4 or IPv6 address"; - } - return ""; + my $inbound_proxies = shift; + my @proxies = split(/[\s,]+/, $inbound_proxies); + foreach my $proxy (@proxies) { + validate_ip($proxy) || return "$proxy is not a valid IPv4 or IPv6 address"; + } + return ""; } sub check_utf8 { - my $utf8 = shift; - # You cannot turn off the UTF-8 parameter if you've already converted - # your tables to utf-8. - my $dbh = Bugzilla->dbh; - if ($dbh->isa('Bugzilla::DB::Mysql') && $dbh->bz_db_is_utf8 && !$utf8) { - return "You cannot disable UTF-8 support, because your MySQL database" - . " is encoded in UTF-8"; - } - return ""; + my $utf8 = shift; + + # You cannot turn off the UTF-8 parameter if you've already converted + # your tables to utf-8. + my $dbh = Bugzilla->dbh; + if ($dbh->isa('Bugzilla::DB::Mysql') && $dbh->bz_db_is_utf8 && !$utf8) { + return "You cannot disable UTF-8 support, because your MySQL database" + . " is encoded in UTF-8"; + } + return ""; } sub check_priority { - my ($value) = (@_); - my $legal_priorities = get_legal_field_values('priority'); - if (!grep($_ eq $value, @$legal_priorities)) { - return "Must be a legal priority value: one of " . - join(", ", @$legal_priorities); - } - return ""; + my ($value) = (@_); + my $legal_priorities = get_legal_field_values('priority'); + if (!grep($_ eq $value, @$legal_priorities)) { + return "Must be a legal priority value: one of " + . join(", ", @$legal_priorities); + } + return ""; } sub check_severity { - my ($value) = (@_); - my $legal_severities = get_legal_field_values('bug_severity'); - if (!grep($_ eq $value, @$legal_severities)) { - return "Must be a legal severity value: one of " . - join(", ", @$legal_severities); - } - return ""; + my ($value) = (@_); + my $legal_severities = get_legal_field_values('bug_severity'); + if (!grep($_ eq $value, @$legal_severities)) { + return "Must be a legal severity value: one of " + . join(", ", @$legal_severities); + } + return ""; } sub check_platform { - my ($value) = (@_); - my $legal_platforms = get_legal_field_values('rep_platform'); - if (!grep($_ eq $value, '', @$legal_platforms)) { - return "Must be empty or a legal platform value: one of " . - join(", ", @$legal_platforms); - } - return ""; + my ($value) = (@_); + my $legal_platforms = get_legal_field_values('rep_platform'); + if (!grep($_ eq $value, '', @$legal_platforms)) { + return "Must be empty or a legal platform value: one of " + . join(", ", @$legal_platforms); + } + return ""; } sub check_opsys { - my ($value) = (@_); - my $legal_OS = get_legal_field_values('op_sys'); - if (!grep($_ eq $value, '', @$legal_OS)) { - return "Must be empty or a legal operating system value: one of " . - join(", ", @$legal_OS); - } - return ""; + my ($value) = (@_); + my $legal_OS = get_legal_field_values('op_sys'); + if (!grep($_ eq $value, '', @$legal_OS)) { + return "Must be empty or a legal operating system value: one of " + . join(", ", @$legal_OS); + } + return ""; } sub check_bug_status { - my $bug_status = shift; - my @closed_bug_statuses = map {$_->name} closed_bug_statuses(); - if (!grep($_ eq $bug_status, @closed_bug_statuses)) { - return "Must be a valid closed status: one of " . join(', ', @closed_bug_statuses); - } - return ""; + my $bug_status = shift; + my @closed_bug_statuses = map { $_->name } closed_bug_statuses(); + if (!grep($_ eq $bug_status, @closed_bug_statuses)) { + return "Must be a valid closed status: one of " + . join(', ', @closed_bug_statuses); + } + return ""; } sub check_group { - my $group_name = shift; - return "" unless $group_name; - my $group = new Bugzilla::Group({'name' => $group_name}); - unless (defined $group) { - return "Must be an existing group name"; - } - return ""; + my $group_name = shift; + return "" unless $group_name; + my $group = new Bugzilla::Group({'name' => $group_name}); + unless (defined $group) { + return "Must be an existing group name"; + } + return ""; } sub check_shadowdb { - my ($value) = (@_); - $value = trim($value); - if ($value eq "") { - return ""; - } + my ($value) = (@_); + $value = trim($value); + if ($value eq "") { + return ""; + } - if (!Bugzilla->params->{'shadowdbhost'}) { - return "You need to specify a host when using a shadow database"; - } + if (!Bugzilla->params->{'shadowdbhost'}) { + return "You need to specify a host when using a shadow database"; + } - # Can't test existence of this because ConnectToDatabase uses the param, - # but we can't set this before testing.... - # This can really only be fixed after we can use the DBI more openly - return ""; + # Can't test existence of this because ConnectToDatabase uses the param, + # but we can't set this before testing.... + # This can really only be fixed after we can use the DBI more openly + return ""; } sub check_urlbase { - my ($url) = (@_); - if ($url && $url !~ m:^http.*/$:) { - return "must be a legal URL, that starts with http and ends with a slash."; - } - return ""; + my ($url) = (@_); + if ($url && $url !~ m:^http.*/$:) { + return "must be a legal URL, that starts with http and ends with a slash."; + } + return ""; } sub check_url { - my ($url) = (@_); - return '' if $url eq ''; # Allow empty URLs - if ($url !~ m:/$:) { - return 'must be a legal URL, absolute or relative, ending with a slash.'; - } - return ''; + my ($url) = (@_); + return '' if $url eq ''; # Allow empty URLs + if ($url !~ m:/$:) { + return 'must be a legal URL, absolute or relative, ending with a slash.'; + } + return ''; } sub check_webdotbase { - my ($value) = (@_); - $value = trim($value); - if ($value eq "") { - return ""; - } - if($value !~ /^https?:/) { - if(! -x $value) { - return "The file path \"$value\" is not a valid executable. Please specify the complete file path to 'dot' if you intend to generate graphs locally."; - } - # Check .htaccess allows access to generated images - my $webdotdir = bz_locations()->{'webdotdir'}; - if(-e "$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"; - } - close HTACCESS; - } - } + my ($value) = (@_); + $value = trim($value); + if ($value eq "") { return ""; + } + if ($value !~ /^https?:/) { + if (!-x $value) { + return + "The file path \"$value\" is not a valid executable. Please specify the complete file path to 'dot' if you intend to generate graphs locally."; + } + + # Check .htaccess allows access to generated images + my $webdotdir = bz_locations()->{'webdotdir'}; + if (-e "$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"; + } + close HTACCESS; + } + } + return ""; } sub check_font_file { - my ($font) = @_; - $font = trim($font); - return '' unless $font; - - if ($font !~ /\.(ttf|otf)$/) { - return "The file must point to a TrueType or OpenType font file (its extension must be .ttf or .otf)" - } - if (! -f $font) { - return "The file '$font' cannot be found. Make sure you typed the full path to the file" - } - return ''; + my ($font) = @_; + $font = trim($font); + return '' unless $font; + + if ($font !~ /\.(ttf|otf)$/) { + return + "The file must point to a TrueType or OpenType font file (its extension must be .ttf or .otf)"; + } + if (!-f $font) { + return + "The file '$font' cannot be found. Make sure you typed the full path to the file"; + } + return ''; } sub check_user_verify_class { - # doeditparams traverses the list of params, and for each one it checks, - # then updates. This means that if one param checker wants to look at - # other params, it must be below that other one. So you can't have two - # params mutually dependent on each other. - # This means that if someone clears the LDAP config params after setting - # the login method as LDAP, we won't notice, but all logins will fail. - # So don't do that. - - my $params = Bugzilla->params; - my ($list, $entry) = @_; - $list || return 'You need to specify at least one authentication mechanism'; - for my $class (split /,\s*/, $list) { - my $res = check_multi($class, $entry); - return $res if $res; - if ($class eq 'RADIUS') { - if (!Bugzilla->feature('auth_radius')) { - return "RADIUS support is not available. Run checksetup.pl" - . " for more details"; - } - return "RADIUS servername (RADIUS_server) is missing" - if !$params->{"RADIUS_server"}; - return "RADIUS_secret is empty" if !$params->{"RADIUS_secret"}; - } - elsif ($class eq 'LDAP') { - if (!Bugzilla->feature('auth_ldap')) { - return "LDAP support is not available. Run checksetup.pl" - . " for more details"; - } - return "LDAP servername (LDAPserver) is missing" - if !$params->{"LDAPserver"}; - return "LDAPBaseDN is empty" if !$params->{"LDAPBaseDN"}; - } - } - return ""; + + # doeditparams traverses the list of params, and for each one it checks, + # then updates. This means that if one param checker wants to look at + # other params, it must be below that other one. So you can't have two + # params mutually dependent on each other. + # This means that if someone clears the LDAP config params after setting + # the login method as LDAP, we won't notice, but all logins will fail. + # So don't do that. + + my $params = Bugzilla->params; + my ($list, $entry) = @_; + $list || return 'You need to specify at least one authentication mechanism'; + for my $class (split /,\s*/, $list) { + my $res = check_multi($class, $entry); + return $res if $res; + if ($class eq 'RADIUS') { + if (!Bugzilla->feature('auth_radius')) { + return "RADIUS support is not available. Run checksetup.pl" + . " for more details"; + } + return "RADIUS servername (RADIUS_server) is missing" + if !$params->{"RADIUS_server"}; + return "RADIUS_secret is empty" if !$params->{"RADIUS_secret"}; + } + elsif ($class eq 'LDAP') { + if (!Bugzilla->feature('auth_ldap')) { + return "LDAP support is not available. Run checksetup.pl" . " for more details"; + } + return "LDAP servername (LDAPserver) is missing" if !$params->{"LDAPserver"}; + return "LDAPBaseDN is empty" if !$params->{"LDAPBaseDN"}; + } + } + return ""; } sub check_mail_delivery_method { - my $check = check_multi(@_); - return $check if $check; - my $mailer = shift; - if ($mailer eq 'Sendmail' and ON_WINDOWS) { - # look for sendmail.exe - return "Failed to locate " . SENDMAIL_EXE - unless -e SENDMAIL_EXE; - } - return ""; + my $check = check_multi(@_); + return $check if $check; + my $mailer = shift; + if ($mailer eq 'Sendmail' and ON_WINDOWS) { + + # look for sendmail.exe + return "Failed to locate " . SENDMAIL_EXE unless -e SENDMAIL_EXE; + } + return ""; } sub check_maxattachmentsize { - my $check = check_numeric(@_); - return $check if $check; - my $size = shift; - my $dbh = Bugzilla->dbh; - if ($dbh->isa('Bugzilla::DB::Mysql')) { - my (undef, $max_packet) = $dbh->selectrow_array( - q{SHOW VARIABLES LIKE 'max\_allowed\_packet'}); - my $byte_size = $size * 1024; - if ($max_packet < $byte_size) { - return "You asked for a maxattachmentsize of $byte_size bytes," - . " but the max_allowed_packet setting in MySQL currently" - . " only allows packets up to $max_packet bytes"; - } - } - return ""; + my $check = check_numeric(@_); + return $check if $check; + my $size = shift; + my $dbh = Bugzilla->dbh; + if ($dbh->isa('Bugzilla::DB::Mysql')) { + my (undef, $max_packet) + = $dbh->selectrow_array(q{SHOW VARIABLES LIKE 'max\_allowed\_packet'}); + my $byte_size = $size * 1024; + if ($max_packet < $byte_size) { + return + "You asked for a maxattachmentsize of $byte_size bytes," + . " but the max_allowed_packet setting in MySQL currently" + . " only allows packets up to $max_packet bytes"; + } + } + return ""; } sub check_notification { - my $option = shift; - my @current_version = - (BUGZILLA_VERSION =~ m/^(\d+)\.(\d+)(?:(rc|\.)(\d+))?\+?$/); - if ($current_version[1] % 2 && $option eq 'stable_branch_release') { - return "You are currently running a development snapshot, and so your " . - "installation is not based on a branch. If you want to be notified " . - "about the next stable release, you should select " . - "'latest_stable_release' instead"; - } - if ($option ne 'disabled' && !Bugzilla->feature('updates')) { - return "Some Perl modules are missing to get notifications about " . - "new releases. See the output of checksetup.pl for more information"; - } - return ""; + my $option = shift; + my @current_version + = (BUGZILLA_VERSION =~ m/^(\d+)\.(\d+)(?:(rc|\.)(\d+))?\+?$/); + if ($current_version[1] % 2 && $option eq 'stable_branch_release') { + return + "You are currently running a development snapshot, and so your " + . "installation is not based on a branch. If you want to be notified " + . "about the next stable release, you should select " + . "'latest_stable_release' instead"; + } + if ($option ne 'disabled' && !Bugzilla->feature('updates')) { + return "Some Perl modules are missing to get notifications about " + . "new releases. See the output of checksetup.pl for more information"; + } + return ""; } sub check_smtp_server { - my $host = shift; - my $port; - - return '' unless $host; - - 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)"; - } - } - trick_taint($host); - # Let's first try to connect using SSL. If this fails, we fall back to - # an unencrypted connection. - foreach my $method (['Net::SMTP::SSL', 465], ['Net::SMTP', 25]) { - my ($class, $default_port) = @$method; - next if $class eq 'Net::SMTP::SSL' && !Bugzilla->feature('smtp_ssl'); - eval "require $class"; - my $smtp = $class->new($host, Port => $port || $default_port, Timeout => 5); - if ($smtp) { - # The connection works! - $smtp->quit; - return ''; - } - } - return "Cannot connect to $host" . ($port ? " using port $port" : ""); + my $host = shift; + my $port; + + return '' unless $host; + + 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)"; + } + } + trick_taint($host); + + # Let's first try to connect using SSL. If this fails, we fall back to + # an unencrypted connection. + foreach my $method (['Net::SMTP::SSL', 465], ['Net::SMTP', 25]) { + my ($class, $default_port) = @$method; + next if $class eq 'Net::SMTP::SSL' && !Bugzilla->feature('smtp_ssl'); + eval "require $class"; + my $smtp = $class->new($host, Port => $port || $default_port, Timeout => 5); + if ($smtp) { + + # The connection works! + $smtp->quit; + return ''; + } + } + return "Cannot connect to $host" . ($port ? " using port $port" : ""); } sub check_smtp_auth { - my $username = shift; - if ($username and !Bugzilla->feature('smtp_auth')) { - return "SMTP Authentication is not available. Run checksetup.pl for" - . " more details"; - } - return ""; + my $username = shift; + if ($username and !Bugzilla->feature('smtp_auth')) { + return "SMTP Authentication is not available. Run checksetup.pl for" + . " more details"; + } + return ""; } sub check_smtp_ssl { - my $use_ssl = shift; - if ($use_ssl && !Bugzilla->feature('smtp_ssl')) { - return "SSL support is not available. Run checksetup.pl for more details"; - } - return ""; + my $use_ssl = shift; + if ($use_ssl && !Bugzilla->feature('smtp_ssl')) { + return "SSL support is not available. Run checksetup.pl for more details"; + } + return ""; } sub check_theschwartz_available { - my $use_queue = shift; - if ($use_queue && !Bugzilla->feature('jobqueue')) { - return "Using the job queue requires that you have certain Perl" - . " modules installed. See the output of checksetup.pl" - . " for more information"; - } - return ""; + my $use_queue = shift; + if ($use_queue && !Bugzilla->feature('jobqueue')) { + return + "Using the job queue requires that you have certain Perl" + . " modules installed. See the output of checksetup.pl" + . " for more information"; + } + return ""; } sub check_comment_taggers_group { - my $group_name = shift; - if ($group_name && !Bugzilla->feature('jsonrpc')) { - return "Comment tagging requires installation of the JSONRPC feature"; - } - return check_group($group_name); + my $group_name = shift; + if ($group_name && !Bugzilla->feature('jsonrpc')) { + return "Comment tagging requires installation of the JSONRPC feature"; + } + return check_group($group_name); } # OK, here are the parameter definitions themselves. @@ -467,13 +481,13 @@ sub check_comment_taggers_group { # } # # Here, 'b' is the default option, and 'a' and 'c' are other possible -# options, but only one at a time! +# options, but only one at a time! # # &check_multi should always be used as the param verification function # for list (single and multiple) parameter types. sub get_param_list { - return; + return; } 1; diff --git a/Bugzilla/Config/Core.pm b/Bugzilla/Config/Core.pm index 654e569ba..50af9a077 100644 --- a/Bugzilla/Config/Core.pm +++ b/Bugzilla/Config/Core.pm @@ -16,31 +16,13 @@ use Bugzilla::Config::Common; our $sortkey = 100; use constant get_param_list => ( - { - name => 'urlbase', - type => 't', - default => '', - checker => \&check_urlbase - }, - - { - name => 'ssl_redirect', - type => 'b', - default => 0 - }, - - { - name => 'sslbase', - type => 't', - default => '', - checker => \&check_sslbase - }, - - { - name => 'cookiepath', - type => 't', - default => '/' - }, + {name => 'urlbase', type => 't', default => '', checker => \&check_urlbase}, + + {name => 'ssl_redirect', type => 'b', default => 0}, + + {name => 'sslbase', type => 't', default => '', checker => \&check_sslbase}, + + {name => 'cookiepath', type => 't', default => '/'}, ); 1; diff --git a/Bugzilla/Config/DependencyGraph.pm b/Bugzilla/Config/DependencyGraph.pm index c815822f3..27bc9938d 100644 --- a/Bugzilla/Config/DependencyGraph.pm +++ b/Bugzilla/Config/DependencyGraph.pm @@ -16,21 +16,17 @@ use Bugzilla::Config::Common; our $sortkey = 800; sub get_param_list { - my $class = shift; + my $class = shift; my @param_list = ( - { - name => 'webdotbase', - type => 't', - default => '', - checker => \&check_webdotbase - }, + { + name => 'webdotbase', + type => 't', + default => '', + checker => \&check_webdotbase + }, - { - name => 'font_file', - type => 't', - default => '', - checker => \&check_font_file - }); + {name => 'font_file', type => 't', default => '', checker => \&check_font_file} + ); return @param_list; } diff --git a/Bugzilla/Config/General.pm b/Bugzilla/Config/General.pm index 380680590..322275aa0 100644 --- a/Bugzilla/Config/General.pm +++ b/Bugzilla/Config/General.pm @@ -17,39 +17,28 @@ our $sortkey = 150; use constant get_param_list => ( { - name => 'maintainer', - type => 't', - no_reset => '1', - default => '', - checker => \&check_email + name => 'maintainer', + type => 't', + no_reset => '1', + default => '', + checker => \&check_email }, - { - name => 'utf8', - type => 'b', - default => '0', - checker => \&check_utf8 - }, + {name => 'utf8', type => 'b', default => '0', checker => \&check_utf8}, - { - name => 'shutdownhtml', - type => 'l', - default => '' - }, + {name => 'shutdownhtml', type => 'l', default => ''}, - { - name => 'announcehtml', - type => 'l', - default => '' - }, + {name => 'announcehtml', type => 'l', default => ''}, { - name => 'upgrade_notification', - type => 's', - choices => ['development_snapshot', 'latest_stable_release', - 'stable_branch_release', 'disabled'], - default => 'latest_stable_release', - checker => \&check_notification + name => 'upgrade_notification', + type => 's', + choices => [ + 'development_snapshot', 'latest_stable_release', + 'stable_branch_release', 'disabled' + ], + default => 'latest_stable_release', + checker => \&check_notification }, ); diff --git a/Bugzilla/Config/GroupSecurity.pm b/Bugzilla/Config/GroupSecurity.pm index e827834a0..6602cdfea 100644 --- a/Bugzilla/Config/GroupSecurity.pm +++ b/Bugzilla/Config/GroupSecurity.pm @@ -20,84 +20,69 @@ sub get_param_list { my $class = shift; my @param_list = ( - { - name => 'makeproductgroups', - type => 'b', - default => 0 - }, - - { - name => 'chartgroup', - type => 's', - choices => \&_get_all_group_names, - default => 'editbugs', - checker => \&check_group - }, - - { - name => 'insidergroup', - type => 's', - choices => \&_get_all_group_names, - default => '', - checker => \&check_group - }, - - { - name => 'timetrackinggroup', - type => 's', - choices => \&_get_all_group_names, - default => 'editbugs', - checker => \&check_group - }, - - { - name => 'querysharegroup', - type => 's', - choices => \&_get_all_group_names, - default => 'editbugs', - checker => \&check_group - }, - - { - name => 'comment_taggers_group', - type => 's', - choices => \&_get_all_group_names, - default => 'editbugs', - checker => \&check_comment_taggers_group - }, - - { - name => 'debug_group', - type => 's', - choices => \&_get_all_group_names, - default => 'admin', - checker => \&check_group - }, - - { - name => 'usevisibilitygroups', - type => 'b', - default => 0 - }, - - { - name => 'strict_isolation', - type => 'b', - default => 0 - }, - - { - name => 'or_groups', - type => 'b', - default => 0 - } ); + {name => 'makeproductgroups', type => 'b', default => 0}, + + { + name => 'chartgroup', + type => 's', + choices => \&_get_all_group_names, + default => 'editbugs', + checker => \&check_group + }, + + { + name => 'insidergroup', + type => 's', + choices => \&_get_all_group_names, + default => '', + checker => \&check_group + }, + + { + name => 'timetrackinggroup', + type => 's', + choices => \&_get_all_group_names, + default => 'editbugs', + checker => \&check_group + }, + + { + name => 'querysharegroup', + type => 's', + choices => \&_get_all_group_names, + default => 'editbugs', + checker => \&check_group + }, + + { + name => 'comment_taggers_group', + type => 's', + choices => \&_get_all_group_names, + default => 'editbugs', + checker => \&check_comment_taggers_group + }, + + { + name => 'debug_group', + type => 's', + choices => \&_get_all_group_names, + default => 'admin', + checker => \&check_group + }, + + {name => 'usevisibilitygroups', type => 'b', default => 0}, + + {name => 'strict_isolation', type => 'b', default => 0}, + + {name => 'or_groups', type => 'b', default => 0} + ); return @param_list; } sub _get_all_group_names { - my @group_names = map {$_->name} Bugzilla::Group->get_all; - unshift(@group_names, ''); - return \@group_names; + my @group_names = map { $_->name } Bugzilla::Group->get_all; + unshift(@group_names, ''); + return \@group_names; } 1; diff --git a/Bugzilla/Config/LDAP.pm b/Bugzilla/Config/LDAP.pm index 0bc8240df..75f58e141 100644 --- a/Bugzilla/Config/LDAP.pm +++ b/Bugzilla/Config/LDAP.pm @@ -16,49 +16,22 @@ use Bugzilla::Config::Common; our $sortkey = 1000; sub get_param_list { - my $class = shift; + my $class = shift; my @param_list = ( - { - name => 'LDAPserver', - type => 't', - default => '' - }, + {name => 'LDAPserver', type => 't', default => ''}, - { - name => 'LDAPstarttls', - type => 'b', - default => 0 - }, + {name => 'LDAPstarttls', type => 'b', default => 0}, - { - name => 'LDAPbinddn', - type => 't', - default => '' - }, + {name => 'LDAPbinddn', type => 't', default => ''}, - { - name => 'LDAPBaseDN', - type => 't', - default => '' - }, + {name => 'LDAPBaseDN', type => 't', default => ''}, - { - name => 'LDAPuidattribute', - type => 't', - default => 'uid' - }, + {name => 'LDAPuidattribute', type => 't', default => 'uid'}, - { - name => 'LDAPmailattribute', - type => 't', - default => 'mail' - }, + {name => 'LDAPmailattribute', type => 't', default => 'mail'}, - { - name => 'LDAPfilter', - type => 't', - default => '', - } ); + {name => 'LDAPfilter', type => 't', default => '',} + ); return @param_list; } diff --git a/Bugzilla/Config/MTA.pm b/Bugzilla/Config/MTA.pm index 467bdab3f..c7f8e5057 100644 --- a/Bugzilla/Config/MTA.pm +++ b/Bugzilla/Config/MTA.pm @@ -16,68 +16,43 @@ use Bugzilla::Config::Common; our $sortkey = 1200; sub get_param_list { - my $class = shift; + my $class = shift; my @param_list = ( - { - name => 'mail_delivery_method', - type => 's', - choices => ['Sendmail', 'SMTP', 'Test', 'None'], - default => 'Sendmail', - checker => \&check_mail_delivery_method - }, - - { - name => 'mailfrom', - type => 't', - default => 'bugzilla-daemon' - }, - - { - name => 'use_mailer_queue', - type => 'b', - default => 0, - checker => \&check_theschwartz_available, - }, - - { - name => 'smtpserver', - type => 't', - default => 'localhost', - checker => \&check_smtp_server - }, - { - name => 'smtp_username', - type => 't', - default => '', - checker => \&check_smtp_auth - }, - { - name => 'smtp_password', - type => 'p', - default => '' - }, - { - name => 'smtp_ssl', - type => 'b', - default => 0, - checker => \&check_smtp_ssl - }, - { - name => 'smtp_debug', - type => 'b', - default => 0 - }, - { - name => 'whinedays', - type => 't', - default => 7, - checker => \&check_numeric - }, - { - name => 'globalwatchers', - type => 't', - default => '', - }, ); + { + name => 'mail_delivery_method', + type => 's', + choices => ['Sendmail', 'SMTP', 'Test', 'None'], + default => 'Sendmail', + checker => \&check_mail_delivery_method + }, + + {name => 'mailfrom', type => 't', default => 'bugzilla-daemon'}, + + { + name => 'use_mailer_queue', + type => 'b', + default => 0, + checker => \&check_theschwartz_available, + }, + + { + name => 'smtpserver', + type => 't', + default => 'localhost', + checker => \&check_smtp_server + }, + { + name => 'smtp_username', + type => 't', + default => '', + checker => \&check_smtp_auth + }, + {name => 'smtp_password', type => 'p', default => ''}, + {name => 'smtp_ssl', type => 'b', default => 0, checker => \&check_smtp_ssl}, + {name => 'smtp_debug', type => 'b', default => 0}, + {name => 'whinedays', type => 't', default => 7, checker => \&check_numeric}, + {name => 'globalwatchers', type => 't', default => '',}, + ); return @param_list; } diff --git a/Bugzilla/Config/Memcached.pm b/Bugzilla/Config/Memcached.pm index 292803d86..5ab3364f9 100644 --- a/Bugzilla/Config/Memcached.pm +++ b/Bugzilla/Config/Memcached.pm @@ -17,16 +17,8 @@ our $sortkey = 1550; sub get_param_list { return ( - { - name => 'memcached_servers', - type => 't', - default => '' - }, - { - name => 'memcached_namespace', - type => 't', - default => 'bugzilla:', - }, + {name => 'memcached_servers', type => 't', default => ''}, + {name => 'memcached_namespace', type => 't', default => 'bugzilla:',}, ); } diff --git a/Bugzilla/Config/Query.pm b/Bugzilla/Config/Query.pm index f18bb90df..adfb4eaf4 100644 --- a/Bugzilla/Config/Query.pm +++ b/Bugzilla/Config/Query.pm @@ -16,47 +16,45 @@ use Bugzilla::Config::Common; our $sortkey = 1400; sub get_param_list { - my $class = shift; + my $class = shift; my @param_list = ( - { - name => 'quip_list_entry_control', - type => 's', - choices => ['open', 'moderated', 'closed'], - default => 'open', - checker => \&check_multi - }, - - { - name => 'mybugstemplate', - type => 't', - default => 'buglist.cgi?resolution=---&emailassigned_to1=1&emailreporter1=1&emailtype1=exact&email1=%userid%' - }, - - { - name => 'defaultquery', - type => 't', - default => 'resolution=---&emailassigned_to1=1&emailassigned_to2=1&emailreporter2=1&emailcc2=1&emailqa_contact2=1&emaillongdesc3=1&order=Importance&long_desc_type=substring' - }, - - { - name => 'search_allow_no_criteria', - type => 'b', - default => 1 - }, - - { - name => 'default_search_limit', - type => 't', - default => '500', - checker => \&check_numeric - }, - - { - name => 'max_search_results', - type => 't', - default => '10000', - checker => \&check_numeric - }, + { + name => 'quip_list_entry_control', + type => 's', + choices => ['open', 'moderated', 'closed'], + default => 'open', + checker => \&check_multi + }, + + { + name => 'mybugstemplate', + type => 't', + default => + 'buglist.cgi?resolution=---&emailassigned_to1=1&emailreporter1=1&emailtype1=exact&email1=%userid%' + }, + + { + name => 'defaultquery', + type => 't', + default => + 'resolution=---&emailassigned_to1=1&emailassigned_to2=1&emailreporter2=1&emailcc2=1&emailqa_contact2=1&emaillongdesc3=1&order=Importance&long_desc_type=substring' + }, + + {name => 'search_allow_no_criteria', type => 'b', default => 1}, + + { + name => 'default_search_limit', + type => 't', + default => '500', + checker => \&check_numeric + }, + + { + name => 'max_search_results', + type => 't', + default => '10000', + checker => \&check_numeric + }, ); return @param_list; } diff --git a/Bugzilla/Config/RADIUS.pm b/Bugzilla/Config/RADIUS.pm index 8e30b07a9..b0a5ddbf5 100644 --- a/Bugzilla/Config/RADIUS.pm +++ b/Bugzilla/Config/RADIUS.pm @@ -16,31 +16,15 @@ use Bugzilla::Config::Common; our $sortkey = 1100; sub get_param_list { - my $class = shift; + my $class = shift; my @param_list = ( - { - name => 'RADIUS_server', - type => 't', - default => '' - }, - - { - name => 'RADIUS_secret', - type => 't', - default => '' - }, - - { - name => 'RADIUS_NAS_IP', - type => 't', - default => '' - }, - - { - name => 'RADIUS_email_suffix', - type => 't', - default => '' - }, + {name => 'RADIUS_server', type => 't', default => ''}, + + {name => 'RADIUS_secret', type => 't', default => ''}, + + {name => 'RADIUS_NAS_IP', type => 't', default => ''}, + + {name => 'RADIUS_email_suffix', type => 't', default => ''}, ); return @param_list; } diff --git a/Bugzilla/Config/ShadowDB.pm b/Bugzilla/Config/ShadowDB.pm index 5dbbb5202..101e4678f 100644 --- a/Bugzilla/Config/ShadowDB.pm +++ b/Bugzilla/Config/ShadowDB.pm @@ -16,35 +16,23 @@ use Bugzilla::Config::Common; our $sortkey = 1500; sub get_param_list { - my $class = shift; + my $class = shift; my @param_list = ( - { - name => 'shadowdbhost', - type => 't', - default => '', - }, - - { - name => 'shadowdbport', - type => 't', - default => '3306', - checker => \&check_numeric, - }, - - { - name => 'shadowdbsock', - type => 't', - default => '', - }, - - # This entry must be _after_ the shadowdb{host,port,sock} settings so that - # they can be used in the validation here - { - name => 'shadowdb', - type => 't', - default => '', - checker => \&check_shadowdb - } ); + {name => 'shadowdbhost', type => 't', default => '',}, + + { + name => 'shadowdbport', + type => 't', + default => '3306', + checker => \&check_numeric, + }, + + {name => 'shadowdbsock', type => 't', default => '',}, + + # This entry must be _after_ the shadowdb{host,port,sock} settings so that + # they can be used in the validation here + {name => 'shadowdb', type => 't', default => '', checker => \&check_shadowdb} + ); return @param_list; } diff --git a/Bugzilla/Config/UserMatch.pm b/Bugzilla/Config/UserMatch.pm index 3f74a7c44..a1f8a3eb2 100644 --- a/Bugzilla/Config/UserMatch.pm +++ b/Bugzilla/Config/UserMatch.pm @@ -16,32 +16,21 @@ use Bugzilla::Config::Common; our $sortkey = 1600; sub get_param_list { - my $class = shift; + my $class = shift; my @param_list = ( - { - name => 'usemenuforusers', - type => 'b', - default => '0' - }, - - { - name => 'ajax_user_autocompletion', - type => 'b', - default => '1', - }, - - { - name => 'maxusermatches', - type => 't', - default => '1000', - checker => \&check_numeric - }, - - { - name => 'confirmuniqueusermatch', - type => 'b', - default => 1, - } ); + {name => 'usemenuforusers', type => 'b', default => '0'}, + + {name => 'ajax_user_autocompletion', type => 'b', default => '1',}, + + { + name => 'maxusermatches', + type => 't', + default => '1000', + checker => \&check_numeric + }, + + {name => 'confirmuniqueusermatch', type => 'b', default => 1,} + ); return @param_list; } diff --git a/Bugzilla/Constants.pm b/Bugzilla/Constants.pm index edaa8baa5..ca8787170 100644 --- a/Bugzilla/Constants.pm +++ b/Bugzilla/Constants.pm @@ -18,181 +18,181 @@ use File::Basename; use Memoize; @Bugzilla::Constants::EXPORT = qw( - BUGZILLA_VERSION - REST_DOC - - REMOTE_FILE - LOCAL_FILE + BUGZILLA_VERSION + REST_DOC - bz_locations - - CONCATENATE_ASSETS - - IS_NULL - NOT_NULL - - CONTROLMAPNA - CONTROLMAPSHOWN - CONTROLMAPDEFAULT - CONTROLMAPMANDATORY - - AUTH_OK - AUTH_NODATA - AUTH_ERROR - AUTH_LOGINFAILED - AUTH_DISABLED - AUTH_NO_SUCH_USER - AUTH_LOCKOUT - - USER_PASSWORD_MIN_LENGTH - - LOGIN_OPTIONAL - LOGIN_NORMAL - LOGIN_REQUIRED - - LOGOUT_ALL - LOGOUT_CURRENT - LOGOUT_KEEP_CURRENT - - GRANT_DIRECT - GRANT_REGEXP - - GROUP_MEMBERSHIP - GROUP_BLESS - GROUP_VISIBLE - - MAILTO_USER - MAILTO_GROUP - - DEFAULT_COLUMN_LIST - DEFAULT_QUERY_NAME - DEFAULT_MILESTONE - - SAVE_NUM_SEARCHES - - COMMENT_COLS - MAX_COMMENT_LENGTH - - MIN_COMMENT_TAG_LENGTH - MAX_COMMENT_TAG_LENGTH - - CMT_NORMAL - CMT_DUPE_OF - CMT_HAS_DUPE - CMT_ATTACHMENT_CREATED - CMT_ATTACHMENT_UPDATED - - THROW_ERROR - - RELATIONSHIPS - REL_ASSIGNEE REL_QA REL_REPORTER REL_CC REL_GLOBAL_WATCHER - REL_ANY - - POS_EVENTS - EVT_OTHER EVT_ADDED_REMOVED EVT_COMMENT EVT_ATTACHMENT EVT_ATTACHMENT_DATA - EVT_PROJ_MANAGEMENT EVT_OPENED_CLOSED EVT_KEYWORD EVT_CC EVT_DEPEND_BLOCK - EVT_BUG_CREATED EVT_COMPONENT - - NEG_EVENTS - EVT_UNCONFIRMED EVT_CHANGED_BY_ME - - GLOBAL_EVENTS - EVT_FLAG_REQUESTED EVT_REQUESTED_FLAG - - ADMIN_GROUP_NAME - PER_PRODUCT_PRIVILEGES - - SENDMAIL_EXE - SENDMAIL_PATH - - FIELD_TYPE_UNKNOWN - FIELD_TYPE_FREETEXT - FIELD_TYPE_SINGLE_SELECT - FIELD_TYPE_MULTI_SELECT - FIELD_TYPE_TEXTAREA - FIELD_TYPE_DATETIME - FIELD_TYPE_DATE - FIELD_TYPE_BUG_ID - FIELD_TYPE_BUG_URLS - FIELD_TYPE_KEYWORDS - FIELD_TYPE_INTEGER - FIELD_TYPE_HIGHEST_PLUS_ONE - - EMPTY_DATETIME_REGEX - - ABNORMAL_SELECTS - - TIMETRACKING_FIELDS - - USAGE_MODE_BROWSER - USAGE_MODE_CMDLINE - USAGE_MODE_XMLRPC - USAGE_MODE_EMAIL - USAGE_MODE_JSON - USAGE_MODE_TEST - USAGE_MODE_REST - - ERROR_MODE_WEBPAGE - ERROR_MODE_DIE - ERROR_MODE_DIE_SOAP_FAULT - ERROR_MODE_JSON_RPC - ERROR_MODE_TEST - ERROR_MODE_REST - - COLOR_ERROR - COLOR_SUCCESS - - INSTALLATION_MODE_INTERACTIVE - INSTALLATION_MODE_NON_INTERACTIVE - - DB_MODULE - ROOT_USER - ON_WINDOWS - ON_ACTIVESTATE - - MAX_TOKEN_AGE - MAX_LOGINCOOKIE_AGE - MAX_SUDO_TOKEN_AGE - MAX_LOGIN_ATTEMPTS - LOGIN_LOCKOUT_INTERVAL - ACCOUNT_CHANGE_INTERVAL - MAX_STS_AGE - - SAFE_PROTOCOLS - LEGAL_CONTENT_TYPES - - MIN_SMALLINT - MAX_SMALLINT - MAX_INT_32 - - MAX_LEN_QUERY_NAME - MAX_CLASSIFICATION_SIZE - MAX_PRODUCT_SIZE - MAX_MILESTONE_SIZE - MAX_COMPONENT_SIZE - MAX_FIELD_VALUE_SIZE - MAX_FIELD_LONG_DESC_LENGTH - MAX_FREETEXT_LENGTH - MAX_BUG_URL_LENGTH - MAX_POSSIBLE_DUPLICATES - MAX_ATTACH_FILENAME_LENGTH - MAX_QUIP_LENGTH - MAX_WEBDOT_BUGS - - PASSWORD_DIGEST_ALGORITHM - PASSWORD_SALT_LENGTH - - CGI_URI_LIMIT - - PRIVILEGES_REQUIRED_NONE - PRIVILEGES_REQUIRED_REPORTER - PRIVILEGES_REQUIRED_ASSIGNEE - PRIVILEGES_REQUIRED_EMPOWERED - - AUDIT_CREATE - AUDIT_REMOVE - - MOST_FREQUENT_THRESHOLD + REMOTE_FILE + LOCAL_FILE + + bz_locations + + CONCATENATE_ASSETS + + IS_NULL + NOT_NULL + + CONTROLMAPNA + CONTROLMAPSHOWN + CONTROLMAPDEFAULT + CONTROLMAPMANDATORY + + AUTH_OK + AUTH_NODATA + AUTH_ERROR + AUTH_LOGINFAILED + AUTH_DISABLED + AUTH_NO_SUCH_USER + AUTH_LOCKOUT + + USER_PASSWORD_MIN_LENGTH + + LOGIN_OPTIONAL + LOGIN_NORMAL + LOGIN_REQUIRED + + LOGOUT_ALL + LOGOUT_CURRENT + LOGOUT_KEEP_CURRENT + + GRANT_DIRECT + GRANT_REGEXP + + GROUP_MEMBERSHIP + GROUP_BLESS + GROUP_VISIBLE + + MAILTO_USER + MAILTO_GROUP + + DEFAULT_COLUMN_LIST + DEFAULT_QUERY_NAME + DEFAULT_MILESTONE + + SAVE_NUM_SEARCHES + + COMMENT_COLS + MAX_COMMENT_LENGTH + + MIN_COMMENT_TAG_LENGTH + MAX_COMMENT_TAG_LENGTH + + CMT_NORMAL + CMT_DUPE_OF + CMT_HAS_DUPE + CMT_ATTACHMENT_CREATED + CMT_ATTACHMENT_UPDATED + + THROW_ERROR + + RELATIONSHIPS + REL_ASSIGNEE REL_QA REL_REPORTER REL_CC REL_GLOBAL_WATCHER + REL_ANY + + POS_EVENTS + EVT_OTHER EVT_ADDED_REMOVED EVT_COMMENT EVT_ATTACHMENT EVT_ATTACHMENT_DATA + EVT_PROJ_MANAGEMENT EVT_OPENED_CLOSED EVT_KEYWORD EVT_CC EVT_DEPEND_BLOCK + EVT_BUG_CREATED EVT_COMPONENT + + NEG_EVENTS + EVT_UNCONFIRMED EVT_CHANGED_BY_ME + + GLOBAL_EVENTS + EVT_FLAG_REQUESTED EVT_REQUESTED_FLAG + + ADMIN_GROUP_NAME + PER_PRODUCT_PRIVILEGES + + SENDMAIL_EXE + SENDMAIL_PATH + + FIELD_TYPE_UNKNOWN + FIELD_TYPE_FREETEXT + FIELD_TYPE_SINGLE_SELECT + FIELD_TYPE_MULTI_SELECT + FIELD_TYPE_TEXTAREA + FIELD_TYPE_DATETIME + FIELD_TYPE_DATE + FIELD_TYPE_BUG_ID + FIELD_TYPE_BUG_URLS + FIELD_TYPE_KEYWORDS + FIELD_TYPE_INTEGER + FIELD_TYPE_HIGHEST_PLUS_ONE + + EMPTY_DATETIME_REGEX + + ABNORMAL_SELECTS + + TIMETRACKING_FIELDS + + USAGE_MODE_BROWSER + USAGE_MODE_CMDLINE + USAGE_MODE_XMLRPC + USAGE_MODE_EMAIL + USAGE_MODE_JSON + USAGE_MODE_TEST + USAGE_MODE_REST + + ERROR_MODE_WEBPAGE + ERROR_MODE_DIE + ERROR_MODE_DIE_SOAP_FAULT + ERROR_MODE_JSON_RPC + ERROR_MODE_TEST + ERROR_MODE_REST + + COLOR_ERROR + COLOR_SUCCESS + + INSTALLATION_MODE_INTERACTIVE + INSTALLATION_MODE_NON_INTERACTIVE + + DB_MODULE + ROOT_USER + ON_WINDOWS + ON_ACTIVESTATE + + MAX_TOKEN_AGE + MAX_LOGINCOOKIE_AGE + MAX_SUDO_TOKEN_AGE + MAX_LOGIN_ATTEMPTS + LOGIN_LOCKOUT_INTERVAL + ACCOUNT_CHANGE_INTERVAL + MAX_STS_AGE + + SAFE_PROTOCOLS + LEGAL_CONTENT_TYPES + + MIN_SMALLINT + MAX_SMALLINT + MAX_INT_32 + + MAX_LEN_QUERY_NAME + MAX_CLASSIFICATION_SIZE + MAX_PRODUCT_SIZE + MAX_MILESTONE_SIZE + MAX_COMPONENT_SIZE + MAX_FIELD_VALUE_SIZE + MAX_FIELD_LONG_DESC_LENGTH + MAX_FREETEXT_LENGTH + MAX_BUG_URL_LENGTH + MAX_POSSIBLE_DUPLICATES + MAX_ATTACH_FILENAME_LENGTH + MAX_QUIP_LENGTH + MAX_WEBDOT_BUGS + + PASSWORD_DIGEST_ALGORITHM + PASSWORD_SALT_LENGTH + + CGI_URI_LIMIT + + PRIVILEGES_REQUIRED_NONE + PRIVILEGES_REQUIRED_REPORTER + PRIVILEGES_REQUIRED_ASSIGNEE + PRIVILEGES_REQUIRED_EMPOWERED + + AUDIT_CREATE + AUDIT_REMOVE + + MOST_FREQUENT_THRESHOLD ); @Bugzilla::Constants::EXPORT_OK = qw(contenttypes); @@ -208,7 +208,7 @@ 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'; -use constant LOCAL_FILE => 'bugzilla-update.xml'; # Relative to datadir. +use constant LOCAL_FILE => 'bugzilla-update.xml'; # Relative to datadir. # When true CSS and JavaScript assets will be concatanted and minified at # run-time, to reduce the number of requests required to render a page. @@ -229,9 +229,9 @@ use constant NOT_NULL => ' __NOT_NULL__ '; # # ControlMap constants for group_control_map. # membercontol:othercontrol => meaning -# Na:Na => Bugs in this product may not be restricted to this +# Na:Na => Bugs in this product may not be restricted to this # group. -# Shown:Na => Members of the group may restrict bugs +# Shown:Na => Members of the group may restrict bugs # in this product to this group. # Shown:Shown => Members of the group may restrict bugs # in this product to this group. @@ -253,46 +253,46 @@ use constant NOT_NULL => ' __NOT_NULL__ '; # Mandatory:Mandatory => Bug will be forced into this group regardless. # All other combinations are illegal. -use constant CONTROLMAPNA => 0; -use constant CONTROLMAPSHOWN => 1; -use constant CONTROLMAPDEFAULT => 2; +use constant CONTROLMAPNA => 0; +use constant CONTROLMAPSHOWN => 1; +use constant CONTROLMAPDEFAULT => 2; use constant CONTROLMAPMANDATORY => 3; # See Bugzilla::Auth for docs on AUTH_*, LOGIN_* and LOGOUT_* -use constant AUTH_OK => 0; -use constant AUTH_NODATA => 1; -use constant AUTH_ERROR => 2; -use constant AUTH_LOGINFAILED => 3; -use constant AUTH_DISABLED => 4; -use constant AUTH_NO_SUCH_USER => 5; -use constant AUTH_LOCKOUT => 6; +use constant AUTH_OK => 0; +use constant AUTH_NODATA => 1; +use constant AUTH_ERROR => 2; +use constant AUTH_LOGINFAILED => 3; +use constant AUTH_DISABLED => 4; +use constant AUTH_NO_SUCH_USER => 5; +use constant AUTH_LOCKOUT => 6; # The minimum length a password must have. use constant USER_PASSWORD_MIN_LENGTH => 6; use constant LOGIN_OPTIONAL => 0; -use constant LOGIN_NORMAL => 1; +use constant LOGIN_NORMAL => 1; use constant LOGIN_REQUIRED => 2; -use constant LOGOUT_ALL => 0; -use constant LOGOUT_CURRENT => 1; +use constant LOGOUT_ALL => 0; +use constant LOGOUT_CURRENT => 1; use constant LOGOUT_KEEP_CURRENT => 2; use constant GRANT_DIRECT => 0; use constant GRANT_REGEXP => 2; use constant GROUP_MEMBERSHIP => 0; -use constant GROUP_BLESS => 1; -use constant GROUP_VISIBLE => 2; +use constant GROUP_BLESS => 1; +use constant GROUP_VISIBLE => 2; -use constant MAILTO_USER => 0; +use constant MAILTO_USER => 0; use constant MAILTO_GROUP => 1; # The default list of columns for buglist.cgi use constant DEFAULT_COLUMN_LIST => ( - "product", "component", "assigned_to", - "bug_status", "resolution", "short_desc", "changeddate" + "product", "component", "assigned_to", "bug_status", + "resolution", "short_desc", "changeddate" ); # Used by query.cgi and buglist.cgi as the named-query name @@ -307,6 +307,7 @@ use constant SAVE_NUM_SEARCHES => 10; # The column width for comment textareas and comments in bugmails. use constant COMMENT_COLS => 80; + # Used in _check_comment(). Gives the max length allowed for a comment. use constant MAX_COMMENT_LENGTH => 65535; @@ -315,9 +316,10 @@ use constant MIN_COMMENT_TAG_LENGTH => 3; use constant MAX_COMMENT_TAG_LENGTH => 24; # The type of bug comments. -use constant CMT_NORMAL => 0; -use constant CMT_DUPE_OF => 1; +use constant CMT_NORMAL => 0; +use constant CMT_DUPE_OF => 1; use constant CMT_HAS_DUPE => 2; + # Type 3 was CMT_POPULAR_VOTES, which moved to the Voting extension. # Type 4 was CMT_MOVED_TO, which moved to the OldBugMove extension. use constant CMT_ATTACHMENT_CREATED => 5; @@ -327,27 +329,26 @@ use constant CMT_ATTACHMENT_UPDATED => 6; # an error when the validation fails. use constant THROW_ERROR => 1; -use constant REL_ASSIGNEE => 0; -use constant REL_QA => 1; -use constant REL_REPORTER => 2; -use constant REL_CC => 3; +use constant REL_ASSIGNEE => 0; +use constant REL_QA => 1; +use constant REL_REPORTER => 2; +use constant REL_CC => 3; + # REL 4 was REL_VOTER, before it was moved ino an extension. -use constant REL_GLOBAL_WATCHER => 5; +use constant REL_GLOBAL_WATCHER => 5; # We need these strings for the X-Bugzilla-Reasons header # Note: this hash uses "," rather than "=>" to avoid auto-quoting of the LHS. # This should be accessed through Bugzilla::BugMail::relationships() instead # of being accessed directly. use constant RELATIONSHIPS => { - REL_ASSIGNEE , "AssignedTo", - REL_REPORTER , "Reporter", - REL_QA , "QAcontact", - REL_CC , "CC", - REL_GLOBAL_WATCHER, "GlobalWatcher" + REL_ASSIGNEE, "AssignedTo", REL_REPORTER, "Reporter", + REL_QA, "QAcontact", REL_CC, "CC", + REL_GLOBAL_WATCHER, "GlobalWatcher" }; - + # Used for global events like EVT_FLAG_REQUESTED -use constant REL_ANY => 100; +use constant REL_ANY => 100; # There are two sorts of event - positive and negative. Positive events are # those for which the user says "I want mail if this happens." Negative events @@ -355,34 +356,34 @@ use constant REL_ANY => 100; # # Exactly when each event fires is defined in wants_bug_mail() in User.pm; I'm # not commenting them here in case the comments and the code get out of sync. -use constant EVT_OTHER => 0; -use constant EVT_ADDED_REMOVED => 1; -use constant EVT_COMMENT => 2; -use constant EVT_ATTACHMENT => 3; -use constant EVT_ATTACHMENT_DATA => 4; -use constant EVT_PROJ_MANAGEMENT => 5; -use constant EVT_OPENED_CLOSED => 6; -use constant EVT_KEYWORD => 7; -use constant EVT_CC => 8; -use constant EVT_DEPEND_BLOCK => 9; -use constant EVT_BUG_CREATED => 10; -use constant EVT_COMPONENT => 11; - -use constant POS_EVENTS => EVT_OTHER, EVT_ADDED_REMOVED, EVT_COMMENT, - EVT_ATTACHMENT, EVT_ATTACHMENT_DATA, - EVT_PROJ_MANAGEMENT, EVT_OPENED_CLOSED, EVT_KEYWORD, - EVT_CC, EVT_DEPEND_BLOCK, EVT_BUG_CREATED, - EVT_COMPONENT; - -use constant EVT_UNCONFIRMED => 50; -use constant EVT_CHANGED_BY_ME => 51; +use constant EVT_OTHER => 0; +use constant EVT_ADDED_REMOVED => 1; +use constant EVT_COMMENT => 2; +use constant EVT_ATTACHMENT => 3; +use constant EVT_ATTACHMENT_DATA => 4; +use constant EVT_PROJ_MANAGEMENT => 5; +use constant EVT_OPENED_CLOSED => 6; +use constant EVT_KEYWORD => 7; +use constant EVT_CC => 8; +use constant EVT_DEPEND_BLOCK => 9; +use constant EVT_BUG_CREATED => 10; +use constant EVT_COMPONENT => 11; + +use constant + POS_EVENTS => EVT_OTHER, + EVT_ADDED_REMOVED, EVT_COMMENT, EVT_ATTACHMENT, EVT_ATTACHMENT_DATA, + EVT_PROJ_MANAGEMENT, EVT_OPENED_CLOSED, EVT_KEYWORD, EVT_CC, + EVT_DEPEND_BLOCK, EVT_BUG_CREATED, EVT_COMPONENT; + +use constant EVT_UNCONFIRMED => 50; +use constant EVT_CHANGED_BY_ME => 51; use constant NEG_EVENTS => EVT_UNCONFIRMED, EVT_CHANGED_BY_ME; # These are the "global" flags, which aren't tied to a particular relationship. # and so use REL_ANY. -use constant EVT_FLAG_REQUESTED => 100; # Flag has been requested of me -use constant EVT_REQUESTED_FLAG => 101; # I have requested a flag +use constant EVT_FLAG_REQUESTED => 100; # Flag has been requested of me +use constant EVT_REQUESTED_FLAG => 101; # I have requested a flag use constant GLOBAL_EVENTS => EVT_FLAG_REQUESTED, EVT_REQUESTED_FLAG; @@ -390,10 +391,12 @@ use constant GLOBAL_EVENTS => EVT_FLAG_REQUESTED, EVT_REQUESTED_FLAG; use constant ADMIN_GROUP_NAME => 'admin'; # Privileges which can be per-product. -use constant PER_PRODUCT_PRIVILEGES => ('editcomponents', 'editbugs', 'canconfirm'); +use constant PER_PRODUCT_PRIVILEGES => + ('editcomponents', 'editbugs', 'canconfirm'); # Path to sendmail.exe (Windows only) use constant SENDMAIL_EXE => '/usr/lib/sendmail.exe'; + # Paths to search for the sendmail binary (non-Windows) use constant SENDMAIL_PATH => '/usr/lib:/usr/sbin:/usr/ucblib'; @@ -404,45 +407,46 @@ use constant SENDMAIL_PATH => '/usr/lib:/usr/sbin:/usr/ucblib'; # we do more than we would do for a standard integer type (f.e. we might # display a user picker). -use constant FIELD_TYPE_UNKNOWN => 0; -use constant FIELD_TYPE_FREETEXT => 1; +use constant FIELD_TYPE_UNKNOWN => 0; +use constant FIELD_TYPE_FREETEXT => 1; use constant FIELD_TYPE_SINGLE_SELECT => 2; -use constant FIELD_TYPE_MULTI_SELECT => 3; -use constant FIELD_TYPE_TEXTAREA => 4; -use constant FIELD_TYPE_DATETIME => 5; -use constant FIELD_TYPE_BUG_ID => 6; -use constant FIELD_TYPE_BUG_URLS => 7; -use constant FIELD_TYPE_KEYWORDS => 8; -use constant FIELD_TYPE_DATE => 9; -use constant FIELD_TYPE_INTEGER => 10; +use constant FIELD_TYPE_MULTI_SELECT => 3; +use constant FIELD_TYPE_TEXTAREA => 4; +use constant FIELD_TYPE_DATETIME => 5; +use constant FIELD_TYPE_BUG_ID => 6; +use constant FIELD_TYPE_BUG_URLS => 7; +use constant FIELD_TYPE_KEYWORDS => 8; +use constant FIELD_TYPE_DATE => 9; +use constant FIELD_TYPE_INTEGER => 10; + # Add new field types above this line, and change the below value in the # obvious fashion use constant FIELD_TYPE_HIGHEST_PLUS_ONE => 11; -use constant EMPTY_DATETIME_REGEX => qr/^[0\-:\sA-Za-z]+$/; +use constant EMPTY_DATETIME_REGEX => qr/^[0\-:\sA-Za-z]+$/; # See the POD for Bugzilla::Field/is_abnormal to see why these are listed # here. -use constant ABNORMAL_SELECTS => { - classification => 1, - component => 1, - product => 1, -}; +use constant ABNORMAL_SELECTS => + {classification => 1, component => 1, product => 1,}; # The fields from fielddefs that are blocked from non-timetracking users. # work_time is sometimes called actual_time. use constant TIMETRACKING_FIELDS => - qw(estimated_time remaining_time work_time actual_time percentage_complete); + qw(estimated_time remaining_time work_time actual_time percentage_complete); # The maximum number of days a token will remain valid. use constant MAX_TOKEN_AGE => 3; + # How many days a logincookie will remain valid if not used. use constant MAX_LOGINCOOKIE_AGE => 30; + # How many seconds (default is 6 hours) a sudo cookie remains valid. use constant MAX_SUDO_TOKEN_AGE => 21600; # Maximum failed logins to lock account for this IP use constant MAX_LOGIN_ATTEMPTS => 5; + # If the maximum login attempts occur during this many minutes, the # account is locked. use constant LOGIN_LOCKOUT_INTERVAL => 30; @@ -456,36 +460,39 @@ use constant ACCOUNT_CHANGE_INTERVAL => 10; use constant MAX_STS_AGE => 604800; # Protocols which are considered as safe. -use constant SAFE_PROTOCOLS => ('afs', 'cid', 'ftp', 'gopher', 'http', 'https', - 'irc', 'ircs', 'mid', 'news', 'nntp', 'prospero', - 'telnet', 'view-source', 'wais'); +use constant SAFE_PROTOCOLS => ( + 'afs', 'cid', 'ftp', 'gopher', 'http', 'https', + 'irc', 'ircs', 'mid', 'news', 'nntp', 'prospero', + 'telnet', 'view-source', 'wais' +); # Valid MIME types for attachments. -use constant LEGAL_CONTENT_TYPES => ('application', 'audio', 'image', 'message', - 'model', 'multipart', 'text', 'video'); - -use constant contenttypes => - { - "html" => "text/html" , - "rdf" => "application/rdf+xml" , - "atom" => "application/atom+xml" , - "xml" => "application/xml" , - "dtd" => "application/xml-dtd" , - "js" => "application/x-javascript" , - "json" => "application/json" , - "csv" => "text/csv" , - "png" => "image/png" , - "ics" => "text/calendar" , - }; +use constant LEGAL_CONTENT_TYPES => ( + 'application', 'audio', 'image', 'message', + 'model', 'multipart', 'text', 'video' +); + +use constant contenttypes => { + "html" => "text/html", + "rdf" => "application/rdf+xml", + "atom" => "application/atom+xml", + "xml" => "application/xml", + "dtd" => "application/xml-dtd", + "js" => "application/x-javascript", + "json" => "application/json", + "csv" => "text/csv", + "png" => "image/png", + "ics" => "text/calendar", +}; # Usage modes. Default USAGE_MODE_BROWSER. Use with Bugzilla->usage_mode. -use constant USAGE_MODE_BROWSER => 0; -use constant USAGE_MODE_CMDLINE => 1; -use constant USAGE_MODE_XMLRPC => 2; -use constant USAGE_MODE_EMAIL => 3; -use constant USAGE_MODE_JSON => 4; -use constant USAGE_MODE_TEST => 5; -use constant USAGE_MODE_REST => 6; +use constant USAGE_MODE_BROWSER => 0; +use constant USAGE_MODE_CMDLINE => 1; +use constant USAGE_MODE_XMLRPC => 2; +use constant USAGE_MODE_EMAIL => 3; +use constant USAGE_MODE_JSON => 4; +use constant USAGE_MODE_TEST => 5; +use constant USAGE_MODE_REST => 6; # Error modes. Default set by Bugzilla->usage_mode (so ERROR_MODE_WEBPAGE # usually). Use with Bugzilla->error_mode. @@ -497,60 +504,76 @@ use constant ERROR_MODE_TEST => 4; use constant ERROR_MODE_REST => 5; # The ANSI colors of messages that command-line scripts use -use constant COLOR_ERROR => 'red'; +use constant COLOR_ERROR => 'red'; use constant COLOR_SUCCESS => 'green'; # The various modes that checksetup.pl can run in. -use constant INSTALLATION_MODE_INTERACTIVE => 0; +use constant INSTALLATION_MODE_INTERACTIVE => 0; use constant INSTALLATION_MODE_NON_INTERACTIVE => 1; # Data about what we require for different databases. 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 => { - package => 'DBD-mysql', - module => 'DBD::mysql', - # Disallow development versions - blacklist => ['_'], - # For UTF-8 support. 4.001 makes sure that blobs aren't - # marked as UTF-8. - version => '4.001', - }, - name => 'MySQL'}, - # 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', - dbd => { - package => 'DBD-Pg', - module => 'DBD::Pg', - # 2.7.0 fixes a problem with quoting strings - # containing backslashes in them. - version => '2.7.0', - }, - name => 'PostgreSQL'}, - 'oracle'=> {db => 'Bugzilla::DB::Oracle', db_version => '10.02.0', - dbd => { - package => 'DBD-Oracle', - module => 'DBD::Oracle', - version => '1.19', - }, - name => 'Oracle'}, - # SQLite 3.6.22 fixes a WHERE clause problem that may affect us. - sqlite => {db => 'Bugzilla::DB::Sqlite', db_version => '3.6.22', - dbd => { - package => 'DBD-SQLite', - module => 'DBD::SQLite', - # 1.29 is the version that contains 3.6.22. - version => '1.29', - }, - name => 'SQLite'}, + + # MySQL 5.0.15 was the first production 5.0.x release. + 'mysql' => { + db => 'Bugzilla::DB::Mysql', + db_version => '5.0.15', + dbd => { + package => 'DBD-mysql', + module => 'DBD::mysql', + + # Disallow development versions + blacklist => ['_'], + + # For UTF-8 support. 4.001 makes sure that blobs aren't + # marked as UTF-8. + version => '4.001', + }, + name => 'MySQL' + }, + + # 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', + dbd => { + package => 'DBD-Pg', + module => 'DBD::Pg', + + # 2.7.0 fixes a problem with quoting strings + # containing backslashes in them. + version => '2.7.0', + }, + name => 'PostgreSQL' + }, + 'oracle' => { + db => 'Bugzilla::DB::Oracle', + db_version => '10.02.0', + dbd => {package => 'DBD-Oracle', module => 'DBD::Oracle', version => '1.19',}, + name => 'Oracle' + }, + + # SQLite 3.6.22 fixes a WHERE clause problem that may affect us. + sqlite => { + db => 'Bugzilla::DB::Sqlite', + db_version => '3.6.22', + dbd => { + package => 'DBD-SQLite', + module => 'DBD::SQLite', + + # 1.29 is the version that contains 3.6.22. + version => '1.29', + }, + name => 'SQLite' + }, }; # True if we're on Win32. use constant ON_WINDOWS => ($^O =~ /MSWin32/i) ? 1 : 0; + # True if we're using ActiveState Perl (as opposed to Strawberry) on Windows. -use constant ON_ACTIVESTATE => eval { &Win32::BuildNumber }; +use constant ON_ACTIVESTATE => eval {&Win32::BuildNumber}; # The user who should be considered "root" when we're giving # instructions to Bugzilla administrators. @@ -558,7 +581,7 @@ use constant ROOT_USER => ON_WINDOWS ? 'Administrator' : 'root'; use constant MIN_SMALLINT => -32768; use constant MAX_SMALLINT => 32767; -use constant MAX_INT_32 => 2147483647; +use constant MAX_INT_32 => 2147483647; # The longest that a saved search name can be. use constant MAX_LEN_QUERY_NAME => 64; @@ -607,6 +630,7 @@ use constant MAX_WEBDOT_BUGS => 2000; # Perl's "Digest" module. Note that if you change this, it won't take # effect until a user logs in or changes their password. use constant PASSWORD_DIGEST_ALGORITHM => 'SHA-256'; + # How long of a salt should we use? Note that if you change this, it # won't take effect until a user logs in or changes their password. use constant PASSWORD_SALT_LENGTH => 8; @@ -615,7 +639,9 @@ use constant PASSWORD_SALT_LENGTH => 8; # via POST such as buglist.cgi. This value determines whether the redirect # can be safely done or not based on the web server's URI length setting. # See http://support.microsoft.com/kb/208427 for why MSIE is different -use constant CGI_URI_LIMIT => ($ENV{'HTTP_USER_AGENT'} || '') =~ /MSIE/ ? 2083 : 8000; +use constant CGI_URI_LIMIT => ($ENV{'HTTP_USER_AGENT'} || '') =~ /MSIE/ + ? 2083 + : 8000; # If the user isn't allowed to change a field, we must tell them who can. # We store the required permission set into the $PrivilegesRequired @@ -636,72 +662,79 @@ use constant AUDIT_REMOVE => '__remove__'; use constant MOST_FREQUENT_THRESHOLD => 2; 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'}); + + # 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 - # directory under both mod_cgi and mod_perl. We call dirname twice - # to get the name of the directory above the "Bugzilla/" directory. - # - # Calling dirname twice like that won't work on VMS or AmigaOS - # but I doubt anybody runs Bugzilla on those. - # - # On mod_cgi this will be a relative path. On mod_perl it will be an - # absolute path. - my $libpath = dirname(dirname($INC{'Bugzilla/Constants.pm'})); - # We have to detaint $libpath, but we can't use Bugzilla::Util here. - $libpath =~ /(.*)/; - $libpath = $1; - - my ($localconfig, $datadir); - if ($project && $project =~ /^(\w+)$/) { - $project = $1; - $localconfig = "localconfig.$project"; - $datadir = "data/$project"; - } else { - $project = undef; - $localconfig = "localconfig"; - $datadir = "data"; - } - - $datadir = "$libpath/$datadir"; - # We have to return absolute paths for mod_perl. - # That means that if you modify these paths, they must be absolute paths. - return { - 'libpath' => $libpath, - 'ext_libpath' => "$libpath/lib", - # If you put the libraries in a different location than the CGIs, - # make sure this still points to the CGIs. - 'cgi_path' => $libpath, - 'templatedir' => "$libpath/template", - 'template_cache' => "$datadir/template", - 'project' => $project, - 'localconfig' => "$libpath/$localconfig", - 'datadir' => $datadir, - 'attachdir' => "$datadir/attachments", - 'skinsdir' => "$libpath/skins", - 'graphsdir' => "$libpath/graphs", - # $webdotdir must be in the web server's tree somewhere. Even if you use a - # local dot, we output images to there. Also, if $webdotdir is - # not relative to the bugzilla root directory, you'll need to - # change showdependencygraph.cgi to set image_url to the correct - # location. - # The script should really generate these graphs directly... - 'webdotdir' => "$datadir/webdot", - 'extensionsdir' => "$libpath/extensions", - 'assetsdir' => "$datadir/assets", - }; + 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 + # directory under both mod_cgi and mod_perl. We call dirname twice + # to get the name of the directory above the "Bugzilla/" directory. + # + # Calling dirname twice like that won't work on VMS or AmigaOS + # but I doubt anybody runs Bugzilla on those. + # + # On mod_cgi this will be a relative path. On mod_perl it will be an + # absolute path. + my $libpath = dirname(dirname($INC{'Bugzilla/Constants.pm'})); + + # We have to detaint $libpath, but we can't use Bugzilla::Util here. + $libpath =~ /(.*)/; + $libpath = $1; + + my ($localconfig, $datadir); + if ($project && $project =~ /^(\w+)$/) { + $project = $1; + $localconfig = "localconfig.$project"; + $datadir = "data/$project"; + } + else { + $project = undef; + $localconfig = "localconfig"; + $datadir = "data"; + } + + $datadir = "$libpath/$datadir"; + + # We have to return absolute paths for mod_perl. + # That means that if you modify these paths, they must be absolute paths. + return { + 'libpath' => $libpath, + 'ext_libpath' => "$libpath/lib", + + # If you put the libraries in a different location than the CGIs, + # make sure this still points to the CGIs. + 'cgi_path' => $libpath, + 'templatedir' => "$libpath/template", + 'template_cache' => "$datadir/template", + 'project' => $project, + 'localconfig' => "$libpath/$localconfig", + 'datadir' => $datadir, + 'attachdir' => "$datadir/attachments", + 'skinsdir' => "$libpath/skins", + 'graphsdir' => "$libpath/graphs", + + # $webdotdir must be in the web server's tree somewhere. Even if you use a + # local dot, we output images to there. Also, if $webdotdir is + # not relative to the bugzilla root directory, you'll need to + # change showdependencygraph.cgi to set image_url to the correct + # location. + # The script should really generate these graphs directly... + 'webdotdir' => "$datadir/webdot", + 'extensionsdir' => "$libpath/extensions", + 'assetsdir' => "$datadir/assets", + }; } # 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 5bc83f9d6..029e05f03 100644 --- a/Bugzilla/DB.pm +++ b/Bugzilla/DB.pm @@ -33,7 +33,7 @@ use Storable qw(dclone); # Constants ##################################################################### -use constant BLOB_TYPE => DBI::SQL_BLOB; +use constant BLOB_TYPE => DBI::SQL_BLOB; use constant ISOLATION_LEVEL => 'REPEATABLE READ'; # Set default values for what used to be the enum types. These values @@ -46,14 +46,14 @@ use constant ISOLATION_LEVEL => 'REPEATABLE READ'; # Bugzilla with enums. After that, they are either controlled through # the Bugzilla UI or through the DB. use constant ENUM_DEFAULTS => { - bug_severity => ['blocker', 'critical', 'major', 'normal', - 'minor', 'trivial', 'enhancement'], - priority => ["Highest", "High", "Normal", "Low", "Lowest", "---"], - op_sys => ["All","Windows","Mac OS","Linux","Other"], - rep_platform => ["All","PC","Macintosh","Other"], - bug_status => ["UNCONFIRMED","CONFIRMED","IN_PROGRESS","RESOLVED", - "VERIFIED"], - resolution => ["","FIXED","INVALID","WONTFIX", "DUPLICATE","WORKSFORME"], + bug_severity => + ['blocker', 'critical', 'major', 'normal', 'minor', 'trivial', 'enhancement'], + priority => ["Highest", "High", "Normal", "Low", "Lowest", "---"], + op_sys => ["All", "Windows", "Mac OS", "Linux", "Other"], + rep_platform => ["All", "PC", "Macintosh", "Other"], + bug_status => + ["UNCONFIRMED", "CONFIRMED", "IN_PROGRESS", "RESOLVED", "VERIFIED"], + resolution => ["", "FIXED", "INVALID", "WONTFIX", "DUPLICATE", "WORKSFORME"], }; # The character that means "OR" in a boolean fulltext search. If empty, @@ -83,14 +83,14 @@ use constant WORD_END => '($|[^[:alnum:]])'; use constant INDEX_DROPS_REQUIRE_FK_DROPS => 1; ##################################################################### -# Overridden Superclass Methods +# Overridden Superclass Methods ##################################################################### sub quote { - my $self = shift; - my $retval = $self->SUPER::quote(@_); - trick_taint($retval) if defined $retval; - return $retval; + my $self = shift; + my $retval = $self->SUPER::quote(@_); + trick_taint($retval) if defined $retval; + return $retval; } ##################################################################### @@ -98,95 +98,94 @@ sub quote { ##################################################################### sub connect_shadow { - my $params = Bugzilla->params; - die "Tried to connect to non-existent shadowdb" - unless $params->{'shadowdb'}; + my $params = Bugzilla->params; + die "Tried to connect to non-existent shadowdb" unless $params->{'shadowdb'}; - # Instead of just passing in a new hashref, we locally modify the - # values of "localconfig", because some drivers access it while - # connecting. - my %connect_params = %{ Bugzilla->localconfig }; - $connect_params{db_host} = $params->{'shadowdbhost'}; - $connect_params{db_name} = $params->{'shadowdb'}; - $connect_params{db_port} = $params->{'shadowdbport'}; - $connect_params{db_sock} = $params->{'shadowdbsock'}; + # Instead of just passing in a new hashref, we locally modify the + # values of "localconfig", because some drivers access it while + # connecting. + my %connect_params = %{Bugzilla->localconfig}; + $connect_params{db_host} = $params->{'shadowdbhost'}; + $connect_params{db_name} = $params->{'shadowdb'}; + $connect_params{db_port} = $params->{'shadowdbport'}; + $connect_params{db_sock} = $params->{'shadowdbsock'}; - return _connect(\%connect_params); + return _connect(\%connect_params); } sub connect_main { - my $lc = Bugzilla->localconfig; - return _connect(Bugzilla->localconfig); + my $lc = Bugzilla->localconfig; + return _connect(Bugzilla->localconfig); } sub _connect { - my ($params) = @_; + my ($params) = @_; - my $driver = $params->{db_driver}; - my $pkg_module = DB_MODULE->{lc($driver)}->{db}; + my $driver = $params->{db_driver}; + my $pkg_module = DB_MODULE->{lc($driver)}->{db}; - # do the actual import - eval ("require $pkg_module") - || die ("'$driver' is not a valid choice for \$db_driver in " - . " localconfig: " . $@); + # do the actual import + eval("require $pkg_module") + || die( + "'$driver' is not a valid choice for \$db_driver in " . " localconfig: " . $@); - # instantiate the correct DB specific module - my $dbh = $pkg_module->new($params); + # instantiate the correct DB specific module + my $dbh = $pkg_module->new($params); - return $dbh; + return $dbh; } sub _handle_error { - require Carp; + require Carp; - # Cut down the error string to a reasonable size - $_[0] = substr($_[0], 0, 2000) . ' ... ' . substr($_[0], -2000) - if length($_[0]) > 4000; - $_[0] = Carp::longmess($_[0]); - return 0; # Now let DBI handle raising the error + # Cut down the error string to a reasonable size + $_[0] = substr($_[0], 0, 2000) . ' ... ' . substr($_[0], -2000) + if length($_[0]) > 4000; + $_[0] = Carp::longmess($_[0]); + return 0; # Now let DBI handle raising the error } sub bz_check_requirements { - my ($output) = @_; + my ($output) = @_; - my $lc = Bugzilla->localconfig; - my $db = DB_MODULE->{lc($lc->{db_driver})}; + my $lc = Bugzilla->localconfig; + my $db = DB_MODULE->{lc($lc->{db_driver})}; - # Only certain values are allowed for $db_driver. - if (!defined $db) { - die "$lc->{db_driver} is not a valid choice for \$db_driver in" - . bz_locations()->{'localconfig'}; - } + # Only certain values are allowed for $db_driver. + if (!defined $db) { + die "$lc->{db_driver} is not a valid choice for \$db_driver in" + . bz_locations()->{'localconfig'}; + } - # Check the existence and version of the DBD that we need. - my $dbd = $db->{dbd}; - _bz_check_dbd($db, $output); + # Check the existence and version of the DBD that we need. + my $dbd = $db->{dbd}; + _bz_check_dbd($db, $output); - # We don't try to connect to the actual database if $db_check is - # disabled. - unless ($lc->{db_check}) { - print "\n" if $output; - return; - } + # We don't try to connect to the actual database if $db_check is + # disabled. + unless ($lc->{db_check}) { + print "\n" if $output; + return; + } - # And now check the version of the database server itself. - my $dbh = _get_no_db_connection(); - $dbh->bz_check_server_version($db, $output); + # And now check the version of the database server itself. + my $dbh = _get_no_db_connection(); + $dbh->bz_check_server_version($db, $output); - print "\n" if $output; + print "\n" if $output; } sub _bz_check_dbd { - my ($db, $output) = @_; + my ($db, $output) = @_; - my $dbd = $db->{dbd}; - unless (have_vers($dbd, $output)) { - my $sql_server = $db->{name}; - my $command = install_command($dbd); - my $root = ROOT_USER; - my $dbd_mod = $dbd->{module}; - my $dbd_ver = $dbd->{version}; - die <{dbd}; + unless (have_vers($dbd, $output)) { + my $sql_server = $db->{name}; + my $command = install_command($dbd); + my $root = ROOT_USER; + my $dbd_mod = $dbd->{module}; + my $dbd_ver = $dbd->{version}; + die <bz_server_version; - $self->disconnect; + my $sql_vers = $self->bz_server_version; + $self->disconnect; - my $sql_want = $db->{db_version}; - my $version_ok = vers_cmp($sql_vers, $sql_want) > -1 ? 1 : 0; + my $sql_want = $db->{db_version}; + my $version_ok = vers_cmp($sql_vers, $sql_want) > -1 ? 1 : 0; - my $sql_server = $db->{name}; - if ($output) { - Bugzilla::Install::Requirements::_checking_for({ - package => $sql_server, wanted => $sql_want, - found => $sql_vers, ok => $version_ok }); - } + my $sql_server = $db->{name}; + if ($output) { + Bugzilla::Install::Requirements::_checking_for({ + package => $sql_server, + wanted => $sql_want, + found => $sql_vers, + ok => $version_ok + }); + } - # 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 (!$version_ok) { - die <localconfig->{db_name}; - - if (!$conn_success) { - $dbh = _get_no_db_connection(); - say "Creating database $db_name..."; - - # Try to create the DB, and if we fail print a friendly error. - my $success = eval { - my @sql = $dbh->_bz_schema->get_create_database_sql($db_name); - # This ends with 1 because this particular do doesn't always - # return something. - $dbh->do($_) foreach @sql; 1; - }; - if (!$success) { - my $error = $dbh->errstr || $@; - chomp($error); - die "The '$db_name' database could not be created.", - " The error returned was:\n\n $error\n\n", - _bz_connect_error_reasons(); - } + my $dbh; + + # See if we can connect to the actual Bugzilla database. + my $conn_success = eval { $dbh = connect_main() }; + my $db_name = Bugzilla->localconfig->{db_name}; + + if (!$conn_success) { + $dbh = _get_no_db_connection(); + say "Creating database $db_name..."; + + # Try to create the DB, and if we fail print a friendly error. + my $success = eval { + my @sql = $dbh->_bz_schema->get_create_database_sql($db_name); + + # This ends with 1 because this particular do doesn't always + # return something. + $dbh->do($_) foreach @sql; + 1; + }; + if (!$success) { + my $error = $dbh->errstr || $@; + chomp($error); + die "The '$db_name' database could not be created.", + " The error returned was:\n\n $error\n\n", _bz_connect_error_reasons(); } + } - $dbh->disconnect; + $dbh->disconnect; } # A helper for bz_create_database and bz_check_requirements. sub _get_no_db_connection { - my ($sql_server) = @_; - my $dbh; - my %connect_params = %{ Bugzilla->localconfig }; - $connect_params{db_name} = ''; - my $conn_success = eval { - $dbh = _connect(\%connect_params); - }; - if (!$conn_success) { - my $driver = $connect_params{db_driver}; - my $sql_server = DB_MODULE->{lc($driver)}->{name}; - # Can't use $dbh->errstr because $dbh is undef. - my $error = $DBI::errstr || $@; - chomp($error); - die "There was an error connecting to $sql_server:\n\n", - " $error\n\n", _bz_connect_error_reasons(), "\n"; - } - return $dbh; + my ($sql_server) = @_; + my $dbh; + my %connect_params = %{Bugzilla->localconfig}; + $connect_params{db_name} = ''; + my $conn_success = eval { $dbh = _connect(\%connect_params); }; + if (!$conn_success) { + my $driver = $connect_params{db_driver}; + my $sql_server = DB_MODULE->{lc($driver)}->{name}; + + # Can't use $dbh->errstr because $dbh is undef. + my $error = $DBI::errstr || $@; + chomp($error); + die "There was an error connecting to $sql_server:\n\n", " $error\n\n", + _bz_connect_error_reasons(), "\n"; + } + return $dbh; } # Just a helper because we have to re-use this text. # We don't use this in db_new because it gives away the database # username, and db_new errors can show up on CGIs. sub _bz_connect_error_reasons { - my $lc_file = bz_locations()->{'localconfig'}; - my $lc = Bugzilla->localconfig; - my $db = DB_MODULE->{lc($lc->{db_driver})}; - my $server = $db->{name}; + my $lc_file = bz_locations()->{'localconfig'}; + my $lc = Bugzilla->localconfig; + my $db = DB_MODULE->{lc($lc->{db_driver})}; + my $server = $db->{name}; -return <can($meth) - or die("Class $pkg does not define method $meth"); - } + my $pkg = shift; + + # do not check this module + if ($pkg ne __PACKAGE__) { + + # make sure all abstract methods are implemented + foreach my $meth (@_abstract_methods) { + $pkg->can($meth) or die("Class $pkg does not define method $meth"); } + } - # Now we want to call our superclass implementation. - # If our superclass is Exporter, which is using caller() to find - # a namespace to populate, we need to adjust for this extra call. - # All this can go when we stop using deprecated functions. - my $is_exporter = $pkg->isa('Exporter'); - $Exporter::ExportLevel++ if $is_exporter; - $pkg->SUPER::import(@_); - $Exporter::ExportLevel-- if $is_exporter; + # Now we want to call our superclass implementation. + # If our superclass is Exporter, which is using caller() to find + # a namespace to populate, we need to adjust for this extra call. + # All this can go when we stop using deprecated functions. + my $is_exporter = $pkg->isa('Exporter'); + $Exporter::ExportLevel++ if $is_exporter; + $pkg->SUPER::import(@_); + $Exporter::ExportLevel-- if $is_exporter; } sub sql_istrcmp { - my ($self, $left, $right, $op) = @_; - $op ||= "="; + my ($self, $left, $right, $op) = @_; + $op ||= "="; - return $self->sql_istring($left) . " $op " . $self->sql_istring($right); + return $self->sql_istring($left) . " $op " . $self->sql_istring($right); } sub sql_istring { - my ($self, $string) = @_; + my ($self, $string) = @_; - return "LOWER($string)"; + return "LOWER($string)"; } sub sql_iposition { - my ($self, $fragment, $text) = @_; - $fragment = $self->sql_istring($fragment); - $text = $self->sql_istring($text); - return $self->sql_position($fragment, $text); + my ($self, $fragment, $text) = @_; + $fragment = $self->sql_istring($fragment); + $text = $self->sql_istring($text); + return $self->sql_position($fragment, $text); } sub sql_position { - my ($self, $fragment, $text) = @_; + my ($self, $fragment, $text) = @_; - return "POSITION($fragment IN $text)"; + return "POSITION($fragment IN $text)"; } sub sql_like { - my ($self, $fragment, $column) = @_; + my ($self, $fragment, $column) = @_; - my $quoted = $self->quote($fragment); + my $quoted = $self->quote($fragment); - return $self->sql_position($quoted, $column) . " > 0"; + return $self->sql_position($quoted, $column) . " > 0"; } sub sql_ilike { - my ($self, $fragment, $column) = @_; + my ($self, $fragment, $column) = @_; - my $quoted = $self->quote($fragment); + my $quoted = $self->quote($fragment); - return $self->sql_iposition($quoted, $column) . " > 0"; + return $self->sql_iposition($quoted, $column) . " > 0"; } sub sql_not_ilike { - my ($self, $fragment, $column) = @_; + my ($self, $fragment, $column) = @_; - my $quoted = $self->quote($fragment); + my $quoted = $self->quote($fragment); - return $self->sql_iposition($quoted, $column) . " = 0"; + return $self->sql_iposition($quoted, $column) . " = 0"; } sub sql_group_by { - my ($self, $needed_columns, $optional_columns) = @_; + my ($self, $needed_columns, $optional_columns) = @_; - my $expression = "GROUP BY $needed_columns"; - $expression .= ", " . $optional_columns if $optional_columns; - - return $expression; + my $expression = "GROUP BY $needed_columns"; + $expression .= ", " . $optional_columns if $optional_columns; + + return $expression; } sub sql_string_concat { - my ($self, @params) = @_; - - return '(' . join(' || ', @params) . ')'; + my ($self, @params) = @_; + + return '(' . join(' || ', @params) . ')'; } sub sql_string_until { - my ($self, $string, $substring) = @_; + my ($self, $string, $substring) = @_; - my $position = $self->sql_position($substring, $string); - return "CASE WHEN $position != 0" - . " THEN SUBSTR($string, 1, $position - 1)" - . " ELSE $string END"; + my $position = $self->sql_position($substring, $string); + return + "CASE WHEN $position != 0" + . " THEN SUBSTR($string, 1, $position - 1)" + . " ELSE $string END"; } sub sql_in { - my ($self, $column_name, $in_list_ref, $negate) = @_; - return " $column_name " - . ($negate ? "NOT " : "") - . "IN (" . join(',', @$in_list_ref) . ") "; + my ($self, $column_name, $in_list_ref, $negate) = @_; + return + " $column_name " + . ($negate ? "NOT " : "") . "IN (" + . join(',', @$in_list_ref) . ") "; } sub sql_fulltext_search { - my ($self, $column, $text) = @_; - - # This is as close as we can get to doing full text search using - # standard ANSI SQL, without real full text search support. DB specific - # modules should override this, as this will be always much slower. - - # make the string lowercase to do case insensitive search - my $lower_text = lc($text); - - # split the text we're searching for into separate words. As a hack - # to allow quicksearch to work, if the field starts and ends with - # a double-quote, then we don't split it into words. We can't use - # Text::ParseWords here because it gets very confused by unbalanced - # quotes, which breaks searches like "don't try this" (because of the - # unbalanced single-quote in "don't"). - my @words; - if ($lower_text =~ /^"/ and $lower_text =~ /"$/) { - $lower_text =~ s/^"//; - $lower_text =~ s/"$//; - @words = ($lower_text); - } - else { - @words = split(/\s+/, $lower_text); - } - - # surround the words with wildcards and SQL quotes so we can use them - # in LIKE search clauses - @words = map($self->quote("\%$_\%"), @words); - - # untaint words, since they are safe to use now that we've quoted them - trick_taint($_) foreach @words; - - # turn the words into a set of LIKE search clauses - @words = map("LOWER($column) LIKE $_", @words); - - # search for occurrences of all specified words in the column - return join (" AND ", @words), "CASE WHEN (" . join(" AND ", @words) . ") THEN 1 ELSE 0 END"; + my ($self, $column, $text) = @_; + + # This is as close as we can get to doing full text search using + # standard ANSI SQL, without real full text search support. DB specific + # modules should override this, as this will be always much slower. + + # make the string lowercase to do case insensitive search + my $lower_text = lc($text); + + # split the text we're searching for into separate words. As a hack + # to allow quicksearch to work, if the field starts and ends with + # a double-quote, then we don't split it into words. We can't use + # Text::ParseWords here because it gets very confused by unbalanced + # quotes, which breaks searches like "don't try this" (because of the + # unbalanced single-quote in "don't"). + my @words; + if ($lower_text =~ /^"/ and $lower_text =~ /"$/) { + $lower_text =~ s/^"//; + $lower_text =~ s/"$//; + @words = ($lower_text); + } + else { + @words = split(/\s+/, $lower_text); + } + + # surround the words with wildcards and SQL quotes so we can use them + # in LIKE search clauses + @words = map($self->quote("\%$_\%"), @words); + + # untaint words, since they are safe to use now that we've quoted them + trick_taint($_) foreach @words; + + # turn the words into a set of LIKE search clauses + @words = map("LOWER($column) LIKE $_", @words); + + # search for occurrences of all specified words in the column + return join(" AND ", @words), + "CASE WHEN (" . join(" AND ", @words) . ") THEN 1 ELSE 0 END"; } ##################################################################### @@ -465,24 +471,27 @@ sub sql_fulltext_search { # XXX - Needs to be documented. sub bz_server_version { - my ($self) = @_; - return $self->get_info(18); # SQL_DBMS_VER + my ($self) = @_; + return $self->get_info(18); # SQL_DBMS_VER } sub bz_last_key { - my ($self, $table, $column) = @_; + my ($self, $table, $column) = @_; - return $self->last_insert_id(Bugzilla->localconfig->{db_name}, undef, - $table, $column); + return $self->last_insert_id(Bugzilla->localconfig->{db_name}, + undef, $table, $column); } sub bz_check_regexp { - my ($self, $pattern) = @_; + my ($self, $pattern) = @_; - eval { $self->do("SELECT " . $self->sql_regexp($self->quote("a"), $pattern, 1)) }; + eval { + $self->do("SELECT " . $self->sql_regexp($self->quote("a"), $pattern, 1)); + }; - $@ && ThrowUserError('illegal_regexp', - { value => $pattern, dberror => $self->errstr }); + $@ + && ThrowUserError('illegal_regexp', + {value => $pattern, dberror => $self->errstr}); } ##################################################################### @@ -490,99 +499,100 @@ sub bz_check_regexp { ##################################################################### sub bz_setup_database { - my ($self) = @_; - - # If we haven't ever stored a serialized schema, - # set up the bz_schema table and store it. - $self->_bz_init_schema_storage(); - - # We don't use bz_table_list here, because that uses _bz_real_schema. - # We actually want the table list from the ABSTRACT_SCHEMA in - # Bugzilla::DB::Schema. - my @desired_tables = $self->_bz_schema->get_table_list(); - my $bugs_exists = $self->bz_table_info('bugs'); - if (!$bugs_exists) { - say install_string('db_table_setup'); - } + my ($self) = @_; - foreach my $table_name (@desired_tables) { - $self->bz_add_table($table_name, { silently => !$bugs_exists }); - } + # If we haven't ever stored a serialized schema, + # set up the bz_schema table and store it. + $self->_bz_init_schema_storage(); + + # We don't use bz_table_list here, because that uses _bz_real_schema. + # We actually want the table list from the ABSTRACT_SCHEMA in + # Bugzilla::DB::Schema. + my @desired_tables = $self->_bz_schema->get_table_list(); + my $bugs_exists = $self->bz_table_info('bugs'); + if (!$bugs_exists) { + say install_string('db_table_setup'); + } + + foreach my $table_name (@desired_tables) { + $self->bz_add_table($table_name, {silently => !$bugs_exists}); + } } # This really just exists to get overridden in Bugzilla::DB::Mysql. sub bz_enum_initial_values { - return ENUM_DEFAULTS; + return ENUM_DEFAULTS; } sub bz_populate_enum_tables { - my ($self) = @_; + my ($self) = @_; - my $any_severities = $self->selectrow_array( - 'SELECT 1 FROM bug_severity ' . $self->sql_limit(1)); - print install_string('db_enum_setup'), "\n " if !$any_severities; + my $any_severities + = $self->selectrow_array('SELECT 1 FROM bug_severity ' . $self->sql_limit(1)); + print install_string('db_enum_setup'), "\n " if !$any_severities; - my $enum_values = $self->bz_enum_initial_values(); - while (my ($table, $values) = each %$enum_values) { - $self->_bz_populate_enum_table($table, $values); - } + my $enum_values = $self->bz_enum_initial_values(); + while (my ($table, $values) = each %$enum_values) { + $self->_bz_populate_enum_table($table, $values); + } - print "\n" if !$any_severities; + print "\n" if !$any_severities; } sub bz_setup_foreign_keys { - my ($self) = @_; - - # profiles_activity was the first table to get foreign keys, - # so if it doesn't have them, then we're setting up FKs - # for the first time, and should be quieter about it. - my $activity_fk = $self->bz_fk_info('profiles_activity', 'userid'); - my $any_fks = $activity_fk && $activity_fk->{created}; - if (!$any_fks) { - say get_text('install_fk_setup'); - } + my ($self) = @_; + + # profiles_activity was the first table to get foreign keys, + # so if it doesn't have them, then we're setting up FKs + # for the first time, and should be quieter about it. + my $activity_fk = $self->bz_fk_info('profiles_activity', 'userid'); + my $any_fks = $activity_fk && $activity_fk->{created}; + if (!$any_fks) { + say get_text('install_fk_setup'); + } + + my @tables = $self->bz_table_list(); + foreach my $table (@tables) { + my @columns = $self->bz_table_columns($table); + my %add_fks; + foreach my $column (@columns) { - my @tables = $self->bz_table_list(); - foreach my $table (@tables) { - my @columns = $self->bz_table_columns($table); - my %add_fks; - foreach my $column (@columns) { - # First we check for any FKs that have created => 0, - # in the _bz_real_schema. This also picks up FKs with - # created => 1, but bz_add_fks will ignore those. - my $fk = $self->bz_fk_info($table, $column); - # Then we check the abstract schema to see if there - # should be an FK on this column, but one wasn't set in the - # _bz_real_schema for some reason. We do this to handle - # various problems caused by upgrading from versions - # prior to 4.2, and also to handle problems caused - # by enabling an extension pre-4.2, disabling it for - # the 4.2 upgrade, and then re-enabling it later. - unless ($fk && $fk->{created}) { - my $standard_def = - $self->_bz_schema->get_column_abstract($table, $column); - if (exists $standard_def->{REFERENCES}) { - $fk = dclone($standard_def->{REFERENCES}); - } - } - - $add_fks{$column} = $fk if $fk; + # First we check for any FKs that have created => 0, + # in the _bz_real_schema. This also picks up FKs with + # created => 1, but bz_add_fks will ignore those. + my $fk = $self->bz_fk_info($table, $column); + + # Then we check the abstract schema to see if there + # should be an FK on this column, but one wasn't set in the + # _bz_real_schema for some reason. We do this to handle + # various problems caused by upgrading from versions + # prior to 4.2, and also to handle problems caused + # by enabling an extension pre-4.2, disabling it for + # the 4.2 upgrade, and then re-enabling it later. + unless ($fk && $fk->{created}) { + my $standard_def = $self->_bz_schema->get_column_abstract($table, $column); + if (exists $standard_def->{REFERENCES}) { + $fk = dclone($standard_def->{REFERENCES}); } - $self->bz_add_fks($table, \%add_fks, { silently => !$any_fks }); + } + + $add_fks{$column} = $fk if $fk; } + $self->bz_add_fks($table, \%add_fks, {silently => !$any_fks}); + } } # This is used by contrib/bzdbcopy.pl, mostly. sub bz_drop_foreign_keys { - my ($self) = @_; + my ($self) = @_; - my @tables = $self->bz_table_list(); - foreach my $table (@tables) { - my @columns = $self->bz_table_columns($table); - foreach my $column (@columns) { - $self->bz_drop_fk($table, $column); - } + my @tables = $self->bz_table_list(); + foreach my $table (@tables) { + my @columns = $self->bz_table_columns($table); + foreach my $column (@columns) { + $self->bz_drop_fk($table, $column); } + } } ##################################################################### @@ -590,119 +600,121 @@ sub bz_drop_foreign_keys { ##################################################################### sub bz_add_column { - my ($self, $table, $name, $new_def, $init_value) = @_; - - # You can't add a NOT NULL column to a table with - # no DEFAULT statement, unless you have an init_value. - # SERIAL types are an exception, though, because they can - # auto-populate. - if ( $new_def->{NOTNULL} && !exists $new_def->{DEFAULT} - && !defined $init_value && $new_def->{TYPE} !~ /SERIAL/) - { - ThrowCodeError('column_not_null_without_default', - { name => "$table.$name" }); + my ($self, $table, $name, $new_def, $init_value) = @_; + + # You can't add a NOT NULL column to a table with + # no DEFAULT statement, unless you have an init_value. + # SERIAL types are an exception, though, because they can + # auto-populate. + if ( $new_def->{NOTNULL} + && !exists $new_def->{DEFAULT} + && !defined $init_value + && $new_def->{TYPE} !~ /SERIAL/) + { + ThrowCodeError('column_not_null_without_default', {name => "$table.$name"}); + } + + 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, $trimmed_def, + defined $init_value ? $self->quote($init_value) : undef); + print get_text('install_column_add', {column => $name, table => $table}) . "\n" + if Bugzilla->usage_mode == USAGE_MODE_CMDLINE; + foreach my $sql (@statements) { + $self->do($sql); } - 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, $trimmed_def, - defined $init_value ? $self->quote($init_value) : undef); - print get_text('install_column_add', - { column => $name, table => $table }) . "\n" - if Bugzilla->usage_mode == USAGE_MODE_CMDLINE; - foreach my $sql (@statements) { - $self->do($sql); - } - - # To make things easier for callers, if they don't specify - # a REFERENCES item, we pull it from the _bz_schema if the - # 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 = - $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; + # To make things easier for callers, if they don't specify + # a REFERENCES item, we pull it from the _bz_schema if the + # 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 = $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; + } } sub bz_add_fk { - my ($self, $table, $column, $def) = @_; - $self->bz_add_fks($table, { $column => $def }); + my ($self, $table, $column, $def) = @_; + $self->bz_add_fks($table, {$column => $def}); } sub bz_add_fks { - my ($self, $table, $column_fks, $options) = @_; - - my %add_these; - foreach my $column (keys %$column_fks) { - my $current_fk = $self->bz_fk_info($table, $column); - next if ($current_fk and $current_fk->{created}); - my $new_fk = $column_fks->{$column}; - $self->_check_references($table, $column, $new_fk); - $add_these{$column} = $new_fk; - if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE - and !$options->{silently}) - { - print get_text('install_fk_add', - { table => $table, column => $column, - fk => $new_fk }), "\n"; - } + my ($self, $table, $column_fks, $options) = @_; + + my %add_these; + foreach my $column (keys %$column_fks) { + my $current_fk = $self->bz_fk_info($table, $column); + next if ($current_fk and $current_fk->{created}); + my $new_fk = $column_fks->{$column}; + $self->_check_references($table, $column, $new_fk); + $add_these{$column} = $new_fk; + if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE and !$options->{silently}) { + print get_text( + 'install_fk_add', {table => $table, column => $column, fk => $new_fk} + ), + "\n"; } + } - return if !scalar(keys %add_these); + return if !scalar(keys %add_these); - my @sql = $self->_bz_real_schema->get_add_fks_sql($table, \%add_these); - $self->do($_) foreach @sql; + my @sql = $self->_bz_real_schema->get_add_fks_sql($table, \%add_these); + $self->do($_) foreach @sql; - foreach my $column (keys %add_these) { - my $fk_def = $add_these{$column}; - $fk_def->{created} = 1; - $self->_bz_real_schema->set_fk($table, $column, $fk_def); - } + foreach my $column (keys %add_these) { + my $fk_def = $add_these{$column}; + $fk_def->{created} = 1; + $self->_bz_real_schema->set_fk($table, $column, $fk_def); + } - $self->_bz_store_real_schema(); + $self->_bz_store_real_schema(); } sub bz_alter_column { - my ($self, $table, $name, $new_def, $set_nulls_to) = @_; + my ($self, $table, $name, $new_def, $set_nulls_to) = @_; - my $current_def = $self->bz_column_info($table, $name); + my $current_def = $self->bz_column_info($table, $name); - if (!$self->_bz_schema->columns_equal($current_def, $new_def)) { - # You can't change a column to be NOT NULL if you have no DEFAULT - # and no value for $set_nulls_to, if there are any NULL values - # in that column. - if ($new_def->{NOTNULL} && - !exists $new_def->{DEFAULT} && !defined $set_nulls_to) - { - # Check for NULLs - my $any_nulls = $self->selectrow_array( - "SELECT 1 FROM $table WHERE $name IS NULL"); - ThrowCodeError('column_not_null_no_default_alter', - { name => "$table.$name" }) if ($any_nulls); - } - # Preserve foreign key definitions in the Schema object when altering - # types. - if (my $fk = $self->bz_fk_info($table, $name)) { - $new_def->{REFERENCES} = $fk; - } - $self->bz_alter_column_raw($table, $name, $new_def, $current_def, - $set_nulls_to); - $self->_bz_real_schema->set_column($table, $name, $new_def); - $self->_bz_store_real_schema; + if (!$self->_bz_schema->columns_equal($current_def, $new_def)) { + + # You can't change a column to be NOT NULL if you have no DEFAULT + # and no value for $set_nulls_to, if there are any NULL values + # in that column. + if ( $new_def->{NOTNULL} + && !exists $new_def->{DEFAULT} + && !defined $set_nulls_to) + { + # Check for NULLs + my $any_nulls + = $self->selectrow_array("SELECT 1 FROM $table WHERE $name IS NULL"); + ThrowCodeError('column_not_null_no_default_alter', {name => "$table.$name"}) + if ($any_nulls); + } + + # Preserve foreign key definitions in the Schema object when altering + # types. + if (my $fk = $self->bz_fk_info($table, $name)) { + $new_def->{REFERENCES} = $fk; } + $self->bz_alter_column_raw($table, $name, $new_def, $current_def, + $set_nulls_to); + $self->_bz_real_schema->set_column($table, $name, $new_def); + $self->_bz_store_real_schema; + } } @@ -728,39 +740,40 @@ sub bz_alter_column { # Returns: nothing # sub bz_alter_column_raw { - my ($self, $table, $name, $new_def, $current_def, $set_nulls_to) = @_; - my @statements = $self->_bz_real_schema->get_alter_column_ddl( - $table, $name, $new_def, - defined $set_nulls_to ? $self->quote($set_nulls_to) : undef); - my $new_ddl = $self->_bz_schema->get_type_ddl($new_def); - say "Updating column $name in table $table ..."; - if (defined $current_def) { - my $old_ddl = $self->_bz_schema->get_type_ddl($current_def); - say "Old: $old_ddl"; - } - say "New: $new_ddl"; - $self->do($_) foreach (@statements); + my ($self, $table, $name, $new_def, $current_def, $set_nulls_to) = @_; + my @statements + = $self->_bz_real_schema->get_alter_column_ddl($table, $name, $new_def, + defined $set_nulls_to ? $self->quote($set_nulls_to) : undef); + my $new_ddl = $self->_bz_schema->get_type_ddl($new_def); + say "Updating column $name in table $table ..."; + if (defined $current_def) { + my $old_ddl = $self->_bz_schema->get_type_ddl($current_def); + say "Old: $old_ddl"; + } + say "New: $new_ddl"; + $self->do($_) foreach (@statements); } sub bz_alter_fk { - my ($self, $table, $column, $fk_def) = @_; - my $current_fk = $self->bz_fk_info($table, $column); - ThrowCodeError('column_alter_nonexistent_fk', - { table => $table, column => $column }) if !$current_fk; - $self->bz_drop_fk($table, $column); - $self->bz_add_fk($table, $column, $fk_def); + my ($self, $table, $column, $fk_def) = @_; + my $current_fk = $self->bz_fk_info($table, $column); + ThrowCodeError('column_alter_nonexistent_fk', + {table => $table, column => $column}) + if !$current_fk; + $self->bz_drop_fk($table, $column); + $self->bz_add_fk($table, $column, $fk_def); } sub bz_add_index { - my ($self, $table, $name, $definition) = @_; + my ($self, $table, $name, $definition) = @_; - my $index_exists = $self->bz_index_info($table, $name); + my $index_exists = $self->bz_index_info($table, $name); - if (!$index_exists) { - $self->bz_add_index_raw($table, $name, $definition); - $self->_bz_real_schema->set_index($table, $name, $definition); - $self->_bz_store_real_schema; - } + if (!$index_exists) { + $self->bz_add_index_raw($table, $name, $definition); + $self->_bz_real_schema->set_index($table, $name, $definition); + $self->_bz_store_real_schema; + } } # bz_add_index_raw($table, $name, $silent) @@ -780,36 +793,36 @@ sub bz_add_index { # Returns: nothing # sub bz_add_index_raw { - my ($self, $table, $name, $definition, $silent) = @_; - my @statements = $self->_bz_schema->get_add_index_ddl( - $table, $name, $definition); - print "Adding new index '$name' to the $table table ...\n" unless $silent; - $self->do($_) foreach (@statements); + my ($self, $table, $name, $definition, $silent) = @_; + my @statements + = $self->_bz_schema->get_add_index_ddl($table, $name, $definition); + print "Adding new index '$name' to the $table table ...\n" unless $silent; + $self->do($_) foreach (@statements); } sub bz_add_table { - my ($self, $name, $options) = @_; - - my $table_exists = $self->bz_table_info($name); - - if (!$table_exists) { - $self->_bz_add_table_raw($name, $options); - my $table_def = dclone($self->_bz_schema->get_table_abstract($name)); - - my %fields = @{$table_def->{FIELDS}}; - foreach my $col (keys %fields) { - # Foreign Key references have to be added by Install::DB after - # initial table creation, because column names have changed - # over history and it's impossible to keep track of that info - # in ABSTRACT_SCHEMA. - next unless exists $fields{$col}->{REFERENCES}; - $fields{$col}->{REFERENCES}->{created} = - $self->_bz_real_schema->FK_ON_CREATE; - } - - $self->_bz_real_schema->add_table($name, $table_def); - $self->_bz_store_real_schema; + my ($self, $name, $options) = @_; + + my $table_exists = $self->bz_table_info($name); + + if (!$table_exists) { + $self->_bz_add_table_raw($name, $options); + my $table_def = dclone($self->_bz_schema->get_table_abstract($name)); + + my %fields = @{$table_def->{FIELDS}}; + foreach my $col (keys %fields) { + + # Foreign Key references have to be added by Install::DB after + # initial table creation, because column names have changed + # over history and it's impossible to keep track of that info + # in ABSTRACT_SCHEMA. + next unless exists $fields{$col}->{REFERENCES}; + $fields{$col}->{REFERENCES}->{created} = $self->_bz_real_schema->FK_ON_CREATE; } + + $self->_bz_real_schema->add_table($name, $table_def); + $self->_bz_store_real_schema; + } } # _bz_add_table_raw($name) - Private @@ -821,164 +834,174 @@ sub bz_add_table { # _bz_init_schema_storage. Used when you don't # yet have a Schema object but you need to # add a table, for some reason. -# Params: $name - The name of the table you're creating. -# The definition for the table is pulled from +# Params: $name - The name of the table you're creating. +# The definition for the table is pulled from # _bz_schema. # Returns: nothing # sub _bz_add_table_raw { - my ($self, $name, $options) = @_; - my @statements = $self->_bz_schema->get_table_ddl($name); - if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE - and !$options->{silently}) - { - say install_string('db_table_new', { table => $name }); - } - $self->do($_) foreach (@statements); + my ($self, $name, $options) = @_; + my @statements = $self->_bz_schema->get_table_ddl($name); + if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE and !$options->{silently}) { + say install_string('db_table_new', {table => $name}); + } + $self->do($_) foreach (@statements); } sub _bz_add_field_table { - my ($self, $name, $schema_ref) = @_; - # We do nothing if the table already exists. - return if $self->bz_table_info($name); - - # Copy this so that we're not modifying the passed reference. - # (This avoids modifying a constant in Bugzilla::DB::Schema.) - my %table_schema = %$schema_ref; - my %indexes = @{ $table_schema{INDEXES} }; - my %fixed_indexes; - foreach my $key (keys %indexes) { - $fixed_indexes{$name . "_" . $key} = $indexes{$key}; - } - # INDEXES is supposed to be an arrayref, so we have to convert back. - my @indexes_array = %fixed_indexes; - $table_schema{INDEXES} = \@indexes_array; - # We add this to the abstract schema so that bz_add_table can find it. - $self->_bz_schema->add_table($name, \%table_schema); - $self->bz_add_table($name); + my ($self, $name, $schema_ref) = @_; + + # We do nothing if the table already exists. + return if $self->bz_table_info($name); + + # Copy this so that we're not modifying the passed reference. + # (This avoids modifying a constant in Bugzilla::DB::Schema.) + my %table_schema = %$schema_ref; + my %indexes = @{$table_schema{INDEXES}}; + my %fixed_indexes; + foreach my $key (keys %indexes) { + $fixed_indexes{$name . "_" . $key} = $indexes{$key}; + } + + # INDEXES is supposed to be an arrayref, so we have to convert back. + my @indexes_array = %fixed_indexes; + $table_schema{INDEXES} = \@indexes_array; + + # We add this to the abstract schema so that bz_add_table can find it. + $self->_bz_schema->add_table($name, \%table_schema); + $self->bz_add_table($name); } sub bz_add_field_tables { - my ($self, $field) = @_; - - $self->_bz_add_field_table($field->name, - $self->_bz_schema->FIELD_TABLE_SCHEMA, $field->type); - if ($field->type == FIELD_TYPE_MULTI_SELECT) { - my $ms_table = "bug_" . $field->name; - $self->_bz_add_field_table($ms_table, - $self->_bz_schema->MULTI_SELECT_VALUE_TABLE); - - $self->bz_add_fks($ms_table, - { bug_id => {TABLE => 'bugs', COLUMN => 'bug_id', - DELETE => 'CASCADE'}, - - value => {TABLE => $field->name, COLUMN => 'value'} }); - } + my ($self, $field) = @_; + + $self->_bz_add_field_table($field->name, $self->_bz_schema->FIELD_TABLE_SCHEMA, + $field->type); + if ($field->type == FIELD_TYPE_MULTI_SELECT) { + my $ms_table = "bug_" . $field->name; + $self->_bz_add_field_table($ms_table, + $self->_bz_schema->MULTI_SELECT_VALUE_TABLE); + + $self->bz_add_fks( + $ms_table, + { + bug_id => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'}, + + value => {TABLE => $field->name, COLUMN => 'value'} + } + ); + } } sub bz_drop_field_tables { - my ($self, $field) = @_; - if ($field->type == FIELD_TYPE_MULTI_SELECT) { - $self->bz_drop_table('bug_' . $field->name); - } - $self->bz_drop_table($field->name); + my ($self, $field) = @_; + if ($field->type == FIELD_TYPE_MULTI_SELECT) { + $self->bz_drop_table('bug_' . $field->name); + } + $self->bz_drop_table($field->name); } sub bz_drop_column { - my ($self, $table, $column) = @_; - - my $current_def = $self->bz_column_info($table, $column); - - if ($current_def) { - my @statements = $self->_bz_real_schema->get_drop_column_ddl( - $table, $column); - print get_text('install_column_drop', - { table => $table, column => $column }) . "\n" - if Bugzilla->usage_mode == USAGE_MODE_CMDLINE; - foreach my $sql (@statements) { - # Because this is a deletion, we don't want to die hard if - # we fail because of some local customization. If something - # is already gone, that's fine with us! - eval { $self->do($sql); } or warn "Failed SQL: [$sql] Error: $@"; - } - $self->_bz_real_schema->delete_column($table, $column); - $self->_bz_store_real_schema; + my ($self, $table, $column) = @_; + + my $current_def = $self->bz_column_info($table, $column); + + if ($current_def) { + my @statements = $self->_bz_real_schema->get_drop_column_ddl($table, $column); + print get_text('install_column_drop', {table => $table, column => $column}) + . "\n" + if Bugzilla->usage_mode == USAGE_MODE_CMDLINE; + foreach my $sql (@statements) { + + # Because this is a deletion, we don't want to die hard if + # we fail because of some local customization. If something + # is already gone, that's fine with us! + eval { $self->do($sql); } or warn "Failed SQL: [$sql] Error: $@"; } + $self->_bz_real_schema->delete_column($table, $column); + $self->_bz_store_real_schema; + } } sub bz_drop_fk { - my ($self, $table, $column) = @_; - - my $fk_def = $self->bz_fk_info($table, $column); - if ($fk_def and $fk_def->{created}) { - print get_text('install_fk_drop', - { table => $table, column => $column, fk => $fk_def }) - . "\n" if Bugzilla->usage_mode == USAGE_MODE_CMDLINE; - my @statements = - $self->_bz_real_schema->get_drop_fk_sql($table, $column, $fk_def); - foreach my $sql (@statements) { - # Because this is a deletion, we don't want to die hard if - # we fail because of some local customization. If something - # is already gone, that's fine with us! - eval { $self->do($sql); } or warn "Failed SQL: [$sql] Error: $@"; - } - # Under normal circumstances, we don't permanently drop the fk-- - # we want checksetup to re-create it again later. The only - # time that FKs get permanently dropped is if the column gets - # dropped. - $fk_def->{created} = 0; - $self->_bz_real_schema->set_fk($table, $column, $fk_def); - $self->_bz_store_real_schema; + my ($self, $table, $column) = @_; + + my $fk_def = $self->bz_fk_info($table, $column); + if ($fk_def and $fk_def->{created}) { + print get_text('install_fk_drop', + {table => $table, column => $column, fk => $fk_def}) + . "\n" + if Bugzilla->usage_mode == USAGE_MODE_CMDLINE; + my @statements + = $self->_bz_real_schema->get_drop_fk_sql($table, $column, $fk_def); + foreach my $sql (@statements) { + + # Because this is a deletion, we don't want to die hard if + # we fail because of some local customization. If something + # is already gone, that's fine with us! + eval { $self->do($sql); } or warn "Failed SQL: [$sql] Error: $@"; } + # Under normal circumstances, we don't permanently drop the fk-- + # we want checksetup to re-create it again later. The only + # time that FKs get permanently dropped is if the column gets + # dropped. + $fk_def->{created} = 0; + $self->_bz_real_schema->set_fk($table, $column, $fk_def); + $self->_bz_store_real_schema; + } + } sub bz_get_related_fks { - my ($self, $table, $column) = @_; - my @tables = $self->_bz_real_schema->get_table_list(); - my @related; - foreach my $check_table (@tables) { - my @columns = $self->bz_table_columns($check_table); - foreach my $check_column (@columns) { - my $fk = $self->bz_fk_info($check_table, $check_column); - if ($fk - and (($fk->{TABLE} eq $table and $fk->{COLUMN} eq $column) - or ($check_column eq $column and $check_table eq $table))) - { - push(@related, [$check_table, $check_column, $fk]); - } - } # foreach $column - } # foreach $table - - return \@related; + my ($self, $table, $column) = @_; + my @tables = $self->_bz_real_schema->get_table_list(); + my @related; + foreach my $check_table (@tables) { + my @columns = $self->bz_table_columns($check_table); + foreach my $check_column (@columns) { + my $fk = $self->bz_fk_info($check_table, $check_column); + if ( + $fk + and (($fk->{TABLE} eq $table and $fk->{COLUMN} eq $column) + or ($check_column eq $column and $check_table eq $table)) + ) + { + push(@related, [$check_table, $check_column, $fk]); + } + } # foreach $column + } # foreach $table + + return \@related; } sub bz_drop_related_fks { - my $self = shift; - my $related = $self->bz_get_related_fks(@_); - foreach my $item (@$related) { - my ($table, $column) = @$item; - $self->bz_drop_fk($table, $column); - } - return $related; + my $self = shift; + my $related = $self->bz_get_related_fks(@_); + foreach my $item (@$related) { + my ($table, $column) = @$item; + $self->bz_drop_fk($table, $column); + } + return $related; } sub bz_drop_index { - my ($self, $table, $name) = @_; + my ($self, $table, $name) = @_; - my $index_exists = $self->bz_index_info($table, $name); + my $index_exists = $self->bz_index_info($table, $name); - if ($index_exists) { - if ($self->INDEX_DROPS_REQUIRE_FK_DROPS) { - # We cannot delete an index used by a FK. - foreach my $column (@{$index_exists->{FIELDS}}) { - $self->bz_drop_related_fks($table, $column); - } - } - $self->bz_drop_index_raw($table, $name); - $self->_bz_real_schema->delete_index($table, $name); - $self->_bz_store_real_schema; + if ($index_exists) { + if ($self->INDEX_DROPS_REQUIRE_FK_DROPS) { + + # We cannot delete an index used by a FK. + foreach my $column (@{$index_exists->{FIELDS}}) { + $self->bz_drop_related_fks($table, $column); + } } + $self->bz_drop_index_raw($table, $name); + $self->_bz_real_schema->delete_index($table, $name); + $self->_bz_store_real_schema; + } } # bz_drop_index_raw($table, $name, $silent) @@ -987,7 +1010,7 @@ sub bz_drop_index { # Drops an index from the database # without updating any Schema object. Generally # should only be called by bz_drop_index. -# Used when either: (1) You don't yet have a Schema +# Used when either: (1) You don't yet have a Schema # object but you need to drop an index, for some reason. # (2) You need to drop an index that somehow got into the # database but doesn't exist in Schema. @@ -998,108 +1021,111 @@ sub bz_drop_index { # Returns: nothing # sub bz_drop_index_raw { - my ($self, $table, $name, $silent) = @_; - my @statements = $self->_bz_schema->get_drop_index_ddl( - $table, $name); - print "Removing index '$name' from the $table table...\n" unless $silent; - foreach my $sql (@statements) { - # Because this is a deletion, we don't want to die hard if - # we fail because of some local customization. If something - # is already gone, that's fine with us! - eval { $self->do($sql) } or warn "Failed SQL: [$sql] Error: $@"; - } + my ($self, $table, $name, $silent) = @_; + my @statements = $self->_bz_schema->get_drop_index_ddl($table, $name); + print "Removing index '$name' from the $table table...\n" unless $silent; + foreach my $sql (@statements) { + + # Because this is a deletion, we don't want to die hard if + # we fail because of some local customization. If something + # is already gone, that's fine with us! + eval { $self->do($sql) } or warn "Failed SQL: [$sql] Error: $@"; + } } sub bz_drop_table { - my ($self, $name) = @_; - - my $table_exists = $self->bz_table_info($name); - - if ($table_exists) { - my @statements = $self->_bz_schema->get_drop_table_ddl($name); - print get_text('install_table_drop', { name => $name }) . "\n" - if Bugzilla->usage_mode == USAGE_MODE_CMDLINE; - foreach my $sql (@statements) { - # Because this is a deletion, we don't want to die hard if - # we fail because of some local customization. If something - # is already gone, that's fine with us! - eval { $self->do($sql); } or warn "Failed SQL: [$sql] Error: $@"; - } - $self->_bz_real_schema->delete_table($name); - $self->_bz_store_real_schema; + my ($self, $name) = @_; + + my $table_exists = $self->bz_table_info($name); + + if ($table_exists) { + my @statements = $self->_bz_schema->get_drop_table_ddl($name); + print get_text('install_table_drop', {name => $name}) . "\n" + if Bugzilla->usage_mode == USAGE_MODE_CMDLINE; + foreach my $sql (@statements) { + + # Because this is a deletion, we don't want to die hard if + # we fail because of some local customization. If something + # is already gone, that's fine with us! + eval { $self->do($sql); } or warn "Failed SQL: [$sql] Error: $@"; } + $self->_bz_real_schema->delete_table($name); + $self->_bz_store_real_schema; + } } sub bz_fk_info { - my ($self, $table, $column) = @_; - my $col_info = $self->bz_column_info($table, $column); - return undef if !$col_info; - my $fk = $col_info->{REFERENCES}; - return $fk; + my ($self, $table, $column) = @_; + my $col_info = $self->bz_column_info($table, $column); + return undef if !$col_info; + my $fk = $col_info->{REFERENCES}; + return $fk; } sub bz_rename_column { - my ($self, $table, $old_name, $new_name) = @_; + my ($self, $table, $old_name, $new_name) = @_; - my $old_col_exists = $self->bz_column_info($table, $old_name); + my $old_col_exists = $self->bz_column_info($table, $old_name); - if ($old_col_exists) { - my $already_renamed = $self->bz_column_info($table, $new_name); - ThrowCodeError('db_rename_conflict', - { old => "$table.$old_name", - new => "$table.$new_name" }) if $already_renamed; - my @statements = $self->_bz_real_schema->get_rename_column_ddl( - $table, $old_name, $new_name); + if ($old_col_exists) { + my $already_renamed = $self->bz_column_info($table, $new_name); + ThrowCodeError('db_rename_conflict', + {old => "$table.$old_name", new => "$table.$new_name"}) + if $already_renamed; + my @statements + = $self->_bz_real_schema->get_rename_column_ddl($table, $old_name, $new_name); - print get_text('install_column_rename', - { old => "$table.$old_name", new => "$table.$new_name" }) - . "\n" if Bugzilla->usage_mode == USAGE_MODE_CMDLINE; + print get_text('install_column_rename', + {old => "$table.$old_name", new => "$table.$new_name"}) + . "\n" + if Bugzilla->usage_mode == USAGE_MODE_CMDLINE; - foreach my $sql (@statements) { - $self->do($sql); - } - $self->_bz_real_schema->rename_column($table, $old_name, $new_name); - $self->_bz_store_real_schema; + foreach my $sql (@statements) { + $self->do($sql); } + $self->_bz_real_schema->rename_column($table, $old_name, $new_name); + $self->_bz_store_real_schema; + } } sub bz_rename_table { - my ($self, $old_name, $new_name) = @_; - my $old_table = $self->bz_table_info($old_name); - return if !$old_table; - - my $new = $self->bz_table_info($new_name); - ThrowCodeError('db_rename_conflict', { old => $old_name, - new => $new_name }) if $new; - - # FKs will all have the wrong names unless we drop and then let them - # be re-created later. Under normal circumstances, checksetup.pl will - # automatically re-create these dropped FKs at the end of its DB upgrade - # run, so we don't need to re-create them in this method. - my @columns = $self->bz_table_columns($old_name); - foreach my $column (@columns) { - # these just return silently if there's no FK to drop - $self->bz_drop_fk($old_name, $column); - $self->bz_drop_related_fks($old_name, $column); - } - - my @sql = $self->_bz_real_schema->get_rename_table_sql($old_name, $new_name); - print get_text('install_table_rename', - { old => $old_name, new => $new_name }) . "\n" - if Bugzilla->usage_mode == USAGE_MODE_CMDLINE; - $self->do($_) foreach @sql; - $self->_bz_real_schema->rename_table($old_name, $new_name); - $self->_bz_store_real_schema; + my ($self, $old_name, $new_name) = @_; + my $old_table = $self->bz_table_info($old_name); + return if !$old_table; + + my $new = $self->bz_table_info($new_name); + ThrowCodeError('db_rename_conflict', {old => $old_name, new => $new_name}) + if $new; + + # FKs will all have the wrong names unless we drop and then let them + # be re-created later. Under normal circumstances, checksetup.pl will + # automatically re-create these dropped FKs at the end of its DB upgrade + # run, so we don't need to re-create them in this method. + my @columns = $self->bz_table_columns($old_name); + foreach my $column (@columns) { + + # these just return silently if there's no FK to drop + $self->bz_drop_fk($old_name, $column); + $self->bz_drop_related_fks($old_name, $column); + } + + my @sql = $self->_bz_real_schema->get_rename_table_sql($old_name, $new_name); + print get_text('install_table_rename', {old => $old_name, new => $new_name}) + . "\n" + if Bugzilla->usage_mode == USAGE_MODE_CMDLINE; + $self->do($_) foreach @sql; + $self->_bz_real_schema->rename_table($old_name, $new_name); + $self->_bz_store_real_schema; } sub bz_set_next_serial_value { - my ($self, $table, $column, $value) = @_; - if (!$value) { - $value = $self->selectrow_array("SELECT MAX($column) FROM $table") || 0; - $value++; - } - my @sql = $self->_bz_real_schema->get_set_serial_sql($table, $column, $value); - $self->do($_) foreach @sql; + my ($self, $table, $column, $value) = @_; + if (!$value) { + $value = $self->selectrow_array("SELECT MAX($column) FROM $table") || 0; + $value++; + } + my @sql = $self->_bz_real_schema->get_set_serial_sql($table, $column, $value); + $self->do($_) foreach @sql; } ##################################################################### @@ -1107,12 +1133,12 @@ sub bz_set_next_serial_value { ##################################################################### sub _bz_schema { - my ($self) = @_; - return $self->{private_bz_schema} if exists $self->{private_bz_schema}; - my @module_parts = split('::', ref $self); - my $module_name = pop @module_parts; - $self->{private_bz_schema} = Bugzilla::DB::Schema->new($module_name); - return $self->{private_bz_schema}; + my ($self) = @_; + return $self->{private_bz_schema} if exists $self->{private_bz_schema}; + my @module_parts = split('::', ref $self); + my $module_name = pop @module_parts; + $self->{private_bz_schema} = Bugzilla::DB::Schema->new($module_name); + return $self->{private_bz_schema}; } # _bz_get_initial_schema() @@ -1126,53 +1152,54 @@ sub _bz_schema { # Returns: A Schema object that can be serialized and written to disk # for _bz_init_schema_storage. sub _bz_get_initial_schema { - my ($self) = @_; - return $self->_bz_schema->get_empty_schema(); + my ($self) = @_; + return $self->_bz_schema->get_empty_schema(); } sub bz_column_info { - my ($self, $table, $column) = @_; - my $def = $self->_bz_real_schema->get_column_abstract($table, $column); - # We dclone it so callers can't modify the Schema. - $def = dclone($def) if defined $def; - return $def; + my ($self, $table, $column) = @_; + my $def = $self->_bz_real_schema->get_column_abstract($table, $column); + + # We dclone it so callers can't modify the Schema. + $def = dclone($def) if defined $def; + return $def; } sub bz_index_info { - my ($self, $table, $index) = @_; - my $index_def = - $self->_bz_real_schema->get_index_abstract($table, $index); - if (ref($index_def) eq 'ARRAY') { - $index_def = {FIELDS => $index_def, TYPE => ''}; - } - return $index_def; + my ($self, $table, $index) = @_; + my $index_def = $self->_bz_real_schema->get_index_abstract($table, $index); + if (ref($index_def) eq 'ARRAY') { + $index_def = {FIELDS => $index_def, TYPE => ''}; + } + return $index_def; } sub bz_table_info { - my ($self, $table) = @_; - return $self->_bz_real_schema->get_table_abstract($table); + my ($self, $table) = @_; + return $self->_bz_real_schema->get_table_abstract($table); } sub bz_table_columns { - my ($self, $table) = @_; - return $self->_bz_real_schema->get_table_columns($table); + my ($self, $table) = @_; + return $self->_bz_real_schema->get_table_columns($table); } sub bz_table_indexes { - my ($self, $table) = @_; - my $indexes = $self->_bz_real_schema->get_table_indexes_abstract($table); - my %return_indexes; - # We do this so that they're always hashes. - foreach my $name (keys %$indexes) { - $return_indexes{$name} = $self->bz_index_info($table, $name); - } - return \%return_indexes; + my ($self, $table) = @_; + my $indexes = $self->_bz_real_schema->get_table_indexes_abstract($table); + my %return_indexes; + + # We do this so that they're always hashes. + foreach my $name (keys %$indexes) { + $return_indexes{$name} = $self->bz_index_info($table, $name); + } + return \%return_indexes; } sub bz_table_list { - my ($self) = @_; - return $self->_bz_real_schema->get_table_list(); + my ($self) = @_; + return $self->_bz_real_schema->get_table_list(); } ##################################################################### @@ -1191,9 +1218,9 @@ sub bz_table_list { # Returns: An array of column names. # sub bz_table_columns_real { - my ($self, $table) = @_; - my $sth = $self->column_info(undef, undef, $table, '%'); - return @{ $self->selectcol_arrayref($sth, {Columns => [4]}) }; + my ($self, $table) = @_; + my $sth = $self->column_info(undef, undef, $table, '%'); + return @{$self->selectcol_arrayref($sth, {Columns => [4]})}; } # bz_table_list_real() @@ -1203,9 +1230,9 @@ sub bz_table_columns_real { # Params: none # Returns: An array containing table names. sub bz_table_list_real { - my ($self) = @_; - my $table_sth = $self->table_info(undef, undef, undef, "TABLE"); - return @{$self->selectcol_arrayref($table_sth, { Columns => [3] })}; + my ($self) = @_; + my $table_sth = $self->table_info(undef, undef, undef, "TABLE"); + return @{$self->selectcol_arrayref($table_sth, {Columns => [3]})}; } ##################################################################### @@ -1213,55 +1240,59 @@ sub bz_table_list_real { ##################################################################### sub bz_in_transaction { - return $_[0]->{private_bz_transaction_count} ? 1 : 0; + return $_[0]->{private_bz_transaction_count} ? 1 : 0; } sub bz_start_transaction { - my ($self) = @_; - - if ($self->bz_in_transaction) { - $self->{private_bz_transaction_count}++; - } else { - # Turn AutoCommit off and start a new transaction - $self->begin_work(); - # REPEATABLE READ means "We work on a snapshot of the DB that - # is created when we execute our first SQL statement." It's - # what we need in Bugzilla to be safe, for what we do. - # Different DBs have different defaults for their isolation - # level, so we just set it here manually. - if ($self->ISOLATION_LEVEL) { - $self->do('SET TRANSACTION ISOLATION LEVEL ' - . $self->ISOLATION_LEVEL); - } - $self->{private_bz_transaction_count} = 1; + my ($self) = @_; + + if ($self->bz_in_transaction) { + $self->{private_bz_transaction_count}++; + } + else { + # Turn AutoCommit off and start a new transaction + $self->begin_work(); + + # REPEATABLE READ means "We work on a snapshot of the DB that + # is created when we execute our first SQL statement." It's + # what we need in Bugzilla to be safe, for what we do. + # Different DBs have different defaults for their isolation + # level, so we just set it here manually. + if ($self->ISOLATION_LEVEL) { + $self->do('SET TRANSACTION ISOLATION LEVEL ' . $self->ISOLATION_LEVEL); } + $self->{private_bz_transaction_count} = 1; + } } sub bz_commit_transaction { - my ($self) = @_; - - if ($self->{private_bz_transaction_count} > 1) { - $self->{private_bz_transaction_count}--; - } elsif ($self->bz_in_transaction) { - $self->commit(); - $self->{private_bz_transaction_count} = 0; - Bugzilla::Mailer->send_staged_mail(); - } else { - ThrowCodeError('not_in_transaction'); - } + my ($self) = @_; + + if ($self->{private_bz_transaction_count} > 1) { + $self->{private_bz_transaction_count}--; + } + elsif ($self->bz_in_transaction) { + $self->commit(); + $self->{private_bz_transaction_count} = 0; + Bugzilla::Mailer->send_staged_mail(); + } + else { + ThrowCodeError('not_in_transaction'); + } } sub bz_rollback_transaction { - my ($self) = @_; - - # Unlike start and commit, if we rollback at any point it happens - # instantly, even if we're in a nested transaction. - if (!$self->bz_in_transaction) { - ThrowCodeError("not_in_transaction"); - } else { - $self->rollback(); - $self->{private_bz_transaction_count} = 0; - } + my ($self) = @_; + + # Unlike start and commit, if we rollback at any point it happens + # instantly, even if we're in a nested transaction. + if (!$self->bz_in_transaction) { + ThrowCodeError("not_in_transaction"); + } + else { + $self->rollback(); + $self->{private_bz_transaction_count} = 0; + } } ##################################################################### @@ -1269,42 +1300,43 @@ sub bz_rollback_transaction { ##################################################################### sub db_new { - my ($class, $params) = @_; - my ($dsn, $user, $pass, $override_attrs) = - @$params{qw(dsn user pass attrs)}; - - # set up default attributes used to connect to the database - # (may be overridden by DB driver implementations) - my $attributes = { RaiseError => 0, - AutoCommit => 1, - PrintError => 0, - ShowErrorStatement => 1, - HandleError => \&_handle_error, - TaintIn => 1, - # See https://rt.perl.org/rt3/Public/Bug/Display.html?id=30933 - # for the reason to use NAME instead of NAME_lc (bug 253696). - FetchHashKeyName => 'NAME', - }; - - if ($override_attrs) { - foreach my $key (keys %$override_attrs) { - $attributes->{$key} = $override_attrs->{$key}; - } + my ($class, $params) = @_; + my ($dsn, $user, $pass, $override_attrs) = @$params{qw(dsn user pass attrs)}; + + # set up default attributes used to connect to the database + # (may be overridden by DB driver implementations) + my $attributes = { + RaiseError => 0, + AutoCommit => 1, + PrintError => 0, + ShowErrorStatement => 1, + HandleError => \&_handle_error, + TaintIn => 1, + + # See https://rt.perl.org/rt3/Public/Bug/Display.html?id=30933 + # for the reason to use NAME instead of NAME_lc (bug 253696). + FetchHashKeyName => 'NAME', + }; + + if ($override_attrs) { + foreach my $key (keys %$override_attrs) { + $attributes->{$key} = $override_attrs->{$key}; } + } - # connect using our known info to the specified db - my $self = DBI->connect($dsn, $user, $pass, $attributes) - or die "\nCan't connect to the database.\nError: $DBI::errstr\n" - . " Is your database installed and up and running?\n Do you have" - . " the correct username and password selected in localconfig?\n\n"; + # connect using our known info to the specified db + my $self = DBI->connect($dsn, $user, $pass, $attributes) + or die "\nCan't connect to the database.\nError: $DBI::errstr\n" + . " Is your database installed and up and running?\n Do you have" + . " the correct username and password selected in localconfig?\n\n"; - # RaiseError was only set to 0 so that we could catch the - # above "die" condition. - $self->{RaiseError} = 1; + # RaiseError was only set to 0 so that we could catch the + # above "die" condition. + $self->{RaiseError} = 1; - bless ($self, $class); + bless($self, $class); - return $self; + return $self; } ##################################################################### @@ -1328,55 +1360,54 @@ These methods really are private. Do not override them in subclasses. =cut sub _bz_init_schema_storage { - my ($self) = @_; - - my $table_size; - eval { - $table_size = - $self->selectrow_array("SELECT COUNT(*) FROM bz_schema"); - }; + my ($self) = @_; + + my $table_size; + eval { $table_size = $self->selectrow_array("SELECT COUNT(*) FROM bz_schema"); }; + + if (!$table_size) { + my $init_schema = $self->_bz_get_initial_schema; + my $store_me = $init_schema->serialize_abstract(); + my $schema_version = $init_schema->SCHEMA_VERSION; + + # If table_size is not defined, then we hit an error reading the + # bz_schema table, which means it probably doesn't exist yet. So, + # we have to create it. If we failed above for some other reason, + # we'll see the failure here. + # However, we must create the table after we do get_initial_schema, + # because some versions of get_initial_schema read that the table + # exists and then add it to the Schema, where other versions don't. + if (!defined $table_size) { + $self->_bz_add_table_raw('bz_schema'); + } - if (!$table_size) { - my $init_schema = $self->_bz_get_initial_schema; - my $store_me = $init_schema->serialize_abstract(); - my $schema_version = $init_schema->SCHEMA_VERSION; - - # If table_size is not defined, then we hit an error reading the - # bz_schema table, which means it probably doesn't exist yet. So, - # we have to create it. If we failed above for some other reason, - # we'll see the failure here. - # However, we must create the table after we do get_initial_schema, - # because some versions of get_initial_schema read that the table - # exists and then add it to the Schema, where other versions don't. - if (!defined $table_size) { - $self->_bz_add_table_raw('bz_schema'); - } + say install_string('db_schema_init'); + my $sth = $self->prepare( + "INSERT INTO bz_schema " . " (schema_data, version) VALUES (?,?)"); + $sth->bind_param(1, $store_me, $self->BLOB_TYPE); + $sth->bind_param(2, $schema_version); + $sth->execute(); - say install_string('db_schema_init'); - my $sth = $self->prepare("INSERT INTO bz_schema " - ." (schema_data, version) VALUES (?,?)"); - $sth->bind_param(1, $store_me, $self->BLOB_TYPE); - $sth->bind_param(2, $schema_version); - $sth->execute(); - - # And now we have to update the on-disk schema to hold the bz_schema - # table, if the bz_schema table didn't exist when we were called. - if (!defined $table_size) { - $self->_bz_real_schema->add_table('bz_schema', - $self->_bz_schema->get_table_abstract('bz_schema')); - $self->_bz_store_real_schema; - } - } - # Sanity check - elsif ($table_size > 1) { - # We tell them to delete the newer one. Better to have checksetup - # run migration code too many times than to have it not run the - # correct migration code at all. - die "Attempted to initialize the schema but there are already " - . " $table_size copies of it stored.\nThis should never happen.\n" - . " Compare the rows of the bz_schema table and delete the " - . "newer one(s)."; + # And now we have to update the on-disk schema to hold the bz_schema + # table, if the bz_schema table didn't exist when we were called. + if (!defined $table_size) { + $self->_bz_real_schema->add_table('bz_schema', + $self->_bz_schema->get_table_abstract('bz_schema')); + $self->_bz_store_real_schema; } + } + + # Sanity check + elsif ($table_size > 1) { + + # We tell them to delete the newer one. Better to have checksetup + # run migration code too many times than to have it not run the + # correct migration code at all. + die "Attempted to initialize the schema but there are already " + . " $table_size copies of it stored.\nThis should never happen.\n" + . " Compare the rows of the bz_schema table and delete the " + . "newer one(s)."; + } } =item C<_bz_real_schema()> @@ -1390,24 +1421,23 @@ sub _bz_init_schema_storage { =cut sub _bz_real_schema { - my ($self) = @_; - return $self->{private_real_schema} if exists $self->{private_real_schema}; - - my $bz_schema; - unless ($bz_schema = Bugzilla->memcached->get({ key => 'bz_schema' })) { - $bz_schema = $self->selectrow_arrayref( - "SELECT schema_data, version FROM bz_schema" - ); - Bugzilla->memcached->set({ key => 'bz_schema', value => $bz_schema }); - } + my ($self) = @_; + return $self->{private_real_schema} if exists $self->{private_real_schema}; - (die "_bz_real_schema tried to read the bz_schema table but it's empty!") - if !$bz_schema; + my $bz_schema; + unless ($bz_schema = Bugzilla->memcached->get({key => 'bz_schema'})) { + $bz_schema + = $self->selectrow_arrayref("SELECT schema_data, version FROM bz_schema"); + Bugzilla->memcached->set({key => 'bz_schema', value => $bz_schema}); + } - $self->{private_real_schema} = - $self->_bz_schema->deserialize_abstract($bz_schema->[0], $bz_schema->[1]); + (die "_bz_real_schema tried to read the bz_schema table but it's empty!") + if !$bz_schema; - return $self->{private_real_schema}; + $self->{private_real_schema} + = $self->_bz_schema->deserialize_abstract($bz_schema->[0], $bz_schema->[1]); + + return $self->{private_real_schema}; } =item C<_bz_store_real_schema()> @@ -1427,106 +1457,135 @@ sub _bz_real_schema { =cut sub _bz_store_real_schema { - my ($self) = @_; - - # Make sure that there's a schema to update - my $table_size = $self->selectrow_array("SELECT COUNT(*) FROM bz_schema"); - - die "Attempted to update the bz_schema table but there's nothing " - . "there to update. Run checksetup." unless $table_size; - - # We want to store the current object, not one - # that we read from the database. So we use the actual hash - # member instead of the subroutine call. If the hash - # member is not defined, we will (and should) fail. - my $update_schema = $self->{private_real_schema}; - my $store_me = $update_schema->serialize_abstract(); - my $schema_version = $update_schema->SCHEMA_VERSION; - my $sth = $self->prepare("UPDATE bz_schema - SET schema_data = ?, version = ?"); - $sth->bind_param(1, $store_me, $self->BLOB_TYPE); - $sth->bind_param(2, $schema_version); - $sth->execute(); + my ($self) = @_; + + # Make sure that there's a schema to update + my $table_size = $self->selectrow_array("SELECT COUNT(*) FROM bz_schema"); - Bugzilla->memcached->clear({ key => 'bz_schema' }); + die "Attempted to update the bz_schema table but there's nothing " + . "there to update. Run checksetup." + unless $table_size; + + # We want to store the current object, not one + # that we read from the database. So we use the actual hash + # member instead of the subroutine call. If the hash + # member is not defined, we will (and should) fail. + my $update_schema = $self->{private_real_schema}; + my $store_me = $update_schema->serialize_abstract(); + my $schema_version = $update_schema->SCHEMA_VERSION; + my $sth = $self->prepare( + "UPDATE bz_schema + SET schema_data = ?, version = ?" + ); + $sth->bind_param(1, $store_me, $self->BLOB_TYPE); + $sth->bind_param(2, $schema_version); + $sth->execute(); + + Bugzilla->memcached->clear({key => 'bz_schema'}); } # For bz_populate_enum_tables sub _bz_populate_enum_table { - my ($self, $table, $valuelist) = @_; - - my $sql_table = $self->quote_identifier($table); - - # Check if there are any table entries - my $table_size = $self->selectrow_array("SELECT COUNT(*) FROM $sql_table"); - - # If the table is empty... - if (!$table_size) { - print " $table"; - my $insert = $self->prepare( - "INSERT INTO $sql_table (value,sortkey) VALUES (?,?)"); - my $sortorder = 0; - my $maxlen = max(map(length($_), @$valuelist)) + 2; - foreach my $value (@$valuelist) { - $sortorder += 100; - $insert->execute($value, $sortorder); - } + my ($self, $table, $valuelist) = @_; + + my $sql_table = $self->quote_identifier($table); + + # Check if there are any table entries + my $table_size = $self->selectrow_array("SELECT COUNT(*) FROM $sql_table"); + + # If the table is empty... + if (!$table_size) { + print " $table"; + my $insert + = $self->prepare("INSERT INTO $sql_table (value,sortkey) VALUES (?,?)"); + my $sortorder = 0; + my $maxlen = max(map(length($_), @$valuelist)) + 2; + foreach my $value (@$valuelist) { + $sortorder += 100; + $insert->execute($value, $sortorder); } + } } # This is used before adding a foreign key to a column, to make sure # that the database won't fail adding the key. sub _check_references { - my ($self, $table, $column, $fk) = @_; - my $foreign_table = $fk->{TABLE}; - my $foreign_column = $fk->{COLUMN}; - - # We use table aliases because sometimes we join a table to itself, - # and we can't use the same table name on both sides of the join. - # We also can't use the words "table" or "foreign" because those are - # reserved words. - my $bad_values = $self->selectcol_arrayref( - "SELECT DISTINCT tabl.$column + my ($self, $table, $column, $fk) = @_; + my $foreign_table = $fk->{TABLE}; + my $foreign_column = $fk->{COLUMN}; + + # We use table aliases because sometimes we join a table to itself, + # and we can't use the same table name on both sides of the join. + # We also can't use the words "table" or "foreign" because those are + # reserved words. + my $bad_values = $self->selectcol_arrayref( + "SELECT DISTINCT tabl.$column FROM $table AS tabl LEFT JOIN $foreign_table AS forn ON tabl.$column = forn.$foreign_column WHERE forn.$foreign_column IS NULL - AND tabl.$column IS NOT NULL"); - - if (@$bad_values) { - my $delete_action = $fk->{DELETE} || ''; - if ($delete_action eq 'CASCADE') { - $self->do("DELETE FROM $table WHERE $column IN (" - . join(',', ('?') x @$bad_values) . ")", - undef, @$bad_values); - if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) { - print "\n", get_text('install_fk_invalid_fixed', - { table => $table, column => $column, - foreign_table => $foreign_table, - foreign_column => $foreign_column, - 'values' => $bad_values, action => 'delete' }), "\n"; - } - } - elsif ($delete_action eq 'SET NULL') { - $self->do("UPDATE $table SET $column = NULL + AND tabl.$column IS NOT NULL" + ); + + if (@$bad_values) { + my $delete_action = $fk->{DELETE} || ''; + if ($delete_action eq 'CASCADE') { + $self->do( + "DELETE FROM $table WHERE $column IN (" . join(',', ('?') x @$bad_values) . ")", + undef, @$bad_values + ); + if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) { + print "\n", + get_text( + 'install_fk_invalid_fixed', + { + table => $table, + column => $column, + foreign_table => $foreign_table, + foreign_column => $foreign_column, + 'values' => $bad_values, + action => 'delete' + } + ), + "\n"; + } + } + elsif ($delete_action eq 'SET NULL') { + $self->do( + "UPDATE $table SET $column = NULL WHERE $column IN (" - . join(',', ('?') x @$bad_values) . ")", - undef, @$bad_values); - if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) { - print "\n", get_text('install_fk_invalid_fixed', - { table => $table, column => $column, - foreign_table => $foreign_table, - foreign_column => $foreign_column, - 'values' => $bad_values, action => 'null' }), "\n"; - } - } - else { - die "\n", get_text('install_fk_invalid', - { table => $table, column => $column, - foreign_table => $foreign_table, - foreign_column => $foreign_column, - 'values' => $bad_values }), "\n"; + . join(',', ('?') x @$bad_values) . ")", undef, @$bad_values + ); + if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) { + print "\n", + get_text( + 'install_fk_invalid_fixed', + { + table => $table, + column => $column, + foreign_table => $foreign_table, + foreign_column => $foreign_column, + 'values' => $bad_values, + action => 'null' + } + ), + "\n"; + } + } + else { + die "\n", + get_text( + 'install_fk_invalid', + { + table => $table, + column => $column, + foreign_table => $foreign_table, + foreign_column => $foreign_column, + 'values' => $bad_values } + ), + "\n"; } + } } 1; diff --git a/Bugzilla/DB/Mysql.pm b/Bugzilla/DB/Mysql.pm index d0915f1e6..96a1fcb21 100644 --- a/Bugzilla/DB/Mysql.pm +++ b/Bugzilla/DB/Mysql.pm @@ -37,258 +37,265 @@ use List::Util qw(max); use Text::ParseWords; # This is how many comments of MAX_COMMENT_LENGTH we expect on a single bug. -# In reality, you could have a LOT more comments than this, because +# In reality, you could have a LOT more comments than this, because # MAX_COMMENT_LENGTH is big. use constant MAX_COMMENTS => 50; use constant FULLTEXT_OR => '|'; sub new { - my ($class, $params) = @_; - my ($user, $pass, $host, $dbname, $port, $sock) = - @$params{qw(db_user db_pass db_host db_name db_port db_sock)}; - - # construct the DSN from the parameters we got - my $dsn = "dbi:mysql:host=$host;database=$dbname"; - $dsn .= ";port=$port" if $port; - $dsn .= ";mysql_socket=$sock" if $sock; - - my %attrs = ( - mysql_enable_utf8 => Bugzilla->params->{'utf8'}, - # Needs to be explicitly specified for command-line processes. - mysql_auto_reconnect => 1, - ); - - # MySQL SSL options - my ($ssl_ca_file, $ssl_ca_path, $ssl_cert, $ssl_key) = - @$params{qw(db_mysql_ssl_ca_file db_mysql_ssl_ca_path - db_mysql_ssl_client_cert db_mysql_ssl_client_key)}; - if ($ssl_ca_file || $ssl_ca_path || $ssl_cert || $ssl_key) { - $attrs{'mysql_ssl'} = 1; - $attrs{'mysql_ssl_ca_file'} = $ssl_ca_file if $ssl_ca_file; - $attrs{'mysql_ssl_ca_path'} = $ssl_ca_path if $ssl_ca_path; - $attrs{'mysql_ssl_client_cert'} = $ssl_cert if $ssl_cert; - $attrs{'mysql_ssl_client_key'} = $ssl_key if $ssl_key; + my ($class, $params) = @_; + my ($user, $pass, $host, $dbname, $port, $sock) + = @$params{qw(db_user db_pass db_host db_name db_port db_sock)}; + + # construct the DSN from the parameters we got + my $dsn = "dbi:mysql:host=$host;database=$dbname"; + $dsn .= ";port=$port" if $port; + $dsn .= ";mysql_socket=$sock" if $sock; + + my %attrs = ( + mysql_enable_utf8 => Bugzilla->params->{'utf8'}, + + # Needs to be explicitly specified for command-line processes. + mysql_auto_reconnect => 1, + ); + + # MySQL SSL options + my ($ssl_ca_file, $ssl_ca_path, $ssl_cert, $ssl_key) = @$params{ + qw(db_mysql_ssl_ca_file db_mysql_ssl_ca_path + db_mysql_ssl_client_cert db_mysql_ssl_client_key) + }; + if ($ssl_ca_file || $ssl_ca_path || $ssl_cert || $ssl_key) { + $attrs{'mysql_ssl'} = 1; + $attrs{'mysql_ssl_ca_file'} = $ssl_ca_file if $ssl_ca_file; + $attrs{'mysql_ssl_ca_path'} = $ssl_ca_path if $ssl_ca_path; + $attrs{'mysql_ssl_client_cert'} = $ssl_cert if $ssl_cert; + $attrs{'mysql_ssl_client_key'} = $ssl_key if $ssl_key; + } + + my $self = $class->db_new( + {dsn => $dsn, user => $user, pass => $pass, attrs => \%attrs}); + + # This makes sure that if the tables are encoded as UTF-8, we + # return their data correctly. + $self->do("SET NAMES utf8") if Bugzilla->params->{'utf8'}; + + # all class local variables stored in DBI derived class needs to have + # a prefix 'private_'. See DBI documentation. + $self->{private_bz_tables_locked} = ""; + + # Needed by TheSchwartz + $self->{private_bz_dsn} = $dsn; + + bless($self, $class); + + # 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 { $_ !~ /^(?:ANSI|STRICT_(?:TRANS|ALL)_TABLES|TRADITIONAL)$/ } + split(/,/, $sql_mode)); + + if ($sql_mode ne $new_sql_mode) { + $self->do("SET SESSION sql_mode = ?", undef, $new_sql_mode); } + } - my $self = $class->db_new({ dsn => $dsn, user => $user, - pass => $pass, attrs => \%attrs }); - - # This makes sure that if the tables are encoded as UTF-8, we - # return their data correctly. - $self->do("SET NAMES utf8") if Bugzilla->params->{'utf8'}; - - # all class local variables stored in DBI derived class needs to have - # a prefix 'private_'. See DBI documentation. - $self->{private_bz_tables_locked} = ""; - - # Needed by TheSchwartz - $self->{private_bz_dsn} = $dsn; + # Allow large GROUP_CONCATs (largely for inserting comments + # into bugs_fulltext). + $self->do('SET SESSION group_concat_max_len = 128000000'); - bless ($self, $class); + # 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'); - # 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 {$_ !~ /^(?:ANSI|STRICT_(?:TRANS|ALL)_TABLES|TRADITIONAL)$/} - split(/,/, $sql_mode)); - - if ($sql_mode ne $new_sql_mode) { - $self->do("SET SESSION sql_mode = ?", undef, $new_sql_mode); - } - } - - # Allow large GROUP_CONCATs (largely for inserting comments - # 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; + return $self; } # when last_insert_id() is supported on MySQL by lowest DBI/DBD version # required by Bugzilla, this implementation can be removed. sub bz_last_key { - my ($self) = @_; + my ($self) = @_; - my ($last_insert_id) = $self->selectrow_array('SELECT LAST_INSERT_ID()'); + my ($last_insert_id) = $self->selectrow_array('SELECT LAST_INSERT_ID()'); - return $last_insert_id; + return $last_insert_id; } sub sql_group_concat { - my ($self, $column, $separator, $sort, $order_by) = @_; - $separator = $self->quote(', ') if !defined $separator; - $sort = 1 if !defined $sort; - if ($order_by) { - $column .= " ORDER BY $order_by"; - } - elsif ($sort) { - my $sort_order = $column; - $sort_order =~ s/^DISTINCT\s+//i; - $column = "$column ORDER BY $sort_order"; - } - return "GROUP_CONCAT($column SEPARATOR $separator)"; + my ($self, $column, $separator, $sort, $order_by) = @_; + $separator = $self->quote(', ') if !defined $separator; + $sort = 1 if !defined $sort; + if ($order_by) { + $column .= " ORDER BY $order_by"; + } + elsif ($sort) { + my $sort_order = $column; + $sort_order =~ s/^DISTINCT\s+//i; + $column = "$column ORDER BY $sort_order"; + } + return "GROUP_CONCAT($column SEPARATOR $separator)"; } sub sql_regexp { - my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_; - $real_pattern ||= $pattern; + my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_; + $real_pattern ||= $pattern; - $self->bz_check_regexp($real_pattern) if !$nocheck; + $self->bz_check_regexp($real_pattern) if !$nocheck; - return "$expr REGEXP $pattern"; + return "$expr REGEXP $pattern"; } sub sql_not_regexp { - my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_; - $real_pattern ||= $pattern; + my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_; + $real_pattern ||= $pattern; - $self->bz_check_regexp($real_pattern) if !$nocheck; + $self->bz_check_regexp($real_pattern) if !$nocheck; - return "$expr NOT REGEXP $pattern"; + return "$expr NOT REGEXP $pattern"; } sub sql_limit { - my ($self, $limit, $offset) = @_; - - if (defined($offset)) { - return "LIMIT $offset, $limit"; - } else { - return "LIMIT $limit"; - } + my ($self, $limit, $offset) = @_; + + if (defined($offset)) { + return "LIMIT $offset, $limit"; + } + else { + return "LIMIT $limit"; + } } sub sql_string_concat { - my ($self, @params) = @_; - - return 'CONCAT(' . join(', ', @params) . ')'; + my ($self, @params) = @_; + + return 'CONCAT(' . join(', ', @params) . ')'; } sub sql_fulltext_search { - my ($self, $column, $text) = @_; - - # Add the boolean mode modifier if the search string contains - # boolean operators at the start or end of a word. - my $mode = ''; - if ($text =~ /(?:^|\W)[+\-<>~"()]/ || $text =~ /[()"*](?:$|\W)/) { - $mode = 'IN BOOLEAN MODE'; - - my @terms = split(quotemeta(FULLTEXT_OR), $text); - foreach my $term (@terms) { - # quote un-quoted compound words - my @words = quotewords('[\s()]+', 'delimiters', $term); - foreach my $word (@words) { - # match words that have non-word chars in the middle of them - if ($word =~ /\w\W+\w/ && $word !~ m/"/) { - $word = '"' . $word . '"'; - } - } - $term = join('', @words); + my ($self, $column, $text) = @_; + + # Add the boolean mode modifier if the search string contains + # boolean operators at the start or end of a word. + my $mode = ''; + if ($text =~ /(?:^|\W)[+\-<>~"()]/ || $text =~ /[()"*](?:$|\W)/) { + $mode = 'IN BOOLEAN MODE'; + + my @terms = split(quotemeta(FULLTEXT_OR), $text); + foreach my $term (@terms) { + + # quote un-quoted compound words + my @words = quotewords('[\s()]+', 'delimiters', $term); + foreach my $word (@words) { + + # match words that have non-word chars in the middle of them + if ($word =~ /\w\W+\w/ && $word !~ m/"/) { + $word = '"' . $word . '"'; } - $text = join(FULLTEXT_OR, @terms); + } + $term = join('', @words); } + $text = join(FULLTEXT_OR, @terms); + } - # quote the text for use in the MATCH AGAINST expression - $text = $self->quote($text); + # quote the text for use in the MATCH AGAINST expression + $text = $self->quote($text); - # untaint the text, since it's safe to use now that we've quoted it - trick_taint($text); + # untaint the text, since it's safe to use now that we've quoted it + trick_taint($text); - return "MATCH($column) AGAINST($text $mode)"; + return "MATCH($column) AGAINST($text $mode)"; } sub sql_istring { - my ($self, $string) = @_; - - return $string; + my ($self, $string) = @_; + + return $string; } sub sql_from_days { - my ($self, $days) = @_; + my ($self, $days) = @_; - return "FROM_DAYS($days)"; + return "FROM_DAYS($days)"; } sub sql_to_days { - my ($self, $date) = @_; + my ($self, $date) = @_; - return "TO_DAYS($date)"; + return "TO_DAYS($date)"; } sub sql_date_format { - my ($self, $date, $format) = @_; + my ($self, $date, $format) = @_; + + $format = "%Y.%m.%d %H:%i:%s" if !$format; - $format = "%Y.%m.%d %H:%i:%s" if !$format; - - return "DATE_FORMAT($date, " . $self->quote($format) . ")"; + return "DATE_FORMAT($date, " . $self->quote($format) . ")"; } sub sql_date_math { - my ($self, $date, $operator, $interval, $units) = @_; - - return "$date $operator INTERVAL $interval $units"; + my ($self, $date, $operator, $interval, $units) = @_; + + return "$date $operator INTERVAL $interval $units"; } sub sql_iposition { - my ($self, $fragment, $text) = @_; - return "INSTR($text, $fragment)"; + my ($self, $fragment, $text) = @_; + return "INSTR($text, $fragment)"; } sub sql_position { - my ($self, $fragment, $text) = @_; + my ($self, $fragment, $text) = @_; - return "INSTR(CAST($text AS BINARY), CAST($fragment AS BINARY))"; + return "INSTR(CAST($text AS BINARY), CAST($fragment AS BINARY))"; } sub sql_group_by { - my ($self, $needed_columns, $optional_columns) = @_; + my ($self, $needed_columns, $optional_columns) = @_; - # MySQL allows you to specify the minimal subset of columns to get - # a unique result. While it does allow specifying all columns as - # ANSI SQL requires, according to MySQL documentation, the fewer - # columns you specify, the faster the query runs. - return "GROUP BY $needed_columns"; + # MySQL allows you to specify the minimal subset of columns to get + # a unique result. While it does allow specifying all columns as + # ANSI SQL requires, according to MySQL documentation, the fewer + # columns you specify, the faster the query runs. + return "GROUP BY $needed_columns"; } sub bz_explain { - my ($self, $sql) = @_; - my $sth = $self->prepare("EXPLAIN $sql"); - $sth->execute(); - my $columns = $sth->{'NAME'}; - my $lengths = $sth->{'mysql_max_length'}; - my $format_string = '|'; - my $i = 0; - foreach my $column (@$columns) { - # Sometimes the column name is longer than the contents. - my $length = max($lengths->[$i], length($column)); - $format_string .= ' %-' . $length . 's |'; - $i++; - } - - my $first_row = sprintf($format_string, @$columns); - my @explain_rows = ($first_row, '-' x length($first_row)); - while (my $row = $sth->fetchrow_arrayref) { - my @fixed = map { defined $_ ? $_ : 'NULL' } @$row; - push(@explain_rows, sprintf($format_string, @fixed)); - } - - return join("\n", @explain_rows); + my ($self, $sql) = @_; + my $sth = $self->prepare("EXPLAIN $sql"); + $sth->execute(); + my $columns = $sth->{'NAME'}; + my $lengths = $sth->{'mysql_max_length'}; + my $format_string = '|'; + my $i = 0; + foreach my $column (@$columns) { + + # Sometimes the column name is longer than the contents. + my $length = max($lengths->[$i], length($column)); + $format_string .= ' %-' . $length . 's |'; + $i++; + } + + my $first_row = sprintf($format_string, @$columns); + my @explain_rows = ($first_row, '-' x length($first_row)); + while (my $row = $sth->fetchrow_arrayref) { + my @fixed = map { defined $_ ? $_ : 'NULL' } @$row; + push(@explain_rows, sprintf($format_string, @fixed)); + } + + return join("\n", @explain_rows); } sub _bz_get_initial_schema { - my ($self) = @_; - return $self->_bz_build_schema_from_disk(); + my ($self) = @_; + return $self->_bz_build_schema_from_disk(); } ##################################################################### @@ -296,493 +303,503 @@ sub _bz_get_initial_schema { ##################################################################### sub bz_check_server_version { - my $self = shift; + my $self = shift; - my $lc = Bugzilla->localconfig; - if (lc(Bugzilla->localconfig->{db_name}) eq 'mysql') { - die "It is not safe to run Bugzilla inside a database named 'mysql'.\n" - . " Please pick a different value for \$db_name in localconfig.\n"; - } + my $lc = Bugzilla->localconfig; + if (lc(Bugzilla->localconfig->{db_name}) eq 'mysql') { + die "It is not safe to run Bugzilla inside a database named 'mysql'.\n" + . " Please pick a different value for \$db_name in localconfig.\n"; + } - $self->SUPER::bz_check_server_version(@_); + $self->SUPER::bz_check_server_version(@_); } sub bz_setup_database { - my ($self) = @_; - - # The "comments" field of the bugs_fulltext table could easily exceed - # MySQL's default max_allowed_packet. Also, MySQL should never have - # a max_allowed_packet smaller than our max_attachment_size. So, we - # warn the user here if max_allowed_packet is too small. - my $min_max_allowed = MAX_COMMENTS * MAX_COMMENT_LENGTH; - my (undef, $current_max_allowed) = $self->selectrow_array( - q{SHOW VARIABLES LIKE 'max\_allowed\_packet'}); - # This parameter is not yet defined when the DB is being built for - # the very first time. The code below still works properly, however, - # because the default maxattachmentsize is smaller than $min_max_allowed. - my $max_attachment = (Bugzilla->params->{'maxattachmentsize'} || 0) * 1024; - my $needed_max_allowed = max($min_max_allowed, $max_attachment); - if ($current_max_allowed < $needed_max_allowed) { - warn install_string('max_allowed_packet', - { current => $current_max_allowed, - needed => $needed_max_allowed }) . "\n"; + my ($self) = @_; + + # The "comments" field of the bugs_fulltext table could easily exceed + # MySQL's default max_allowed_packet. Also, MySQL should never have + # a max_allowed_packet smaller than our max_attachment_size. So, we + # warn the user here if max_allowed_packet is too small. + my $min_max_allowed = MAX_COMMENTS * MAX_COMMENT_LENGTH; + my (undef, $current_max_allowed) + = $self->selectrow_array(q{SHOW VARIABLES LIKE 'max\_allowed\_packet'}); + + # This parameter is not yet defined when the DB is being built for + # the very first time. The code below still works properly, however, + # because the default maxattachmentsize is smaller than $min_max_allowed. + my $max_attachment = (Bugzilla->params->{'maxattachmentsize'} || 0) * 1024; + my $needed_max_allowed = max($min_max_allowed, $max_attachment); + if ($current_max_allowed < $needed_max_allowed) { + warn install_string('max_allowed_packet', + {current => $current_max_allowed, needed => $needed_max_allowed}) + . "\n"; + } + + # Make sure the installation has InnoDB turned on, or we're going to be + # doing silly things like making foreign keys on MyISAM tables, which is + # hard to fix later. We do this up here because none of the code below + # works if InnoDB is off. (Particularly if we've already converted the + # tables to InnoDB.) + my %engines = @{$self->selectcol_arrayref('SHOW ENGINES', {Columns => [1, 2]})}; + if (!$engines{InnoDB} || $engines{InnoDB} !~ /^(YES|DEFAULT)$/) { + die install_string('mysql_innodb_disabled'); + } + + + my ($sd_index_deleted, $longdescs_index_deleted); + my @tables = $self->bz_table_list_real(); + + # We want to convert tables to InnoDB, but it's possible that they have + # fulltext indexes on them, and conversion will fail unless we remove + # the indexes. + if (grep($_ eq 'bugs', @tables) and !grep($_ eq 'bugs_fulltext', @tables)) { + if ($self->bz_index_info_real('bugs', 'short_desc')) { + $self->bz_drop_index_raw('bugs', 'short_desc'); } - - # Make sure the installation has InnoDB turned on, or we're going to be - # doing silly things like making foreign keys on MyISAM tables, which is - # hard to fix later. We do this up here because none of the code below - # works if InnoDB is off. (Particularly if we've already converted the - # tables to InnoDB.) - my %engines = @{$self->selectcol_arrayref('SHOW ENGINES', {Columns => [1,2]})}; - if (!$engines{InnoDB} || $engines{InnoDB} !~ /^(YES|DEFAULT)$/) { - die install_string('mysql_innodb_disabled'); + if ($self->bz_index_info_real('bugs', 'bugs_short_desc_idx')) { + $self->bz_drop_index_raw('bugs', 'bugs_short_desc_idx'); + $sd_index_deleted = 1; # Used for later schema cleanup. } - - - my ($sd_index_deleted, $longdescs_index_deleted); - my @tables = $self->bz_table_list_real(); - # We want to convert tables to InnoDB, but it's possible that they have - # fulltext indexes on them, and conversion will fail unless we remove - # the indexes. - if (grep($_ eq 'bugs', @tables) - and !grep($_ eq 'bugs_fulltext', @tables)) - { - if ($self->bz_index_info_real('bugs', 'short_desc')) { - $self->bz_drop_index_raw('bugs', 'short_desc'); - } - if ($self->bz_index_info_real('bugs', 'bugs_short_desc_idx')) { - $self->bz_drop_index_raw('bugs', 'bugs_short_desc_idx'); - $sd_index_deleted = 1; # Used for later schema cleanup. - } + } + if (grep($_ eq 'longdescs', @tables) and !grep($_ eq 'bugs_fulltext', @tables)) + { + if ($self->bz_index_info_real('longdescs', 'thetext')) { + $self->bz_drop_index_raw('longdescs', 'thetext'); } - if (grep($_ eq 'longdescs', @tables) - and !grep($_ eq 'bugs_fulltext', @tables)) - { - if ($self->bz_index_info_real('longdescs', 'thetext')) { - $self->bz_drop_index_raw('longdescs', 'thetext'); - } - if ($self->bz_index_info_real('longdescs', 'longdescs_thetext_idx')) { - $self->bz_drop_index_raw('longdescs', 'longdescs_thetext_idx'); - $longdescs_index_deleted = 1; # For later schema cleanup. - } + if ($self->bz_index_info_real('longdescs', 'longdescs_thetext_idx')) { + $self->bz_drop_index_raw('longdescs', 'longdescs_thetext_idx'); + $longdescs_index_deleted = 1; # For later schema cleanup. } - - # Upgrade tables from MyISAM to InnoDB - my $db_name = Bugzilla->localconfig->{db_name}; - my $myisam_tables = $self->selectcol_arrayref( - 'SELECT TABLE_NAME FROM information_schema.TABLES - WHERE TABLE_SCHEMA = ? AND ENGINE = ?', - undef, $db_name, 'MyISAM'); - foreach my $should_be_myisam (Bugzilla::DB::Schema::Mysql::MYISAM_TABLES) { - @$myisam_tables = grep { $_ ne $should_be_myisam } @$myisam_tables; + } + + # Upgrade tables from MyISAM to InnoDB + my $db_name = Bugzilla->localconfig->{db_name}; + my $myisam_tables = $self->selectcol_arrayref( + 'SELECT TABLE_NAME FROM information_schema.TABLES + WHERE TABLE_SCHEMA = ? AND ENGINE = ?', undef, $db_name, 'MyISAM' + ); + foreach my $should_be_myisam (Bugzilla::DB::Schema::Mysql::MYISAM_TABLES) { + @$myisam_tables = grep { $_ ne $should_be_myisam } @$myisam_tables; + } + + if (scalar @$myisam_tables) { + print "Bugzilla now uses the InnoDB storage engine in MySQL for", + " most tables.\nConverting tables to InnoDB:\n"; + foreach my $table (@$myisam_tables) { + print "Converting table $table... "; + $self->do("ALTER TABLE $table ENGINE = InnoDB"); + print "done.\n"; } - - if (scalar @$myisam_tables) { - print "Bugzilla now uses the InnoDB storage engine in MySQL for", - " most tables.\nConverting tables to InnoDB:\n"; - foreach my $table (@$myisam_tables) { - print "Converting table $table... "; - $self->do("ALTER TABLE $table ENGINE = InnoDB"); - print "done.\n"; - } + } + + # Versions of Bugzilla before the existence of Bugzilla::DB::Schema did + # not provide explicit names for the table indexes. This means + # that our upgrades will not be reliable, because we look for the name + # of the index, not what fields it is on, when doing upgrades. + # (using the name is much better for cross-database compatibility + # and general reliability). It's also very important that our + # Schema object be consistent with what is on the disk. + # + # While we're at it, we also fix some inconsistent index naming + # from the original checkin of Bugzilla::DB::Schema. + + # We check for the existence of a particular "short name" index that + # has existed at least since Bugzilla 2.8, and probably earlier. + # For fixing the inconsistent naming of Schema indexes, + # we also check for one of those inconsistently-named indexes. + if ( + grep($_ eq 'bugs', @tables) + && ( $self->bz_index_info_real('bugs', 'assigned_to') + || $self->bz_index_info_real('flags', 'flags_bidattid_idx')) + ) + { + + # This is a check unrelated to the indexes, to see if people are + # upgrading from 2.18 or below, but somehow have a bz_schema table + # already. This only happens if they have done a mysqldump into + # a database without doing a DROP DATABASE first. + # We just do the check here since this check is a reliable way + # of telling that we are upgrading from a version pre-2.20. + if (grep($_ eq 'bz_schema', $self->bz_table_list_real())) { + die install_string('bz_schema_exists_before_220'); } - - # Versions of Bugzilla before the existence of Bugzilla::DB::Schema did - # not provide explicit names for the table indexes. This means - # that our upgrades will not be reliable, because we look for the name - # of the index, not what fields it is on, when doing upgrades. - # (using the name is much better for cross-database compatibility - # and general reliability). It's also very important that our - # Schema object be consistent with what is on the disk. - # - # While we're at it, we also fix some inconsistent index naming - # from the original checkin of Bugzilla::DB::Schema. - - # We check for the existence of a particular "short name" index that - # has existed at least since Bugzilla 2.8, and probably earlier. - # For fixing the inconsistent naming of Schema indexes, - # we also check for one of those inconsistently-named indexes. - if (grep($_ eq 'bugs', @tables) - && ($self->bz_index_info_real('bugs', 'assigned_to') - || $self->bz_index_info_real('flags', 'flags_bidattid_idx')) ) - { - # This is a check unrelated to the indexes, to see if people are - # upgrading from 2.18 or below, but somehow have a bz_schema table - # already. This only happens if they have done a mysqldump into - # a database without doing a DROP DATABASE first. - # We just do the check here since this check is a reliable way - # of telling that we are upgrading from a version pre-2.20. - if (grep($_ eq 'bz_schema', $self->bz_table_list_real())) { - die install_string('bz_schema_exists_before_220'); - } + my $bug_count = $self->selectrow_array("SELECT COUNT(*) FROM bugs"); - my $bug_count = $self->selectrow_array("SELECT COUNT(*) FROM bugs"); - # We estimate one minute for each 3000 bugs, plus 3 minutes just - # to handle basic MySQL stuff. - my $rename_time = int($bug_count / 3000) + 3; - # And 45 minutes for every 15,000 attachments, per some experiments. - my ($attachment_count) = - $self->selectrow_array("SELECT COUNT(*) FROM attachments"); - $rename_time += int(($attachment_count * 45) / 15000); - # If we're going to take longer than 5 minutes, we let the user know - # and allow them to abort. - if ($rename_time > 5) { - print "\n", install_string('mysql_index_renaming', - { minutes => $rename_time }); - # Wait 45 seconds for them to respond. - sleep(45) unless Bugzilla->installation_answers->{NO_PAUSE}; - } - print "Renaming indexes...\n"; - - # We can't be interrupted, because of how the "if" - # works above. - local $SIG{INT} = 'IGNORE'; - local $SIG{TERM} = 'IGNORE'; - local $SIG{PIPE} = 'IGNORE'; - - # Certain indexes had names in Schema that did not easily conform - # to a standard. We store those names here, so that they - # can be properly renamed. - # Also, sometimes an old mysqldump would incorrectly rename - # unique indexes to "PRIMARY", so we address that here, also. - my $bad_names = { - # 'when' is a possible leftover from Bugzillas before 2.8 - bugs_activity => ['when', 'bugs_activity_bugid_idx', - 'bugs_activity_bugwhen_idx'], - cc => ['PRIMARY'], - longdescs => ['longdescs_bugid_idx', - 'longdescs_bugwhen_idx'], - flags => ['flags_bidattid_idx'], - flaginclusions => ['flaginclusions_tpcid_idx'], - flagexclusions => ['flagexclusions_tpc_id_idx'], - keywords => ['PRIMARY'], - milestones => ['PRIMARY'], - profiles_activity => ['profiles_activity_when_idx'], - group_control_map => ['group_control_map_gid_idx', 'PRIMARY'], - user_group_map => ['PRIMARY'], - group_group_map => ['PRIMARY'], - email_setting => ['PRIMARY'], - bug_group_map => ['PRIMARY'], - category_group_map => ['PRIMARY'], - watch => ['PRIMARY'], - namedqueries => ['PRIMARY'], - series_data => ['PRIMARY'], - # series_categories is dealt with below, not here. - }; - - # The series table is broken and needs to have one index - # dropped before we begin the renaming, because it had a - # useless index on it that would cause a naming conflict here. - if (grep($_ eq 'series', @tables)) { - my $dropname; - # This is what the bad index was called before Schema. - if ($self->bz_index_info_real('series', 'creator_2')) { - $dropname = 'creator_2'; - } - # This is what the bad index is called in Schema. - elsif ($self->bz_index_info_real('series', 'series_creator_idx')) { - $dropname = 'series_creator_idx'; - } - $self->bz_drop_index_raw('series', $dropname) if $dropname; - } + # We estimate one minute for each 3000 bugs, plus 3 minutes just + # to handle basic MySQL stuff. + my $rename_time = int($bug_count / 3000) + 3; - # The email_setting table also had the same problem. - if( grep($_ eq 'email_setting', @tables) - && $self->bz_index_info_real('email_setting', - 'email_settings_user_id_idx') ) - { - $self->bz_drop_index_raw('email_setting', - 'email_settings_user_id_idx'); - } - - # Go through all the tables. - foreach my $table (@tables) { - # Will contain the names of old indexes as keys, and the - # definition of the new indexes as a value. The values - # include an extra hash key, NAME, with the new name of - # the index. - my %rename_indexes; - # And go through all the columns on each table. - my @columns = $self->bz_table_columns_real($table); - - # We also want to fix the silly naming of unique indexes - # that happened when we first checked-in Bugzilla::DB::Schema. - if ($table eq 'series_categories') { - # The series_categories index had a nonstandard name. - push(@columns, 'series_cats_unique_idx'); - } - elsif ($table eq 'email_setting') { - # The email_setting table had a similar problem. - push(@columns, 'email_settings_unique_idx'); - } - else { - push(@columns, "${table}_unique_idx"); - } - # And this is how we fix the other inconsistent Schema naming. - push(@columns, @{$bad_names->{$table}}) - if (exists $bad_names->{$table}); - foreach my $column (@columns) { - # If we have an index named after this column, it's an - # old-style-name index. - if (my $index = $self->bz_index_info_real($table, $column)) { - # Fix the name to fit in with the new naming scheme. - $index->{NAME} = $table . "_" . - $index->{FIELDS}->[0] . "_idx"; - print "Renaming index $column to " - . $index->{NAME} . "...\n"; - $rename_indexes{$column} = $index; - } # if - } # foreach column - - my @rename_sql = $self->_bz_schema->get_rename_indexes_ddl( - $table, %rename_indexes); - $self->do($_) foreach (@rename_sql); - - } # foreach table - } # if old-name indexes - - # If there are no tables, but the DB isn't utf8 and it should be, - # then we should alter the database to be utf8. We know it should be - # if the utf8 parameter is true or there are no params at all. - # This kind of situation happens when people create the database - # themselves, and if we don't do this they will get the big - # scary WARNING statement about conversion to UTF8. - if ( !$self->bz_db_is_utf8 && !@tables - && (Bugzilla->params->{'utf8'} || !scalar keys %{Bugzilla->params}) ) - { - $self->_alter_db_charset_to_utf8(); - } + # And 45 minutes for every 15,000 attachments, per some experiments. + my ($attachment_count) + = $self->selectrow_array("SELECT COUNT(*) FROM attachments"); + $rename_time += int(($attachment_count * 45) / 15000); - # And now we create the tables and the Schema object. - $self->SUPER::bz_setup_database(); + # If we're going to take longer than 5 minutes, we let the user know + # and allow them to abort. + if ($rename_time > 5) { + print "\n", install_string('mysql_index_renaming', {minutes => $rename_time}); - if ($sd_index_deleted) { - $self->_bz_real_schema->delete_index('bugs', 'bugs_short_desc_idx'); - $self->_bz_store_real_schema; + # Wait 45 seconds for them to respond. + sleep(45) unless Bugzilla->installation_answers->{NO_PAUSE}; } - if ($longdescs_index_deleted) { - $self->_bz_real_schema->delete_index('longdescs', - 'longdescs_thetext_idx'); - $self->_bz_store_real_schema; + print "Renaming indexes...\n"; + + # We can't be interrupted, because of how the "if" + # works above. + local $SIG{INT} = 'IGNORE'; + local $SIG{TERM} = 'IGNORE'; + local $SIG{PIPE} = 'IGNORE'; + + # Certain indexes had names in Schema that did not easily conform + # to a standard. We store those names here, so that they + # can be properly renamed. + # Also, sometimes an old mysqldump would incorrectly rename + # unique indexes to "PRIMARY", so we address that here, also. + my $bad_names = { + + # 'when' is a possible leftover from Bugzillas before 2.8 + bugs_activity => + ['when', 'bugs_activity_bugid_idx', 'bugs_activity_bugwhen_idx'], + cc => ['PRIMARY'], + longdescs => ['longdescs_bugid_idx', 'longdescs_bugwhen_idx'], + flags => ['flags_bidattid_idx'], + flaginclusions => ['flaginclusions_tpcid_idx'], + flagexclusions => ['flagexclusions_tpc_id_idx'], + keywords => ['PRIMARY'], + milestones => ['PRIMARY'], + profiles_activity => ['profiles_activity_when_idx'], + group_control_map => ['group_control_map_gid_idx', 'PRIMARY'], + user_group_map => ['PRIMARY'], + group_group_map => ['PRIMARY'], + email_setting => ['PRIMARY'], + bug_group_map => ['PRIMARY'], + category_group_map => ['PRIMARY'], + watch => ['PRIMARY'], + namedqueries => ['PRIMARY'], + series_data => ['PRIMARY'], + + # series_categories is dealt with below, not here. + }; + + # The series table is broken and needs to have one index + # dropped before we begin the renaming, because it had a + # useless index on it that would cause a naming conflict here. + if (grep($_ eq 'series', @tables)) { + my $dropname; + + # This is what the bad index was called before Schema. + if ($self->bz_index_info_real('series', 'creator_2')) { + $dropname = 'creator_2'; + } + + # This is what the bad index is called in Schema. + elsif ($self->bz_index_info_real('series', 'series_creator_idx')) { + $dropname = 'series_creator_idx'; + } + $self->bz_drop_index_raw('series', $dropname) if $dropname; } - # The old timestamp fields need to be adjusted here instead of in - # checksetup. Otherwise the UPDATE statements inside of bz_add_column - # will cause accidental timestamp updates. - # The code that does this was moved here from checksetup. - - # 2002-08-14 - bbaetz@student.usyd.edu.au - bug 153578 - # attachments creation time needs to be a datetime, not a timestamp - my $attach_creation = - $self->bz_column_info("attachments", "creation_ts"); - if ($attach_creation && $attach_creation->{TYPE} =~ /^TIMESTAMP/i) { - print "Fixing creation time on attachments...\n"; + # The email_setting table also had the same problem. + if (grep($_ eq 'email_setting', @tables) + && $self->bz_index_info_real('email_setting', 'email_settings_user_id_idx')) + { + $self->bz_drop_index_raw('email_setting', 'email_settings_user_id_idx'); + } - my $sth = $self->prepare("SELECT COUNT(attach_id) FROM attachments"); - $sth->execute(); - my ($attach_count) = $sth->fetchrow_array(); + # Go through all the tables. + foreach my $table (@tables) { - if ($attach_count > 1000) { - print "This may take a while...\n"; - } - my $i = 0; - - # This isn't just as simple as changing the field type, because - # the creation_ts was previously updated when an attachment was made - # obsolete from the attachment creation screen. So we have to go - # and recreate these times from the comments.. - $sth = $self->prepare("SELECT bug_id, attach_id, submitter_id " . - "FROM attachments"); - $sth->execute(); - - # Restrict this as much as possible in order to avoid false - # positives, and keep the db search time down - my $sth2 = $self->prepare("SELECT bug_when FROM longdescs - WHERE bug_id=? AND who=? - AND thetext LIKE ? - ORDER BY bug_when " . $self->sql_limit(1)); - while (my ($bug_id, $attach_id, $submitter_id) - = $sth->fetchrow_array()) - { - $sth2->execute($bug_id, $submitter_id, - "Created an attachment (id=$attach_id)%"); - my ($when) = $sth2->fetchrow_array(); - if ($when) { - $self->do("UPDATE attachments " . - "SET creation_ts='$when' " . - "WHERE attach_id=$attach_id"); - } else { - print "Warning - could not determine correct creation" - . " time for attachment $attach_id on bug $bug_id\n"; - } - ++$i; - print "Converted $i of $attach_count attachments\n" if !($i % 1000); - } - print "Done - converted $i attachments\n"; + # Will contain the names of old indexes as keys, and the + # definition of the new indexes as a value. The values + # include an extra hash key, NAME, with the new name of + # the index. + my %rename_indexes; + + # And go through all the columns on each table. + my @columns = $self->bz_table_columns_real($table); + + # We also want to fix the silly naming of unique indexes + # that happened when we first checked-in Bugzilla::DB::Schema. + if ($table eq 'series_categories') { + + # The series_categories index had a nonstandard name. + push(@columns, 'series_cats_unique_idx'); + } + elsif ($table eq 'email_setting') { + + # The email_setting table had a similar problem. + push(@columns, 'email_settings_unique_idx'); + } + else { + push(@columns, "${table}_unique_idx"); + } + + # And this is how we fix the other inconsistent Schema naming. + push(@columns, @{$bad_names->{$table}}) if (exists $bad_names->{$table}); + foreach my $column (@columns) { + + # If we have an index named after this column, it's an + # old-style-name index. + if (my $index = $self->bz_index_info_real($table, $column)) { + + # Fix the name to fit in with the new naming scheme. + $index->{NAME} = $table . "_" . $index->{FIELDS}->[0] . "_idx"; + print "Renaming index $column to " . $index->{NAME} . "...\n"; + $rename_indexes{$column} = $index; + } # if + } # foreach column + + my @rename_sql + = $self->_bz_schema->get_rename_indexes_ddl($table, %rename_indexes); + $self->do($_) foreach (@rename_sql); + + } # foreach table + } # if old-name indexes + + # If there are no tables, but the DB isn't utf8 and it should be, + # then we should alter the database to be utf8. We know it should be + # if the utf8 parameter is true or there are no params at all. + # This kind of situation happens when people create the database + # themselves, and if we don't do this they will get the big + # scary WARNING statement about conversion to UTF8. + if ( !$self->bz_db_is_utf8 + && !@tables + && (Bugzilla->params->{'utf8'} || !scalar keys %{Bugzilla->params})) + { + $self->_alter_db_charset_to_utf8(); + } + + # And now we create the tables and the Schema object. + $self->SUPER::bz_setup_database(); + + if ($sd_index_deleted) { + $self->_bz_real_schema->delete_index('bugs', 'bugs_short_desc_idx'); + $self->_bz_store_real_schema; + } + if ($longdescs_index_deleted) { + $self->_bz_real_schema->delete_index('longdescs', 'longdescs_thetext_idx'); + $self->_bz_store_real_schema; + } + + # The old timestamp fields need to be adjusted here instead of in + # checksetup. Otherwise the UPDATE statements inside of bz_add_column + # will cause accidental timestamp updates. + # The code that does this was moved here from checksetup. + + # 2002-08-14 - bbaetz@student.usyd.edu.au - bug 153578 + # attachments creation time needs to be a datetime, not a timestamp + my $attach_creation = $self->bz_column_info("attachments", "creation_ts"); + if ($attach_creation && $attach_creation->{TYPE} =~ /^TIMESTAMP/i) { + print "Fixing creation time on attachments...\n"; + + my $sth = $self->prepare("SELECT COUNT(attach_id) FROM attachments"); + $sth->execute(); + my ($attach_count) = $sth->fetchrow_array(); - $self->bz_alter_column("attachments", "creation_ts", - {TYPE => 'DATETIME', NOTNULL => 1}); + if ($attach_count > 1000) { + print "This may take a while...\n"; } + my $i = 0; - # 2004-08-29 - Tomas.Kopal@altap.cz, bug 257303 - # Change logincookies.lastused type from timestamp to datetime - my $login_lastused = $self->bz_column_info("logincookies", "lastused"); - if ($login_lastused && $login_lastused->{TYPE} =~ /^TIMESTAMP/i) { - $self->bz_alter_column('logincookies', 'lastused', - { TYPE => 'DATETIME', NOTNULL => 1}); - } + # This isn't just as simple as changing the field type, because + # the creation_ts was previously updated when an attachment was made + # obsolete from the attachment creation screen. So we have to go + # and recreate these times from the comments.. + $sth = $self->prepare( + "SELECT bug_id, attach_id, submitter_id " . "FROM attachments"); + $sth->execute(); - # 2005-01-17 - Tomas.Kopal@altap.cz, bug 257315 - # Change bugs.delta_ts type from timestamp to datetime - my $bugs_deltats = $self->bz_column_info("bugs", "delta_ts"); - if ($bugs_deltats && $bugs_deltats->{TYPE} =~ /^TIMESTAMP/i) { - $self->bz_alter_column('bugs', 'delta_ts', - {TYPE => 'DATETIME', NOTNULL => 1}); + # Restrict this as much as possible in order to avoid false + # positives, and keep the db search time down + my $sth2 = $self->prepare( + "SELECT bug_when FROM longdescs + WHERE bug_id=? AND who=? + AND thetext LIKE ? + ORDER BY bug_when " . $self->sql_limit(1) + ); + while (my ($bug_id, $attach_id, $submitter_id) = $sth->fetchrow_array()) { + $sth2->execute($bug_id, $submitter_id, + "Created an attachment (id=$attach_id)%"); + my ($when) = $sth2->fetchrow_array(); + if ($when) { + $self->do("UPDATE attachments " + . "SET creation_ts='$when' " + . "WHERE attach_id=$attach_id"); + } + else { + print "Warning - could not determine correct creation" + . " time for attachment $attach_id on bug $bug_id\n"; + } + ++$i; + print "Converted $i of $attach_count attachments\n" if !($i % 1000); } - - # 2005-09-24 - bugreport@peshkin.net, bug 307602 - # Make sure that default 4G table limit is overridden - my $attach_data_create = $self->selectrow_array( - 'SELECT CREATE_OPTIONS FROM information_schema.TABLES - WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?', - undef, $db_name, 'attach_data'); - if ($attach_data_create !~ /MAX_ROWS/i) { - print "Converting attach_data maximum size to 100G...\n"; - $self->do("ALTER TABLE attach_data + print "Done - converted $i attachments\n"; + + $self->bz_alter_column("attachments", "creation_ts", + {TYPE => 'DATETIME', NOTNULL => 1}); + } + + # 2004-08-29 - Tomas.Kopal@altap.cz, bug 257303 + # Change logincookies.lastused type from timestamp to datetime + my $login_lastused = $self->bz_column_info("logincookies", "lastused"); + if ($login_lastused && $login_lastused->{TYPE} =~ /^TIMESTAMP/i) { + $self->bz_alter_column('logincookies', 'lastused', + {TYPE => 'DATETIME', NOTNULL => 1}); + } + + # 2005-01-17 - Tomas.Kopal@altap.cz, bug 257315 + # Change bugs.delta_ts type from timestamp to datetime + my $bugs_deltats = $self->bz_column_info("bugs", "delta_ts"); + if ($bugs_deltats && $bugs_deltats->{TYPE} =~ /^TIMESTAMP/i) { + $self->bz_alter_column('bugs', 'delta_ts', {TYPE => 'DATETIME', NOTNULL => 1}); + } + + # 2005-09-24 - bugreport@peshkin.net, bug 307602 + # Make sure that default 4G table limit is overridden + my $attach_data_create = $self->selectrow_array( + 'SELECT CREATE_OPTIONS FROM information_schema.TABLES + WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?', undef, $db_name, 'attach_data' + ); + if ($attach_data_create !~ /MAX_ROWS/i) { + print "Converting attach_data maximum size to 100G...\n"; + $self->do( + "ALTER TABLE attach_data AVG_ROW_LENGTH=1000000, - MAX_ROWS=100000"); - } - - # Convert the database to UTF-8 if the utf8 parameter is on. - # We check if any table isn't utf8, because lots of crazy - # partial-conversion situations can happen, and this handles anything - # that could come up (including having the DB charset be utf8 but not - # the table charsets. - # - # TABLE_COLLATION IS NOT NULL prevents us from trying to convert views. - my $non_utf8_tables = $self->selectrow_array( - "SELECT 1 FROM information_schema.TABLES + MAX_ROWS=100000" + ); + } + + # Convert the database to UTF-8 if the utf8 parameter is on. + # We check if any table isn't utf8, because lots of crazy + # partial-conversion situations can happen, and this handles anything + # that could come up (including having the DB charset be utf8 but not + # the table charsets. + # + # TABLE_COLLATION IS NOT NULL prevents us from trying to convert views. + my $non_utf8_tables = $self->selectrow_array( + "SELECT 1 FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_COLLATION IS NOT NULL AND TABLE_COLLATION NOT LIKE 'utf8%' - LIMIT 1", undef, $db_name); - - if (Bugzilla->params->{'utf8'} && $non_utf8_tables) { - print "\n", install_string('mysql_utf8_conversion'); - - if (!Bugzilla->installation_answers->{NO_PAUSE}) { - if (Bugzilla->installation_mode == - INSTALLATION_MODE_NON_INTERACTIVE) - { - die install_string('continue_without_answers'), "\n"; - } - else { - print "\n " . install_string('enter_or_ctrl_c'); - getc; - } - } - - print "Converting table storage format to UTF-8. This may take a", - " while.\n"; - foreach my $table ($self->bz_table_list_real) { - my $info_sth = $self->prepare("SHOW FULL COLUMNS FROM $table"); - $info_sth->execute(); - my (@binary_sql, @utf8_sql); - while (my $column = $info_sth->fetchrow_hashref) { - # Our conversion code doesn't work on enum fields, but they - # all go away later in checksetup anyway. - next if $column->{Type} =~ /enum/i; - - # If this particular column isn't stored in utf-8 - if ($column->{Collation} - && $column->{Collation} ne 'NULL' - && $column->{Collation} !~ /utf8/) - { - my $name = $column->{Field}; - - print "$table.$name needs to be converted to UTF-8...\n"; - - # These will be automatically re-created at the end - # of checksetup. - $self->bz_drop_related_fks($table, $name); - - my $col_info = - $self->bz_column_info_real($table, $name); - # CHANGE COLUMN doesn't take PRIMARY KEY - delete $col_info->{PRIMARYKEY}; - my $sql_def = $self->_bz_schema->get_type_ddl($col_info); - # We don't want MySQL to actually try to *convert* - # from our current charset to UTF-8, we just want to - # transfer the bytes directly. This is how we do that. - - # The CHARACTER SET part of the definition has to come - # right after the type, which will always come first. - my ($binary, $utf8) = ($sql_def, $sql_def); - my $type = $self->_bz_schema->convert_type($col_info->{TYPE}); - $binary =~ s/(\Q$type\E)/$1 CHARACTER SET binary/; - $utf8 =~ s/(\Q$type\E)/$1 CHARACTER SET utf8/; - push(@binary_sql, "MODIFY COLUMN $name $binary"); - push(@utf8_sql, "MODIFY COLUMN $name $utf8"); - } - } # foreach column - - if (@binary_sql) { - my %indexes = %{ $self->bz_table_indexes($table) }; - foreach my $index_name (keys %indexes) { - my $index = $indexes{$index_name}; - if ($index->{TYPE} and $index->{TYPE} eq 'FULLTEXT') { - $self->bz_drop_index($table, $index_name); - } - else { - delete $indexes{$index_name}; - } - } - - print "Converting the $table table to UTF-8...\n"; - my $bin = "ALTER TABLE $table " . join(', ', @binary_sql); - my $utf = "ALTER TABLE $table " . join(', ', @utf8_sql, - 'DEFAULT CHARACTER SET utf8'); - $self->do($bin); - $self->do($utf); - - # Re-add any removed FULLTEXT indexes. - foreach my $index (keys %indexes) { - $self->bz_add_index($table, $index, $indexes{$index}); - } - } - else { - $self->do("ALTER TABLE $table DEFAULT CHARACTER SET utf8"); - } - - } # foreach my $table (@tables) + LIMIT 1", undef, $db_name + ); + + if (Bugzilla->params->{'utf8'} && $non_utf8_tables) { + print "\n", install_string('mysql_utf8_conversion'); + + if (!Bugzilla->installation_answers->{NO_PAUSE}) { + if (Bugzilla->installation_mode == INSTALLATION_MODE_NON_INTERACTIVE) { + die install_string('continue_without_answers'), "\n"; + } + else { + print "\n " . install_string('enter_or_ctrl_c'); + getc; + } } - # Sometimes you can have a situation where all the tables are utf8, - # but the database isn't. (This tends to happen when you've done - # a mysqldump.) So we have this change outside of the above block, - # so that it just happens silently if no actual *table* conversion - # needs to happen. - if (Bugzilla->params->{'utf8'} && !$self->bz_db_is_utf8) { - $self->_alter_db_charset_to_utf8(); - } + print "Converting table storage format to UTF-8. This may take a", " while.\n"; + foreach my $table ($self->bz_table_list_real) { + my $info_sth = $self->prepare("SHOW FULL COLUMNS FROM $table"); + $info_sth->execute(); + my (@binary_sql, @utf8_sql); + while (my $column = $info_sth->fetchrow_hashref) { + + # Our conversion code doesn't work on enum fields, but they + # all go away later in checksetup anyway. + next if $column->{Type} =~ /enum/i; + + # If this particular column isn't stored in utf-8 + if ( $column->{Collation} + && $column->{Collation} ne 'NULL' + && $column->{Collation} !~ /utf8/) + { + my $name = $column->{Field}; - $self->_fix_defaults(); + print "$table.$name needs to be converted to UTF-8...\n"; - # Bug 451735 highlighted a bug in bz_drop_index() which didn't - # check for FKs before trying to delete an index. Consequently, - # the series_creator_idx index was considered to be deleted - # despite it was still present in the DB. That's why we have to - # force the deletion, bypassing the DB schema. - if (!$self->bz_index_info('series', 'series_category_idx')) { - if (!$self->bz_index_info('series', 'series_creator_idx') - && $self->bz_index_info_real('series', 'series_creator_idx')) - { - foreach my $column (qw(creator category subcategory name)) { - $self->bz_drop_related_fks('series', $column); - } - $self->bz_drop_index_raw('series', 'series_creator_idx'); + # These will be automatically re-created at the end + # of checksetup. + $self->bz_drop_related_fks($table, $name); + + my $col_info = $self->bz_column_info_real($table, $name); + + # CHANGE COLUMN doesn't take PRIMARY KEY + delete $col_info->{PRIMARYKEY}; + my $sql_def = $self->_bz_schema->get_type_ddl($col_info); + + # We don't want MySQL to actually try to *convert* + # from our current charset to UTF-8, we just want to + # transfer the bytes directly. This is how we do that. + + # The CHARACTER SET part of the definition has to come + # right after the type, which will always come first. + my ($binary, $utf8) = ($sql_def, $sql_def); + my $type = $self->_bz_schema->convert_type($col_info->{TYPE}); + $binary =~ s/(\Q$type\E)/$1 CHARACTER SET binary/; + $utf8 =~ s/(\Q$type\E)/$1 CHARACTER SET utf8/; + push(@binary_sql, "MODIFY COLUMN $name $binary"); + push(@utf8_sql, "MODIFY COLUMN $name $utf8"); } + } # foreach column + + if (@binary_sql) { + my %indexes = %{$self->bz_table_indexes($table)}; + foreach my $index_name (keys %indexes) { + my $index = $indexes{$index_name}; + if ($index->{TYPE} and $index->{TYPE} eq 'FULLTEXT') { + $self->bz_drop_index($table, $index_name); + } + else { + delete $indexes{$index_name}; + } + } + + print "Converting the $table table to UTF-8...\n"; + my $bin = "ALTER TABLE $table " . join(', ', @binary_sql); + my $utf + = "ALTER TABLE $table " . join(', ', @utf8_sql, 'DEFAULT CHARACTER SET utf8'); + $self->do($bin); + $self->do($utf); + + # Re-add any removed FULLTEXT indexes. + foreach my $index (keys %indexes) { + $self->bz_add_index($table, $index, $indexes{$index}); + } + } + else { + $self->do("ALTER TABLE $table DEFAULT CHARACTER SET utf8"); + } + + } # foreach my $table (@tables) + } + + # Sometimes you can have a situation where all the tables are utf8, + # but the database isn't. (This tends to happen when you've done + # a mysqldump.) So we have this change outside of the above block, + # so that it just happens silently if no actual *table* conversion + # needs to happen. + if (Bugzilla->params->{'utf8'} && !$self->bz_db_is_utf8) { + $self->_alter_db_charset_to_utf8(); + } + + $self->_fix_defaults(); + + # Bug 451735 highlighted a bug in bz_drop_index() which didn't + # check for FKs before trying to delete an index. Consequently, + # the series_creator_idx index was considered to be deleted + # despite it was still present in the DB. That's why we have to + # force the deletion, bypassing the DB schema. + if (!$self->bz_index_info('series', 'series_category_idx')) { + if (!$self->bz_index_info('series', 'series_creator_idx') + && $self->bz_index_info_real('series', 'series_creator_idx')) + { + foreach my $column (qw(creator category subcategory name)) { + $self->bz_drop_related_fks('series', $column); + } + $self->bz_drop_index_raw('series', 'series_creator_idx'); } + } } # When you import a MySQL 3/4 mysqldump into MySQL 5, columns that @@ -792,100 +809,109 @@ sub bz_setup_database { # looks like. So we remove defaults from columns that aren't supposed # to have them sub _fix_defaults { - my $self = shift; - my $maj_version = substr($self->bz_server_version, 0, 1); - return if $maj_version < 5; - - # The oldest column that could have this problem is bugs.assigned_to, - # so if it doesn't have the problem, we just skip doing this entirely. - my $assi_def = $self->_bz_raw_column_info('bugs', 'assigned_to'); - my $assi_default = $assi_def->{COLUMN_DEF}; - # This "ne ''" thing is necessary because _raw_column_info seems to - # return COLUMN_DEF as an empty string for columns that don't have - # a default. - return unless (defined $assi_default && $assi_default ne ''); - - my %fix_columns; - foreach my $table ($self->_bz_real_schema->get_table_list()) { - foreach my $column ($self->bz_table_columns($table)) { - my $abs_def = $self->bz_column_info($table, $column); - # BLOB/TEXT columns never have defaults - next if $abs_def->{TYPE} =~ /BLOB|TEXT/i; - if (!defined $abs_def->{DEFAULT}) { - # Get the exact default from the database without any - # "fixing" by bz_column_info_real. - my $raw_info = $self->_bz_raw_column_info($table, $column); - my $raw_default = $raw_info->{COLUMN_DEF}; - if (defined $raw_default) { - if ($raw_default eq '') { - # Only (var)char columns can have empty strings as - # defaults, so if we got an empty string for some - # other default type, then it's bogus. - next unless $abs_def->{TYPE} =~ /char/i; - $raw_default = "''"; - } - $fix_columns{$table} ||= []; - push(@{ $fix_columns{$table} }, $column); - print "$table.$column has incorrect DB default: $raw_default\n"; - } - } - } # foreach $column - } # foreach $table - - print "Fixing defaults...\n"; - foreach my $table (reverse sort keys %fix_columns) { - my @alters = map("ALTER COLUMN $_ DROP DEFAULT", - @{ $fix_columns{$table} }); - my $sql = "ALTER TABLE $table " . join(',', @alters); - $self->do($sql); - } + my $self = shift; + my $maj_version = substr($self->bz_server_version, 0, 1); + return if $maj_version < 5; + + # The oldest column that could have this problem is bugs.assigned_to, + # so if it doesn't have the problem, we just skip doing this entirely. + my $assi_def = $self->_bz_raw_column_info('bugs', 'assigned_to'); + my $assi_default = $assi_def->{COLUMN_DEF}; + + # This "ne ''" thing is necessary because _raw_column_info seems to + # return COLUMN_DEF as an empty string for columns that don't have + # a default. + return unless (defined $assi_default && $assi_default ne ''); + + my %fix_columns; + foreach my $table ($self->_bz_real_schema->get_table_list()) { + foreach my $column ($self->bz_table_columns($table)) { + my $abs_def = $self->bz_column_info($table, $column); + + # BLOB/TEXT columns never have defaults + next if $abs_def->{TYPE} =~ /BLOB|TEXT/i; + if (!defined $abs_def->{DEFAULT}) { + + # Get the exact default from the database without any + # "fixing" by bz_column_info_real. + my $raw_info = $self->_bz_raw_column_info($table, $column); + my $raw_default = $raw_info->{COLUMN_DEF}; + if (defined $raw_default) { + if ($raw_default eq '') { + + # Only (var)char columns can have empty strings as + # defaults, so if we got an empty string for some + # other default type, then it's bogus. + next unless $abs_def->{TYPE} =~ /char/i; + $raw_default = "''"; + } + $fix_columns{$table} ||= []; + push(@{$fix_columns{$table}}, $column); + print "$table.$column has incorrect DB default: $raw_default\n"; + } + } + } # foreach $column + } # foreach $table + + print "Fixing defaults...\n"; + foreach my $table (reverse sort keys %fix_columns) { + my @alters = map("ALTER COLUMN $_ DROP DEFAULT", @{$fix_columns{$table}}); + my $sql = "ALTER TABLE $table " . join(',', @alters); + $self->do($sql); + } } sub _alter_db_charset_to_utf8 { - my $self = shift; - my $db_name = Bugzilla->localconfig->{db_name}; - $self->do("ALTER DATABASE $db_name CHARACTER SET utf8"); + my $self = shift; + my $db_name = Bugzilla->localconfig->{db_name}; + $self->do("ALTER DATABASE $db_name CHARACTER SET utf8"); } sub bz_db_is_utf8 { - my $self = shift; - my $db_collation = $self->selectrow_arrayref( - "SHOW VARIABLES LIKE 'character_set_database'"); - # First column holds the variable name, second column holds the value. - return $db_collation->[1] =~ /utf8/ ? 1 : 0; + my $self = shift; + my $db_collation + = $self->selectrow_arrayref("SHOW VARIABLES LIKE 'character_set_database'"); + + # First column holds the variable name, second column holds the value. + return $db_collation->[1] =~ /utf8/ ? 1 : 0; } sub bz_enum_initial_values { - my ($self) = @_; - my %enum_values = %{$self->ENUM_DEFAULTS}; - # Get a complete description of the 'bugs' table; with DBD::MySQL - # there isn't a column-by-column way of doing this. Could use - # $dbh->column_info, but it would go slower and we would have to - # use the undocumented mysql_type_name accessor to get the type - # of each row. - my $sth = $self->prepare("DESCRIBE bugs"); - $sth->execute(); - # Look for the particular columns we are interested in. - while (my ($thiscol, $thistype) = $sth->fetchrow_array()) { - if (defined $enum_values{$thiscol}) { - # this is a column of interest. - my @value_list; - if ($thistype and ($thistype =~ /^enum\(/)) { - # it has an enum type; get the set of values. - while ($thistype =~ /'([^']*)'(.*)/) { - push(@value_list, $1); - $thistype = $2; - } - } - if (@value_list) { - # record the enum values found. - $enum_values{$thiscol} = \@value_list; - } + my ($self) = @_; + my %enum_values = %{$self->ENUM_DEFAULTS}; + + # Get a complete description of the 'bugs' table; with DBD::MySQL + # there isn't a column-by-column way of doing this. Could use + # $dbh->column_info, but it would go slower and we would have to + # use the undocumented mysql_type_name accessor to get the type + # of each row. + my $sth = $self->prepare("DESCRIBE bugs"); + $sth->execute(); + + # Look for the particular columns we are interested in. + while (my ($thiscol, $thistype) = $sth->fetchrow_array()) { + if (defined $enum_values{$thiscol}) { + + # this is a column of interest. + my @value_list; + if ($thistype and ($thistype =~ /^enum\(/)) { + + # it has an enum type; get the set of values. + while ($thistype =~ /'([^']*)'(.*)/) { + push(@value_list, $1); + $thistype = $2; } + } + if (@value_list) { + + # record the enum values found. + $enum_values{$thiscol} = \@value_list; + } } + } - return \%enum_values; + return \%enum_values; } ##################################################################### @@ -916,29 +942,29 @@ backwards-compatibility anyway, for versions of Bugzilla before 2.20. =cut sub bz_column_info_real { - my ($self, $table, $column) = @_; - my $col_data = $self->_bz_raw_column_info($table, $column); - return $self->_bz_schema->column_info_to_column($col_data); + my ($self, $table, $column) = @_; + my $col_data = $self->_bz_raw_column_info($table, $column); + return $self->_bz_schema->column_info_to_column($col_data); } sub _bz_raw_column_info { - my ($self, $table, $column) = @_; - - # DBD::mysql does not support selecting a specific column, - # so we have to get all the columns on the table and find - # the one we want. - my $info_sth = $self->column_info(undef, undef, $table, '%'); - - # Don't use fetchall_hashref as there's a Win32 DBI bug (292821) - my $col_data; - while ($col_data = $info_sth->fetchrow_hashref) { - last if $col_data->{'COLUMN_NAME'} eq $column; - } - - if (!defined $col_data) { - return undef; - } - return $col_data; + my ($self, $table, $column) = @_; + + # DBD::mysql does not support selecting a specific column, + # so we have to get all the columns on the table and find + # the one we want. + my $info_sth = $self->column_info(undef, undef, $table, '%'); + + # Don't use fetchall_hashref as there's a Win32 DBI bug (292821) + my $col_data; + while ($col_data = $info_sth->fetchrow_hashref) { + last if $col_data->{'COLUMN_NAME'} eq $column; + } + + if (!defined $col_data) { + return undef; + } + return $col_data; } =item C @@ -952,42 +978,43 @@ sub _bz_raw_column_info { =cut sub bz_index_info_real { - my ($self, $table, $index) = @_; - - my $sth = $self->prepare("SHOW INDEX FROM $table"); - $sth->execute; - - my @fields; - my $index_type; - # $raw_def will be an arrayref containing the following information: - # 0 = name of the table that the index is on - # 1 = 0 if unique, 1 if not unique - # 2 = name of the index - # 3 = seq_in_index (The order of the current field in the index). - # 4 = Name of ONE column that the index is on - # 5 = 'Collation' of the index. Usually 'A'. - # 6 = Cardinality. Either a number or undef. - # 7 = sub_part. Usually undef. Sometimes 1. - # 8 = "packed". Usually undef. - # 9 = Null. Sometimes undef, sometimes 'YES'. - # 10 = Index_type. The type of the index. Usually either 'BTREE' or 'FULLTEXT' - # 11 = 'Comment.' Usually undef. - while (my $raw_def = $sth->fetchrow_arrayref) { - if ($raw_def->[2] eq $index) { - push(@fields, $raw_def->[4]); - # No index can be both UNIQUE and FULLTEXT, that's why - # this is written this way. - $index_type = $raw_def->[1] ? '' : 'UNIQUE'; - $index_type = $raw_def->[10] eq 'FULLTEXT' - ? 'FULLTEXT' : $index_type; - } + my ($self, $table, $index) = @_; + + my $sth = $self->prepare("SHOW INDEX FROM $table"); + $sth->execute; + + my @fields; + my $index_type; + + # $raw_def will be an arrayref containing the following information: + # 0 = name of the table that the index is on + # 1 = 0 if unique, 1 if not unique + # 2 = name of the index + # 3 = seq_in_index (The order of the current field in the index). + # 4 = Name of ONE column that the index is on + # 5 = 'Collation' of the index. Usually 'A'. + # 6 = Cardinality. Either a number or undef. + # 7 = sub_part. Usually undef. Sometimes 1. + # 8 = "packed". Usually undef. + # 9 = Null. Sometimes undef, sometimes 'YES'. + # 10 = Index_type. The type of the index. Usually either 'BTREE' or 'FULLTEXT' + # 11 = 'Comment.' Usually undef. + while (my $raw_def = $sth->fetchrow_arrayref) { + if ($raw_def->[2] eq $index) { + push(@fields, $raw_def->[4]); + + # No index can be both UNIQUE and FULLTEXT, that's why + # this is written this way. + $index_type = $raw_def->[1] ? '' : 'UNIQUE'; + $index_type = $raw_def->[10] eq 'FULLTEXT' ? 'FULLTEXT' : $index_type; } + } - my $retval; - if (scalar(@fields)) { - $retval = {FIELDS => \@fields, TYPE => $index_type}; - } - return $retval; + my $retval; + if (scalar(@fields)) { + $retval = {FIELDS => \@fields, TYPE => $index_type}; + } + return $retval; } =item C @@ -1000,10 +1027,11 @@ sub bz_index_info_real { =cut sub bz_index_list_real { - my ($self, $table) = @_; - my $sth = $self->prepare("SHOW INDEX FROM $table"); - # Column 3 of a SHOW INDEX statement contains the name of the index. - return @{ $self->selectcol_arrayref($sth, {Columns => [3]}) }; + my ($self, $table) = @_; + my $sth = $self->prepare("SHOW INDEX FROM $table"); + + # Column 3 of a SHOW INDEX statement contains the name of the index. + return @{$self->selectcol_arrayref($sth, {Columns => [3]})}; } ##################################################################### @@ -1027,34 +1055,33 @@ this code does. # bz_column_info_real function would be very difficult to create # properly for any other DB besides MySQL. sub _bz_build_schema_from_disk { - my ($self) = @_; - - my $schema = $self->_bz_schema->get_empty_schema(); - - my @tables = $self->bz_table_list_real(); - if (@tables) { - print "Building Schema object from database...\n"; + my ($self) = @_; + + my $schema = $self->_bz_schema->get_empty_schema(); + + my @tables = $self->bz_table_list_real(); + if (@tables) { + print "Building Schema object from database...\n"; + } + foreach my $table (@tables) { + $schema->add_table($table); + my @columns = $self->bz_table_columns_real($table); + foreach my $column (@columns) { + my $type_info = $self->bz_column_info_real($table, $column); + $schema->set_column($table, $column, $type_info); } - foreach my $table (@tables) { - $schema->add_table($table); - my @columns = $self->bz_table_columns_real($table); - foreach my $column (@columns) { - my $type_info = $self->bz_column_info_real($table, $column); - $schema->set_column($table, $column, $type_info); - } - my @indexes = $self->bz_index_list_real($table); - foreach my $index (@indexes) { - unless ($index eq 'PRIMARY') { - my $index_info = $self->bz_index_info_real($table, $index); - ($index_info = $index_info->{FIELDS}) - if (!$index_info->{TYPE}); - $schema->set_index($table, $index, $index_info); - } - } + my @indexes = $self->bz_index_list_real($table); + foreach my $index (@indexes) { + unless ($index eq 'PRIMARY') { + my $index_info = $self->bz_index_info_real($table, $index); + ($index_info = $index_info->{FIELDS}) if (!$index_info->{TYPE}); + $schema->set_index($table, $index, $index_info); + } } + } - return $schema; + return $schema; } 1; diff --git a/Bugzilla/DB/Oracle.pm b/Bugzilla/DB/Oracle.pm index 7424019ac..337a0b5ba 100644 --- a/Bugzilla/DB/Oracle.pm +++ b/Bugzilla/DB/Oracle.pm @@ -38,461 +38,473 @@ use Bugzilla::Util; ##################################################################### # Constants ##################################################################### -use constant EMPTY_STRING => '__BZ_EMPTY_STR__'; +use constant EMPTY_STRING => '__BZ_EMPTY_STR__'; use constant ISOLATION_LEVEL => 'READ COMMITTED'; -use constant BLOB_TYPE => { ora_type => ORA_BLOB }; +use constant BLOB_TYPE => {ora_type => ORA_BLOB}; + # The max size allowed for LOB fields, in kilobytes. use constant MIN_LONG_READ_LEN => 32 * 1024; -use constant FULLTEXT_OR => ' OR '; +use constant FULLTEXT_OR => ' OR '; sub new { - my ($class, $params) = @_; - my ($user, $pass, $host, $dbname, $port) = - @$params{qw(db_user db_pass db_host db_name db_port)}; - - # You can never connect to Oracle without a DB name, - # and there is no default DB. - $dbname ||= Bugzilla->localconfig->{db_name}; - - # Set the language enviroment - $ENV{'NLS_LANG'} = '.AL32UTF8' if Bugzilla->params->{'utf8'}; - - # construct the DSN from the parameters we got - my $dsn = "dbi:Oracle:host=$host;sid=$dbname"; - $dsn .= ";port=$port" if $port; - my $attrs = { FetchHashKeyName => 'NAME_lc', - LongReadLen => max(Bugzilla->params->{'maxattachmentsize'} || 0, - MIN_LONG_READ_LEN) * 1024, - }; - my $self = $class->db_new({ dsn => $dsn, user => $user, - pass => $pass, attrs => $attrs }); - # Needed by TheSchwartz - $self->{private_bz_dsn} = $dsn; - - bless ($self, $class); - - # Set the session's default date format to match MySQL - $self->do("ALTER SESSION SET NLS_DATE_FORMAT='YYYY-MM-DD HH24:MI:SS'"); - $self->do("ALTER SESSION SET NLS_TIMESTAMP_FORMAT='YYYY-MM-DD HH24:MI:SS'"); - $self->do("ALTER SESSION SET NLS_LENGTH_SEMANTICS='CHAR'") - if Bugzilla->params->{'utf8'}; - # To allow case insensitive query. - $self->do("ALTER SESSION SET NLS_COMP='ANSI'"); - $self->do("ALTER SESSION SET NLS_SORT='BINARY_AI'"); - return $self; + my ($class, $params) = @_; + my ($user, $pass, $host, $dbname, $port) + = @$params{qw(db_user db_pass db_host db_name db_port)}; + + # You can never connect to Oracle without a DB name, + # and there is no default DB. + $dbname ||= Bugzilla->localconfig->{db_name}; + + # Set the language enviroment + $ENV{'NLS_LANG'} = '.AL32UTF8' if Bugzilla->params->{'utf8'}; + + # construct the DSN from the parameters we got + my $dsn = "dbi:Oracle:host=$host;sid=$dbname"; + $dsn .= ";port=$port" if $port; + my $attrs = { + FetchHashKeyName => 'NAME_lc', + LongReadLen => + max(Bugzilla->params->{'maxattachmentsize'} || 0, MIN_LONG_READ_LEN) * 1024, + }; + my $self = $class->db_new( + {dsn => $dsn, user => $user, pass => $pass, attrs => $attrs}); + + # Needed by TheSchwartz + $self->{private_bz_dsn} = $dsn; + + bless($self, $class); + + # Set the session's default date format to match MySQL + $self->do("ALTER SESSION SET NLS_DATE_FORMAT='YYYY-MM-DD HH24:MI:SS'"); + $self->do("ALTER SESSION SET NLS_TIMESTAMP_FORMAT='YYYY-MM-DD HH24:MI:SS'"); + $self->do("ALTER SESSION SET NLS_LENGTH_SEMANTICS='CHAR'") + if Bugzilla->params->{'utf8'}; + + # To allow case insensitive query. + $self->do("ALTER SESSION SET NLS_COMP='ANSI'"); + $self->do("ALTER SESSION SET NLS_SORT='BINARY_AI'"); + return $self; } sub bz_last_key { - my ($self, $table, $column) = @_; + my ($self, $table, $column) = @_; - my $seq = $table . "_" . $column . "_SEQ"; - my ($last_insert_id) = $self->selectrow_array("SELECT $seq.CURRVAL " - . " FROM DUAL"); - return $last_insert_id; + my $seq = $table . "_" . $column . "_SEQ"; + my ($last_insert_id) + = $self->selectrow_array("SELECT $seq.CURRVAL " . " FROM DUAL"); + return $last_insert_id; } sub bz_check_regexp { - my ($self, $pattern) = @_; + my ($self, $pattern) = @_; - eval { $self->do("SELECT 1 FROM DUAL WHERE " - . $self->sql_regexp($self->quote("a"), $pattern, 1)) }; + eval { + $self->do("SELECT 1 FROM DUAL WHERE " + . $self->sql_regexp($self->quote("a"), $pattern, 1)); + }; - $@ && ThrowUserError('illegal_regexp', - { value => $pattern, dberror => $self->errstr }); + $@ + && ThrowUserError('illegal_regexp', + {value => $pattern, dberror => $self->errstr}); } -sub bz_explain { - my ($self, $sql) = @_; - my $sth = $self->prepare("EXPLAIN PLAN FOR $sql"); - $sth->execute(); - my $explain = $self->selectcol_arrayref( - "SELECT PLAN_TABLE_OUTPUT FROM TABLE(DBMS_XPLAN.DISPLAY)"); - return join("\n", @$explain); -} +sub bz_explain { + my ($self, $sql) = @_; + my $sth = $self->prepare("EXPLAIN PLAN FOR $sql"); + $sth->execute(); + my $explain = $self->selectcol_arrayref( + "SELECT PLAN_TABLE_OUTPUT FROM TABLE(DBMS_XPLAN.DISPLAY)"); + return join("\n", @$explain); +} sub sql_group_concat { - my ($self, $text, $separator) = @_; - $separator = $self->quote(', ') if !defined $separator; - my ($distinct, $rest) = $text =~/^(\s*DISTINCT\s|)(.+)$/i; - return "group_concat($distinct T_CLOB_DELIM(NVL($rest, ' '), $separator))"; + my ($self, $text, $separator) = @_; + $separator = $self->quote(', ') if !defined $separator; + my ($distinct, $rest) = $text =~ /^(\s*DISTINCT\s|)(.+)$/i; + return "group_concat($distinct T_CLOB_DELIM(NVL($rest, ' '), $separator))"; } sub sql_regexp { - my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_; - $real_pattern ||= $pattern; + my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_; + $real_pattern ||= $pattern; - $self->bz_check_regexp($real_pattern) if !$nocheck; + $self->bz_check_regexp($real_pattern) if !$nocheck; - return "REGEXP_LIKE($expr, $pattern)"; + return "REGEXP_LIKE($expr, $pattern)"; } sub sql_not_regexp { - my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_; - $real_pattern ||= $pattern; + my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_; + $real_pattern ||= $pattern; - $self->bz_check_regexp($real_pattern) if !$nocheck; + $self->bz_check_regexp($real_pattern) if !$nocheck; - return "NOT REGEXP_LIKE($expr, $pattern)" + return "NOT REGEXP_LIKE($expr, $pattern)"; } sub sql_limit { - my ($self, $limit, $offset) = @_; + my ($self, $limit, $offset) = @_; - if(defined $offset) { - return "/* LIMIT $limit $offset */"; - } - return "/* LIMIT $limit */"; + if (defined $offset) { + return "/* LIMIT $limit $offset */"; + } + return "/* LIMIT $limit */"; } sub sql_string_concat { - my ($self, @params) = @_; + my ($self, @params) = @_; - return 'CONCAT(' . join(', ', @params) . ')'; + return 'CONCAT(' . join(', ', @params) . ')'; } sub sql_to_days { - my ($self, $date) = @_; + my ($self, $date) = @_; - return " TO_CHAR(TO_DATE($date),'J') "; + return " TO_CHAR(TO_DATE($date),'J') "; } -sub sql_from_days{ - my ($self, $date) = @_; - return " TO_DATE($date,'J') "; +sub sql_from_days { + my ($self, $date) = @_; + + return " TO_DATE($date,'J') "; } sub sql_fulltext_search { - my ($self, $column, $text) = @_; - state $label = 0; - $text = $self->quote($text); - trick_taint($text); - $label++; - return "CONTAINS($column,$text,$label) > 0", "SCORE($label)"; + my ($self, $column, $text) = @_; + state $label = 0; + $text = $self->quote($text); + trick_taint($text); + $label++; + return "CONTAINS($column,$text,$label) > 0", "SCORE($label)"; } sub sql_date_format { - my ($self, $date, $format) = @_; - - $format = "%Y.%m.%d %H:%i:%s" if !$format; + my ($self, $date, $format) = @_; + + $format = "%Y.%m.%d %H:%i:%s" if !$format; - $format =~ s/\%Y/YYYY/g; - $format =~ s/\%y/YY/g; - $format =~ s/\%m/MM/g; - $format =~ s/\%d/DD/g; - $format =~ s/\%a/Dy/g; - $format =~ s/\%H/HH24/g; - $format =~ s/\%i/MI/g; - $format =~ s/\%s/SS/g; + $format =~ s/\%Y/YYYY/g; + $format =~ s/\%y/YY/g; + $format =~ s/\%m/MM/g; + $format =~ s/\%d/DD/g; + $format =~ s/\%a/Dy/g; + $format =~ s/\%H/HH24/g; + $format =~ s/\%i/MI/g; + $format =~ s/\%s/SS/g; - return "TO_CHAR($date, " . $self->quote($format) . ")"; + return "TO_CHAR($date, " . $self->quote($format) . ")"; } sub sql_date_math { - my ($self, $date, $operator, $interval, $units) = @_; - my $time_sql; - if ($units =~ /YEAR|MONTH/i) { - $time_sql = "NUMTOYMINTERVAL($interval,'$units')"; - } else{ - $time_sql = "NUMTODSINTERVAL($interval,'$units')"; - } - return "$date $operator $time_sql"; + my ($self, $date, $operator, $interval, $units) = @_; + my $time_sql; + if ($units =~ /YEAR|MONTH/i) { + $time_sql = "NUMTOYMINTERVAL($interval,'$units')"; + } + else { + $time_sql = "NUMTODSINTERVAL($interval,'$units')"; + } + return "$date $operator $time_sql"; } sub sql_position { - my ($self, $fragment, $text) = @_; - return "INSTR($text, $fragment)"; + my ($self, $fragment, $text) = @_; + return "INSTR($text, $fragment)"; } sub sql_in { - my ($self, $column_name, $in_list_ref, $negate) = @_; - my @in_list = @$in_list_ref; - return $self->SUPER::sql_in($column_name, $in_list_ref, $negate) if $#in_list < 1000; - my @in_str; - while (@in_list) { - my $length = $#in_list + 1; - my $splice = $length > 1000 ? 1000 : $length; - my @sub_in_list = splice(@in_list, 0, $splice); - push(@in_str, - $self->SUPER::sql_in($column_name, \@sub_in_list, $negate)); - } - return "( " . join(" OR ", @in_str) . " )"; + my ($self, $column_name, $in_list_ref, $negate) = @_; + my @in_list = @$in_list_ref; + return $self->SUPER::sql_in($column_name, $in_list_ref, $negate) + if $#in_list < 1000; + my @in_str; + while (@in_list) { + my $length = $#in_list + 1; + my $splice = $length > 1000 ? 1000 : $length; + my @sub_in_list = splice(@in_list, 0, $splice); + push(@in_str, $self->SUPER::sql_in($column_name, \@sub_in_list, $negate)); + } + return "( " . join(" OR ", @in_str) . " )"; } sub _bz_add_field_table { - my ($self, $name, $schema_ref, $type) = @_; - $self->SUPER::_bz_add_field_table($name, $schema_ref); - if (defined($type) && $type == FIELD_TYPE_MULTI_SELECT) { - my $uk_name = "UK_" . $self->_bz_schema->_hash_identifier($name . '_value'); - $self->do("ALTER TABLE $name ADD CONSTRAINT $uk_name UNIQUE(value)"); - } + my ($self, $name, $schema_ref, $type) = @_; + $self->SUPER::_bz_add_field_table($name, $schema_ref); + if (defined($type) && $type == FIELD_TYPE_MULTI_SELECT) { + my $uk_name = "UK_" . $self->_bz_schema->_hash_identifier($name . '_value'); + $self->do("ALTER TABLE $name ADD CONSTRAINT $uk_name UNIQUE(value)"); + } } sub bz_drop_table { - my ($self, $name) = @_; - my $table_exists = $self->bz_table_info($name); - if ($table_exists) { - $self->_bz_drop_fks($name); - $self->SUPER::bz_drop_table($name); - } + my ($self, $name) = @_; + my $table_exists = $self->bz_table_info($name); + if ($table_exists) { + $self->_bz_drop_fks($name); + $self->SUPER::bz_drop_table($name); + } } -# Dropping all FKs for a specified table. +# Dropping all FKs for a specified table. sub _bz_drop_fks { - my ($self, $table) = @_; - my @columns = $self->bz_table_columns($table); - foreach my $column (@columns) { - $self->bz_drop_fk($table, $column); - } + my ($self, $table) = @_; + my @columns = $self->bz_table_columns($table); + foreach my $column (@columns) { + $self->bz_drop_fk($table, $column); + } } sub _fix_empty { - my ($string) = @_; - $string = '' if $string eq EMPTY_STRING; - return $string; + my ($string) = @_; + $string = '' if $string eq EMPTY_STRING; + return $string; } sub _fix_arrayref { - my ($row) = @_; - return undef if !defined $row; - foreach my $field (@$row) { - $field = _fix_empty($field) if defined $field; - } - return $row; + my ($row) = @_; + return undef if !defined $row; + foreach my $field (@$row) { + $field = _fix_empty($field) if defined $field; + } + return $row; } sub _fix_hashref { - my ($row) = @_; - return undef if !defined $row; - foreach my $value (values %$row) { - $value = _fix_empty($value) if defined $value; - } - return $row; + my ($row) = @_; + return undef if !defined $row; + foreach my $value (values %$row) { + $value = _fix_empty($value) if defined $value; + } + return $row; } sub adjust_statement { - my ($sql) = @_; - - if ($sql =~ /^CREATE OR REPLACE.*/i){ - return $sql; - } - - # We can't just assume any occurrence of "''" in $sql is an empty - # string, since "''" can occur inside a string literal as a way of - # escaping a single "'" in the literal. Therefore we must be trickier... - - # split the statement into parts by single-quotes. The negative value - # at the end to the split operator from dropping trailing empty strings - # (e.g., when $sql ends in "''") - my @parts = split /'/, $sql, -1; - - if( !(@parts % 2) ) { - # Either the string is empty or the quotes are mismatched - # Returning input unmodified. - return $sql; + my ($sql) = @_; + + if ($sql =~ /^CREATE OR REPLACE.*/i) { + return $sql; + } + + # We can't just assume any occurrence of "''" in $sql is an empty + # string, since "''" can occur inside a string literal as a way of + # escaping a single "'" in the literal. Therefore we must be trickier... + + # split the statement into parts by single-quotes. The negative value + # at the end to the split operator from dropping trailing empty strings + # (e.g., when $sql ends in "''") + my @parts = split /'/, $sql, -1; + + if (!(@parts % 2)) { + + # Either the string is empty or the quotes are mismatched + # Returning input unmodified. + return $sql; + } + + # We already verified that we have an odd number of parts. If we take + # the first part off now, we know we're entering the loop with an even + # number of parts + my @result; + my $part = shift @parts; + + # Oracle requires a FROM clause in all SELECT statements, so append + # "FROM dual" to queries without one (e.g., "SELECT NOW()") + my $is_select = ($part =~ m/^\s*SELECT\b/io); + my $has_from = ($part =~ m/\bFROM\b/io) if $is_select; + + # Oracle includes the time in CURRENT_DATE. + $part =~ s/\bCURRENT_DATE\b/TRUNC(CURRENT_DATE)/io; + + # Oracle use SUBSTR instead of SUBSTRING + $part =~ s/\bSUBSTRING\b/SUBSTR/io; + + # Oracle need no 'AS' + $part =~ s/\bAS\b//ig; + + # Oracle doesn't have LIMIT, so if we find the LIMIT comment, wrap the + # query with "SELECT * FROM (...) WHERE rownum < $limit" + my ($limit, $offset) = ($part =~ m{/\* LIMIT (\d*) (\d*) \*/}o); + + push @result, $part; + while (@parts) { + my $string = shift @parts; + my $nonstring = shift @parts; + + # if the non-string part is zero-length and there are more parts left, + # then this is an escaped quote inside a string literal + while (!(length $nonstring) && @parts) { + + # we know it's safe to remove two parts at a time, since we + # entered the loop with an even number of parts + $string .= "''" . shift @parts; + $nonstring = shift @parts; } - # We already verified that we have an odd number of parts. If we take - # the first part off now, we know we're entering the loop with an even - # number of parts - my @result; - my $part = shift @parts; - - # Oracle requires a FROM clause in all SELECT statements, so append - # "FROM dual" to queries without one (e.g., "SELECT NOW()") - my $is_select = ($part =~ m/^\s*SELECT\b/io); - my $has_from = ($part =~ m/\bFROM\b/io) if $is_select; + # Look for a FROM if this is a SELECT and we haven't found one yet + $has_from = ($nonstring =~ m/\bFROM\b/io) if ($is_select and !$has_from); # Oracle includes the time in CURRENT_DATE. - $part =~ s/\bCURRENT_DATE\b/TRUNC(CURRENT_DATE)/io; + $nonstring =~ s/\bCURRENT_DATE\b/TRUNC(CURRENT_DATE)/io; # Oracle use SUBSTR instead of SUBSTRING - $part =~ s/\bSUBSTRING\b/SUBSTR/io; - + $nonstring =~ s/\bSUBSTRING\b/SUBSTR/io; + # Oracle need no 'AS' - $part =~ s/\bAS\b//ig; - - # Oracle doesn't have LIMIT, so if we find the LIMIT comment, wrap the - # query with "SELECT * FROM (...) WHERE rownum < $limit" - my ($limit,$offset) = ($part =~ m{/\* LIMIT (\d*) (\d*) \*/}o); - - push @result, $part; - while( @parts ) { - my $string = shift @parts; - my $nonstring = shift @parts; - - # if the non-string part is zero-length and there are more parts left, - # then this is an escaped quote inside a string literal - while( !(length $nonstring) && @parts ) { - # we know it's safe to remove two parts at a time, since we - # entered the loop with an even number of parts - $string .= "''" . shift @parts; - $nonstring = shift @parts; - } + $nonstring =~ s/\bAS\b//ig; - # Look for a FROM if this is a SELECT and we haven't found one yet - $has_from = ($nonstring =~ m/\bFROM\b/io) - if ($is_select and !$has_from); + # Look for a LIMIT clause + ($limit) = ($nonstring =~ m(/\* LIMIT (\d*) \*/)o); - # Oracle includes the time in CURRENT_DATE. - $nonstring =~ s/\bCURRENT_DATE\b/TRUNC(CURRENT_DATE)/io; + if (!length($string)) { + push @result, EMPTY_STRING; + push @result, $nonstring; + } + else { + push @result, $string; + push @result, $nonstring; + } + } - # Oracle use SUBSTR instead of SUBSTRING - $nonstring =~ s/\bSUBSTRING\b/SUBSTR/io; + my $new_sql = join "'", @result; - # Oracle need no 'AS' - $nonstring =~ s/\bAS\b//ig; + # Append "FROM dual" if this is a SELECT without a FROM clause + $new_sql .= " FROM DUAL" if ($is_select and !$has_from); - # Look for a LIMIT clause - ($limit) = ($nonstring =~ m(/\* LIMIT (\d*) \*/)o); + # Wrap the query with a "WHERE rownum <= ..." if we found LIMIT - if(!length($string)){ - push @result, EMPTY_STRING; - push @result, $nonstring; - } else { - push @result, $string; - push @result, $nonstring; - } + if (defined($limit)) { + if ($new_sql !~ /\bWHERE\b/) { + $new_sql = $new_sql . " WHERE 1=1"; } - - my $new_sql = join "'", @result; - - # Append "FROM dual" if this is a SELECT without a FROM clause - $new_sql .= " FROM DUAL" if ($is_select and !$has_from); - - # Wrap the query with a "WHERE rownum <= ..." if we found LIMIT - - if (defined($limit)) { - if ($new_sql !~ /\bWHERE\b/) { - $new_sql = $new_sql." WHERE 1=1"; - } - my ($before_where, $after_where) = split(/\bWHERE\b/i, $new_sql, 2); - if (defined($offset)) { - my ($before_from, $after_from) = split(/\bFROM\b/i, $new_sql, 2); - $before_where = "$before_from FROM ($before_from," - . " ROW_NUMBER() OVER (ORDER BY 1) R " - . " FROM $after_from ) "; - $after_where = " R BETWEEN $offset+1 AND $limit+$offset"; - } else { - $after_where = " rownum <=$limit AND ".$after_where; - } - $new_sql = $before_where." WHERE ".$after_where; + my ($before_where, $after_where) = split(/\bWHERE\b/i, $new_sql, 2); + if (defined($offset)) { + my ($before_from, $after_from) = split(/\bFROM\b/i, $new_sql, 2); + $before_where + = "$before_from FROM ($before_from," + . " ROW_NUMBER() OVER (ORDER BY 1) R " + . " FROM $after_from ) "; + $after_where = " R BETWEEN $offset+1 AND $limit+$offset"; + } + else { + $after_where = " rownum <=$limit AND " . $after_where; } - return $new_sql; + $new_sql = $before_where . " WHERE " . $after_where; + } + return $new_sql; } sub do { - my $self = shift; - my $sql = shift; - $sql = adjust_statement($sql); - unshift @_, $sql; - return $self->SUPER::do(@_); + my $self = shift; + my $sql = shift; + $sql = adjust_statement($sql); + unshift @_, $sql; + return $self->SUPER::do(@_); } sub selectrow_array { - my $self = shift; - my $stmt = shift; - my $new_stmt = (ref $stmt) ? $stmt : adjust_statement($stmt); - unshift @_, $new_stmt; - if ( wantarray ) { - my @row = $self->SUPER::selectrow_array(@_); - _fix_arrayref(\@row); - return @row; - } else { - my $row = $self->SUPER::selectrow_array(@_); - $row = _fix_empty($row) if defined $row; - return $row; - } + my $self = shift; + my $stmt = shift; + my $new_stmt = (ref $stmt) ? $stmt : adjust_statement($stmt); + unshift @_, $new_stmt; + if (wantarray) { + my @row = $self->SUPER::selectrow_array(@_); + _fix_arrayref(\@row); + return @row; + } + else { + my $row = $self->SUPER::selectrow_array(@_); + $row = _fix_empty($row) if defined $row; + return $row; + } } sub selectrow_arrayref { - my $self = shift; - my $stmt = shift; - my $new_stmt = (ref $stmt) ? $stmt : adjust_statement($stmt); - unshift @_, $new_stmt; - my $ref = $self->SUPER::selectrow_arrayref(@_); - return undef if !defined $ref; + my $self = shift; + my $stmt = shift; + my $new_stmt = (ref $stmt) ? $stmt : adjust_statement($stmt); + unshift @_, $new_stmt; + my $ref = $self->SUPER::selectrow_arrayref(@_); + return undef if !defined $ref; - _fix_arrayref($ref); - return $ref; + _fix_arrayref($ref); + return $ref; } sub selectrow_hashref { - my $self = shift; - my $stmt = shift; - my $new_stmt = (ref $stmt) ? $stmt : adjust_statement($stmt); - unshift @_, $new_stmt; - my $ref = $self->SUPER::selectrow_hashref(@_); - return undef if !defined $ref; + my $self = shift; + my $stmt = shift; + my $new_stmt = (ref $stmt) ? $stmt : adjust_statement($stmt); + unshift @_, $new_stmt; + my $ref = $self->SUPER::selectrow_hashref(@_); + return undef if !defined $ref; - _fix_hashref($ref); - return $ref; + _fix_hashref($ref); + return $ref; } sub selectall_arrayref { - my $self = shift; - my $stmt = shift; - my $new_stmt = (ref $stmt) ? $stmt : adjust_statement($stmt); - unshift @_, $new_stmt; - my $ref = $self->SUPER::selectall_arrayref(@_); - return undef if !defined $ref; - - foreach my $row (@$ref) { - if (ref($row) eq 'ARRAY') { - _fix_arrayref($row); - } - elsif (ref($row) eq 'HASH') { - _fix_hashref($row); - } + my $self = shift; + my $stmt = shift; + my $new_stmt = (ref $stmt) ? $stmt : adjust_statement($stmt); + unshift @_, $new_stmt; + my $ref = $self->SUPER::selectall_arrayref(@_); + return undef if !defined $ref; + + foreach my $row (@$ref) { + if (ref($row) eq 'ARRAY') { + _fix_arrayref($row); } + elsif (ref($row) eq 'HASH') { + _fix_hashref($row); + } + } - return $ref; + return $ref; } sub selectall_hashref { - my $self = shift; - my $stmt = shift; - my $new_stmt = (ref $stmt) ? $stmt : adjust_statement($stmt); - unshift @_, $new_stmt; - my $rows = $self->SUPER::selectall_hashref(@_); - return undef if !defined $rows; - foreach my $row (values %$rows) { - _fix_hashref($row); - } - return $rows; + my $self = shift; + my $stmt = shift; + my $new_stmt = (ref $stmt) ? $stmt : adjust_statement($stmt); + unshift @_, $new_stmt; + my $rows = $self->SUPER::selectall_hashref(@_); + return undef if !defined $rows; + foreach my $row (values %$rows) { + _fix_hashref($row); + } + return $rows; } sub selectcol_arrayref { - my $self = shift; - my $stmt = shift; - my $new_stmt = (ref $stmt) ? $stmt : adjust_statement($stmt); - unshift @_, $new_stmt; - my $ref = $self->SUPER::selectcol_arrayref(@_); - return undef if !defined $ref; - _fix_arrayref($ref); - return $ref; + my $self = shift; + my $stmt = shift; + my $new_stmt = (ref $stmt) ? $stmt : adjust_statement($stmt); + unshift @_, $new_stmt; + my $ref = $self->SUPER::selectcol_arrayref(@_); + return undef if !defined $ref; + _fix_arrayref($ref); + return $ref; } sub prepare { - my $self = shift; - my $sql = shift; - my $new_sql = adjust_statement($sql); - unshift @_, $new_sql; - return bless $self->SUPER::prepare(@_), - 'Bugzilla::DB::Oracle::st'; + my $self = shift; + my $sql = shift; + my $new_sql = adjust_statement($sql); + unshift @_, $new_sql; + return bless $self->SUPER::prepare(@_), 'Bugzilla::DB::Oracle::st'; } sub prepare_cached { - my $self = shift; - my $sql = shift; - my $new_sql = adjust_statement($sql); - unshift @_, $new_sql; - return bless $self->SUPER::prepare_cached(@_), - 'Bugzilla::DB::Oracle::st'; + my $self = shift; + my $sql = shift; + my $new_sql = adjust_statement($sql); + unshift @_, $new_sql; + return bless $self->SUPER::prepare_cached(@_), 'Bugzilla::DB::Oracle::st'; } sub quote_identifier { - my ($self,$id) = @_; - return $id; + my ($self, $id) = @_; + return $id; } ##################################################################### @@ -500,20 +512,22 @@ sub quote_identifier { ##################################################################### sub bz_table_columns_real { - my ($self, $table) = @_; - $table = uc($table); - my $cols = $self->selectcol_arrayref( - "SELECT LOWER(COLUMN_NAME) FROM USER_TAB_COLUMNS WHERE - TABLE_NAME = ? ORDER BY COLUMN_NAME", undef, $table); - return @$cols; + my ($self, $table) = @_; + $table = uc($table); + my $cols = $self->selectcol_arrayref( + "SELECT LOWER(COLUMN_NAME) FROM USER_TAB_COLUMNS WHERE + TABLE_NAME = ? ORDER BY COLUMN_NAME", undef, $table + ); + return @$cols; } sub bz_table_list_real { - my ($self) = @_; - my $tables = $self->selectcol_arrayref( - "SELECT LOWER(TABLE_NAME) FROM USER_TABLES WHERE - TABLE_NAME NOT LIKE ? ORDER BY TABLE_NAME", undef, 'DR$%'); - return @$tables; + my ($self) = @_; + my $tables = $self->selectcol_arrayref( + "SELECT LOWER(TABLE_NAME) FROM USER_TABLES WHERE + TABLE_NAME NOT LIKE ? ORDER BY TABLE_NAME", undef, 'DR$%' + ); + return @$tables; } ##################################################################### @@ -521,32 +535,37 @@ sub bz_table_list_real { ##################################################################### sub bz_setup_database { - my $self = shift; - - # Create a function that returns SYSDATE to emulate MySQL's "NOW()". - # Function NOW() is used widely in Bugzilla SQLs, but Oracle does not - # have that function, So we have to create one ourself. - $self->do("CREATE OR REPLACE FUNCTION NOW " - . " RETURN DATE IS BEGIN RETURN SYSDATE; END;"); - $self->do("CREATE OR REPLACE FUNCTION CHAR_LENGTH(COLUMN_NAME VARCHAR2)" - . " RETURN NUMBER IS BEGIN RETURN LENGTH(COLUMN_NAME); END;"); - - # Create types for group_concat - my $type_exists = $self->selectrow_array("SELECT 1 FROM user_types - WHERE type_name = 'T_GROUP_CONCAT'"); - $self->do("DROP TYPE T_GROUP_CONCAT") if $type_exists; - $self->do("CREATE OR REPLACE TYPE T_CLOB_DELIM AS OBJECT " - . "( p_CONTENT CLOB, p_DELIMITER VARCHAR2(256)" - . ", MAP MEMBER FUNCTION T_CLOB_DELIM_ToVarchar return VARCHAR2" - . ");"); - $self->do("CREATE OR REPLACE TYPE BODY T_CLOB_DELIM IS + my $self = shift; + + # Create a function that returns SYSDATE to emulate MySQL's "NOW()". + # Function NOW() is used widely in Bugzilla SQLs, but Oracle does not + # have that function, So we have to create one ourself. + $self->do("CREATE OR REPLACE FUNCTION NOW " + . " RETURN DATE IS BEGIN RETURN SYSDATE; END;"); + $self->do("CREATE OR REPLACE FUNCTION CHAR_LENGTH(COLUMN_NAME VARCHAR2)" + . " RETURN NUMBER IS BEGIN RETURN LENGTH(COLUMN_NAME); END;"); + + # Create types for group_concat + my $type_exists = $self->selectrow_array( + "SELECT 1 FROM user_types + WHERE type_name = 'T_GROUP_CONCAT'" + ); + $self->do("DROP TYPE T_GROUP_CONCAT") if $type_exists; + $self->do("CREATE OR REPLACE TYPE T_CLOB_DELIM AS OBJECT " + . "( p_CONTENT CLOB, p_DELIMITER VARCHAR2(256)" + . ", MAP MEMBER FUNCTION T_CLOB_DELIM_ToVarchar return VARCHAR2" + . ");"); + $self->do( + "CREATE OR REPLACE TYPE BODY T_CLOB_DELIM IS MAP MEMBER FUNCTION T_CLOB_DELIM_ToVarchar return VARCHAR2 is BEGIN RETURN p_CONTENT; END; - END;"); + END;" + ); - $self->do("CREATE OR REPLACE TYPE T_GROUP_CONCAT AS OBJECT + $self->do( + "CREATE OR REPLACE TYPE T_GROUP_CONCAT AS OBJECT ( CLOB_CONTENT CLOB, DELIMITER VARCHAR2(256), STATIC FUNCTION ODCIAGGREGATEINITIALIZE( @@ -564,9 +583,11 @@ sub bz_setup_database { MEMBER FUNCTION ODCIAGGREGATEMERGE( SELF IN OUT NOCOPY T_GROUP_CONCAT, CTX2 IN T_GROUP_CONCAT) - RETURN NUMBER);"); + RETURN NUMBER);" + ); - $self->do("CREATE OR REPLACE TYPE BODY T_GROUP_CONCAT IS + $self->do( + "CREATE OR REPLACE TYPE BODY T_GROUP_CONCAT IS STATIC FUNCTION ODCIAGGREGATEINITIALIZE( SCTX IN OUT NOCOPY T_GROUP_CONCAT) RETURN NUMBER IS @@ -610,110 +631,117 @@ sub bz_setup_database { DBMS_LOB.APPEND(SELF.CLOB_CONTENT, CTX2.CLOB_CONTENT); RETURN ODCICONST.SUCCESS; END; - END;"); + END;" + ); - # Create user-defined aggregate function group_concat - $self->do("CREATE OR REPLACE FUNCTION GROUP_CONCAT(P_INPUT T_CLOB_DELIM) + # Create user-defined aggregate function group_concat + $self->do( + "CREATE OR REPLACE FUNCTION GROUP_CONCAT(P_INPUT T_CLOB_DELIM) RETURN CLOB - DETERMINISTIC PARALLEL_ENABLE AGGREGATE USING T_GROUP_CONCAT;"); - - # Create a WORLD_LEXER named BZ_LEX for multilingual fulltext search - my $lexer = $self->selectcol_arrayref( - "SELECT pre_name FROM CTXSYS.CTX_PREFERENCES WHERE pre_name = ? AND - pre_owner = ?", - undef,'BZ_LEX',uc(Bugzilla->localconfig->{db_user})); - if(!@$lexer) { - $self->do("BEGIN CTX_DDL.CREATE_PREFERENCE - ('BZ_LEX', 'WORLD_LEXER'); END;"); - } - - $self->SUPER::bz_setup_database(@_); - - my $sth = $self->prepare("SELECT OBJECT_NAME FROM USER_OBJECTS WHERE OBJECT_NAME = ?"); - my @tables = $self->bz_table_list_real(); - - foreach my $table (@tables) { - my @columns = $self->bz_table_columns_real($table); - foreach my $column (@columns) { - my $def = $self->bz_column_info($table, $column); - # bz_add_column() before Bugzilla 4.2.3 didn't handle primary keys - # correctly (bug 731156). We have to add missing sequences and - # triggers ourselves. - if ($def->{TYPE} =~ /SERIAL/i) { - my $sequence = "${table}_${column}_SEQ"; - my $exists = $self->selectrow_array($sth, undef, $sequence); - if (!$exists) { - my @sql = $self->_get_create_seq_ddl($table, $column); - $self->do($_) foreach @sql; - } - } - - if ($def->{REFERENCES}) { - my $references = $def->{REFERENCES}; - my $update = $references->{UPDATE} || 'CASCADE'; - my $to_table = $references->{TABLE}; - my $to_column = $references->{COLUMN}; - my $fk_name = $self->_bz_schema->_get_fk_name($table, - $column, - $references); - # bz_rename_table didn't rename the trigger correctly. - if ($table eq 'bug_tag' && $to_table eq 'tags') { - $to_table = 'tag'; - } - if ( $update =~ /CASCADE/i ){ - my $trigger_name = uc($fk_name . "_UC"); - my $exist_trigger = $self->selectcol_arrayref($sth, undef, $trigger_name); - if(@$exist_trigger) { - $self->do("DROP TRIGGER $trigger_name"); - } - - my $tr_str = "CREATE OR REPLACE TRIGGER $trigger_name" - . " AFTER UPDATE OF $to_column ON $to_table " - . " REFERENCING " - . " NEW AS NEW " - . " OLD AS OLD " - . " FOR EACH ROW " - . " BEGIN " - . " UPDATE $table" - . " SET $column = :NEW.$to_column" - . " WHERE $column = :OLD.$to_column;" - . " END $trigger_name;"; - $self->do($tr_str); - } - } + DETERMINISTIC PARALLEL_ENABLE AGGREGATE USING T_GROUP_CONCAT;" + ); + + # Create a WORLD_LEXER named BZ_LEX for multilingual fulltext search + my $lexer = $self->selectcol_arrayref( + "SELECT pre_name FROM CTXSYS.CTX_PREFERENCES WHERE pre_name = ? AND + pre_owner = ?", undef, 'BZ_LEX', uc(Bugzilla->localconfig->{db_user}) + ); + if (!@$lexer) { + $self->do( + "BEGIN CTX_DDL.CREATE_PREFERENCE + ('BZ_LEX', 'WORLD_LEXER'); END;" + ); + } + + $self->SUPER::bz_setup_database(@_); + + my $sth = $self->prepare( + "SELECT OBJECT_NAME FROM USER_OBJECTS WHERE OBJECT_NAME = ?"); + my @tables = $self->bz_table_list_real(); + + foreach my $table (@tables) { + my @columns = $self->bz_table_columns_real($table); + foreach my $column (@columns) { + my $def = $self->bz_column_info($table, $column); + + # bz_add_column() before Bugzilla 4.2.3 didn't handle primary keys + # correctly (bug 731156). We have to add missing sequences and + # triggers ourselves. + if ($def->{TYPE} =~ /SERIAL/i) { + my $sequence = "${table}_${column}_SEQ"; + my $exists = $self->selectrow_array($sth, undef, $sequence); + if (!$exists) { + my @sql = $self->_get_create_seq_ddl($table, $column); + $self->do($_) foreach @sql; } + } + + if ($def->{REFERENCES}) { + my $references = $def->{REFERENCES}; + my $update = $references->{UPDATE} || 'CASCADE'; + my $to_table = $references->{TABLE}; + my $to_column = $references->{COLUMN}; + my $fk_name = $self->_bz_schema->_get_fk_name($table, $column, $references); + + # bz_rename_table didn't rename the trigger correctly. + if ($table eq 'bug_tag' && $to_table eq 'tags') { + $to_table = 'tag'; + } + if ($update =~ /CASCADE/i) { + my $trigger_name = uc($fk_name . "_UC"); + my $exist_trigger = $self->selectcol_arrayref($sth, undef, $trigger_name); + if (@$exist_trigger) { + $self->do("DROP TRIGGER $trigger_name"); + } + + my $tr_str + = "CREATE OR REPLACE TRIGGER $trigger_name" + . " AFTER UPDATE OF $to_column ON $to_table " + . " REFERENCING " + . " NEW AS NEW " + . " OLD AS OLD " + . " FOR EACH ROW " + . " BEGIN " + . " UPDATE $table" + . " SET $column = :NEW.$to_column" + . " WHERE $column = :OLD.$to_column;" + . " END $trigger_name;"; + $self->do($tr_str); + } + } } + } - # Drop the trigger which causes bug 541553 - my $trigger_name = "PRODUCTS_MILESTONEURL"; - my $exist_trigger = $self->selectcol_arrayref($sth, undef, $trigger_name); - if(@$exist_trigger) { - $self->do("DROP TRIGGER $trigger_name"); - } + # Drop the trigger which causes bug 541553 + my $trigger_name = "PRODUCTS_MILESTONEURL"; + my $exist_trigger = $self->selectcol_arrayref($sth, undef, $trigger_name); + if (@$exist_trigger) { + $self->do("DROP TRIGGER $trigger_name"); + } } # These two methods have been copied from Bugzilla::DB::Schema::Oracle. sub _get_create_seq_ddl { - my ($self, $table, $column) = @_; + my ($self, $table, $column) = @_; - my $seq_name = "${table}_${column}_SEQ"; - my $seq_sql = "CREATE SEQUENCE $seq_name INCREMENT BY 1 START WITH 1 " . - "NOMAXVALUE NOCYCLE NOCACHE"; - my $trigger_sql = $self->_get_create_trigger_ddl($table, $column, $seq_name); - return ($seq_sql, $trigger_sql); + my $seq_name = "${table}_${column}_SEQ"; + my $seq_sql = "CREATE SEQUENCE $seq_name INCREMENT BY 1 START WITH 1 " + . "NOMAXVALUE NOCYCLE NOCACHE"; + my $trigger_sql = $self->_get_create_trigger_ddl($table, $column, $seq_name); + return ($seq_sql, $trigger_sql); } sub _get_create_trigger_ddl { - my ($self, $table, $column, $seq_name) = @_; + my ($self, $table, $column, $seq_name) = @_; - my $trigger_sql = "CREATE OR REPLACE TRIGGER ${table}_${column}_TR " - . " BEFORE INSERT ON $table " - . " FOR EACH ROW " - . " BEGIN " - . " SELECT ${seq_name}.NEXTVAL " - . " INTO :NEW.$column FROM DUAL; " - . " END;"; - return $trigger_sql; + my $trigger_sql + = "CREATE OR REPLACE TRIGGER ${table}_${column}_TR " + . " BEFORE INSERT ON $table " + . " FOR EACH ROW " + . " BEGIN " + . " SELECT ${seq_name}.NEXTVAL " + . " INTO :NEW.$column FROM DUAL; " . " END;"; + return $trigger_sql; } ############################################################################ @@ -725,68 +753,69 @@ use strict; use warnings; use parent -norequire, qw(DBI::st); - + sub fetchrow_arrayref { - my $self = shift; - my $ref = $self->SUPER::fetchrow_arrayref(@_); - return undef if !defined $ref; - Bugzilla::DB::Oracle::_fix_arrayref($ref); - return $ref; + my $self = shift; + my $ref = $self->SUPER::fetchrow_arrayref(@_); + return undef if !defined $ref; + Bugzilla::DB::Oracle::_fix_arrayref($ref); + return $ref; } sub fetchrow_array { - my $self = shift; - if ( wantarray ) { - my @row = $self->SUPER::fetchrow_array(@_); - Bugzilla::DB::Oracle::_fix_arrayref(\@row); - return @row; - } else { - my $row = $self->SUPER::fetchrow_array(@_); - $row = Bugzilla::DB::Oracle::_fix_empty($row) if defined $row; - return $row; - } + my $self = shift; + if (wantarray) { + my @row = $self->SUPER::fetchrow_array(@_); + Bugzilla::DB::Oracle::_fix_arrayref(\@row); + return @row; + } + else { + my $row = $self->SUPER::fetchrow_array(@_); + $row = Bugzilla::DB::Oracle::_fix_empty($row) if defined $row; + return $row; + } } sub fetchrow_hashref { - my $self = shift; - my $ref = $self->SUPER::fetchrow_hashref(@_); - return undef if !defined $ref; - Bugzilla::DB::Oracle::_fix_hashref($ref); - return $ref; + my $self = shift; + my $ref = $self->SUPER::fetchrow_hashref(@_); + return undef if !defined $ref; + Bugzilla::DB::Oracle::_fix_hashref($ref); + return $ref; } sub fetchall_arrayref { - my $self = shift; - my $ref = $self->SUPER::fetchall_arrayref(@_); - return undef if !defined $ref; - foreach my $row (@$ref) { - if (ref($row) eq 'ARRAY') { - Bugzilla::DB::Oracle::_fix_arrayref($row); - } - elsif (ref($row) eq 'HASH') { - Bugzilla::DB::Oracle::_fix_hashref($row); - } + my $self = shift; + my $ref = $self->SUPER::fetchall_arrayref(@_); + return undef if !defined $ref; + foreach my $row (@$ref) { + if (ref($row) eq 'ARRAY') { + Bugzilla::DB::Oracle::_fix_arrayref($row); } - return $ref; + elsif (ref($row) eq 'HASH') { + Bugzilla::DB::Oracle::_fix_hashref($row); + } + } + return $ref; } sub fetchall_hashref { - my $self = shift; - my $ref = $self->SUPER::fetchall_hashref(@_); - return undef if !defined $ref; - foreach my $row (values %$ref) { - Bugzilla::DB::Oracle::_fix_hashref($row); - } - return $ref; + my $self = shift; + my $ref = $self->SUPER::fetchall_hashref(@_); + return undef if !defined $ref; + foreach my $row (values %$ref) { + Bugzilla::DB::Oracle::_fix_hashref($row); + } + return $ref; } sub fetch { - my $self = shift; - my $row = $self->SUPER::fetch(@_); - if ($row) { - Bugzilla::DB::Oracle::_fix_arrayref($row); - } - return $row; + my $self = shift; + my $row = $self->SUPER::fetch(@_); + if ($row) { + Bugzilla::DB::Oracle::_fix_arrayref($row); + } + return $row; } 1; diff --git a/Bugzilla/DB/Pg.pm b/Bugzilla/DB/Pg.pm index cbf8d7af1..15a268381 100644 --- a/Bugzilla/DB/Pg.pm +++ b/Bugzilla/DB/Pg.pm @@ -32,215 +32,227 @@ use DBD::Pg; # This module extends the DB interface via inheritance use parent qw(Bugzilla::DB); -use constant BLOB_TYPE => { pg_type => DBD::Pg::PG_BYTEA }; +use constant BLOB_TYPE => {pg_type => DBD::Pg::PG_BYTEA}; sub new { - my ($class, $params) = @_; - my ($user, $pass, $host, $dbname, $port) = - @$params{qw(db_user db_pass db_host db_name db_port)}; + my ($class, $params) = @_; + my ($user, $pass, $host, $dbname, $port) + = @$params{qw(db_user db_pass db_host db_name db_port)}; - # The default database name for PostgreSQL. We have - # to connect to SOME database, even if we have - # no $dbname parameter. - $dbname ||= 'template1'; + # The default database name for PostgreSQL. We have + # to connect to SOME database, even if we have + # no $dbname parameter. + $dbname ||= 'template1'; - # construct the DSN from the parameters we got - my $dsn = "dbi:Pg:dbname=$dbname"; - $dsn .= ";host=$host" if $host; - $dsn .= ";port=$port" if $port; + # construct the DSN from the parameters we got + my $dsn = "dbi:Pg:dbname=$dbname"; + $dsn .= ";host=$host" if $host; + $dsn .= ";port=$port" if $port; - # This stops Pg from printing out lots of "NOTICE" messages when - # creating tables. - $dsn .= ";options='-c client_min_messages=warning'"; + # This stops Pg from printing out lots of "NOTICE" messages when + # creating tables. + $dsn .= ";options='-c client_min_messages=warning'"; - my $attrs = { pg_enable_utf8 => Bugzilla->params->{'utf8'} }; + my $attrs = {pg_enable_utf8 => Bugzilla->params->{'utf8'}}; - my $self = $class->db_new({ dsn => $dsn, user => $user, - pass => $pass, attrs => $attrs }); + my $self = $class->db_new( + {dsn => $dsn, user => $user, pass => $pass, attrs => $attrs}); - # all class local variables stored in DBI derived class needs to have - # a prefix 'private_'. See DBI documentation. - $self->{private_bz_tables_locked} = ""; - # Needed by TheSchwartz - $self->{private_bz_dsn} = $dsn; + # all class local variables stored in DBI derived class needs to have + # a prefix 'private_'. See DBI documentation. + $self->{private_bz_tables_locked} = ""; - bless ($self, $class); + # Needed by TheSchwartz + $self->{private_bz_dsn} = $dsn; - return $self; + bless($self, $class); + + return $self; } # if last_insert_id is supported on PostgreSQL by lowest DBI/DBD version # supported by Bugzilla, this implementation can be removed. sub bz_last_key { - my ($self, $table, $column) = @_; + my ($self, $table, $column) = @_; - my $seq = $table . "_" . $column . "_seq"; - my ($last_insert_id) = $self->selectrow_array("SELECT CURRVAL('$seq')"); + my $seq = $table . "_" . $column . "_seq"; + my ($last_insert_id) = $self->selectrow_array("SELECT CURRVAL('$seq')"); - return $last_insert_id; + return $last_insert_id; } sub sql_group_concat { - my ($self, $text, $separator, $sort, $order_by) = @_; - $sort = 1 if !defined $sort; - $separator = $self->quote(', ') if !defined $separator; - - # PostgreSQL 8.x doesn't support STRING_AGG - if (vers_cmp($self->bz_server_version, 9) < 0) { - my $sql = "ARRAY_ACCUM($text)"; - if ($sort) { - $sql = "ARRAY_SORT($sql)"; - } - return "ARRAY_TO_STRING($sql, $separator)"; - } - - if ($order_by && $text =~ /^DISTINCT\s*(.+)$/i) { - # Since Postgres (quite rightly) doesn't support "SELECT DISTINCT x - # ORDER BY y", we need to sort the list, and then get the unique - # values - return "ARRAY_TO_STRING(ANYARRAY_UNIQ(ARRAY_AGG($1 ORDER BY $order_by)), $separator)"; - } - - # Determine the ORDER BY clause (if any) - if ($order_by) { - $order_by = " ORDER BY $order_by"; - } - elsif ($sort) { - # We don't include the DISTINCT keyword in an order by - $text =~ /^(?:DISTINCT\s*)?(.+)$/i; - $order_by = " ORDER BY $1"; + my ($self, $text, $separator, $sort, $order_by) = @_; + $sort = 1 if !defined $sort; + $separator = $self->quote(', ') if !defined $separator; + + # PostgreSQL 8.x doesn't support STRING_AGG + if (vers_cmp($self->bz_server_version, 9) < 0) { + my $sql = "ARRAY_ACCUM($text)"; + if ($sort) { + $sql = "ARRAY_SORT($sql)"; } - - return "STRING_AGG(${text}::text, $separator${order_by}::text)" + return "ARRAY_TO_STRING($sql, $separator)"; + } + + if ($order_by && $text =~ /^DISTINCT\s*(.+)$/i) { + + # Since Postgres (quite rightly) doesn't support "SELECT DISTINCT x + # ORDER BY y", we need to sort the list, and then get the unique + # values + return + "ARRAY_TO_STRING(ANYARRAY_UNIQ(ARRAY_AGG($1 ORDER BY $order_by)), $separator)"; + } + + # Determine the ORDER BY clause (if any) + if ($order_by) { + $order_by = " ORDER BY $order_by"; + } + elsif ($sort) { + + # We don't include the DISTINCT keyword in an order by + $text =~ /^(?:DISTINCT\s*)?(.+)$/i; + $order_by = " ORDER BY $1"; + } + + return "STRING_AGG(${text}::text, $separator${order_by}::text)"; } sub sql_istring { - my ($self, $string) = @_; + my ($self, $string) = @_; - return "LOWER(${string}::text)"; + return "LOWER(${string}::text)"; } sub sql_position { - my ($self, $fragment, $text) = @_; + my ($self, $fragment, $text) = @_; - return "POSITION(${fragment}::text IN ${text}::text)"; + return "POSITION(${fragment}::text IN ${text}::text)"; } sub sql_like { - my ($self, $fragment, $column, $not) = @_; - $not //= ''; + my ($self, $fragment, $column, $not) = @_; + $not //= ''; - return "${column}::text $not LIKE " . $self->sql_like_escape($fragment) . " ESCAPE '|'"; + return + "${column}::text $not LIKE " + . $self->sql_like_escape($fragment) + . " ESCAPE '|'"; } sub sql_ilike { - my ($self, $fragment, $column, $not) = @_; - $not //= ''; + my ($self, $fragment, $column, $not) = @_; + $not //= ''; - return "${column}::text $not ILIKE " . $self->sql_like_escape($fragment) . " ESCAPE '|'"; + return + "${column}::text $not ILIKE " + . $self->sql_like_escape($fragment) + . " ESCAPE '|'"; } sub sql_not_ilike { - return shift->sql_ilike(@_, 'NOT'); + return shift->sql_ilike(@_, 'NOT'); } # Escapes any % or _ characters which are special in a LIKE match. # Also performs a $dbh->quote to escape any quote characters. sub sql_like_escape { - my ($self, $fragment) = @_; + my ($self, $fragment) = @_; - $fragment =~ s/\|/\|\|/g; # escape the escape character if it appears - $fragment =~ s/%/\|%/g; # percent and underscore are the special match - $fragment =~ s/_/\|_/g; # characters in SQL. + $fragment =~ s/\|/\|\|/g; # escape the escape character if it appears + $fragment =~ s/%/\|%/g; # percent and underscore are the special match + $fragment =~ s/_/\|_/g; # characters in SQL. - return $self->quote("%$fragment%"); + return $self->quote("%$fragment%"); } sub sql_regexp { - my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_; - $real_pattern ||= $pattern; + my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_; + $real_pattern ||= $pattern; - $self->bz_check_regexp($real_pattern) if !$nocheck; + $self->bz_check_regexp($real_pattern) if !$nocheck; - return "${expr}::text ~* $pattern"; + return "${expr}::text ~* $pattern"; } sub sql_not_regexp { - my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_; - $real_pattern ||= $pattern; + my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_; + $real_pattern ||= $pattern; - $self->bz_check_regexp($real_pattern) if !$nocheck; + $self->bz_check_regexp($real_pattern) if !$nocheck; - return "${expr}::text !~* $pattern" + return "${expr}::text !~* $pattern"; } sub sql_limit { - my ($self, $limit, $offset) = @_; - - if (defined($offset)) { - return "LIMIT $limit OFFSET $offset"; - } else { - return "LIMIT $limit"; - } + my ($self, $limit, $offset) = @_; + + if (defined($offset)) { + return "LIMIT $limit OFFSET $offset"; + } + else { + return "LIMIT $limit"; + } } sub sql_from_days { - my ($self, $days) = @_; + my ($self, $days) = @_; - return "TO_TIMESTAMP('$days', 'J')::date"; + return "TO_TIMESTAMP('$days', 'J')::date"; } sub sql_to_days { - my ($self, $date) = @_; + my ($self, $date) = @_; - return "TO_CHAR(${date}::date, 'J')::int"; + return "TO_CHAR(${date}::date, 'J')::int"; } sub sql_date_format { - my ($self, $date, $format) = @_; - - $format = "%Y.%m.%d %H:%i:%s" if !$format; - - $format =~ s/\%Y/YYYY/g; - $format =~ s/\%y/YY/g; - $format =~ s/\%m/MM/g; - $format =~ s/\%d/DD/g; - $format =~ s/\%a/Dy/g; - $format =~ s/\%H/HH24/g; - $format =~ s/\%i/MI/g; - $format =~ s/\%s/SS/g; - - return "TO_CHAR($date, " . $self->quote($format) . ")"; + my ($self, $date, $format) = @_; + + $format = "%Y.%m.%d %H:%i:%s" if !$format; + + $format =~ s/\%Y/YYYY/g; + $format =~ s/\%y/YY/g; + $format =~ s/\%m/MM/g; + $format =~ s/\%d/DD/g; + $format =~ s/\%a/Dy/g; + $format =~ s/\%H/HH24/g; + $format =~ s/\%i/MI/g; + $format =~ s/\%s/SS/g; + + return "TO_CHAR($date, " . $self->quote($format) . ")"; } sub sql_date_math { - my ($self, $date, $operator, $interval, $units) = @_; - - return "$date $operator $interval * INTERVAL '1 $units'"; + my ($self, $date, $operator, $interval, $units) = @_; + + return "$date $operator $interval * INTERVAL '1 $units'"; } sub sql_string_concat { - my ($self, @params) = @_; - - # Postgres 7.3 does not support concatenating of different types, so we - # need to cast both parameters to text. Version 7.4 seems to handle this - # properly, so when we stop support 7.3, this can be removed. - return '(CAST(' . join(' AS text) || CAST(', @params) . ' AS text))'; + my ($self, @params) = @_; + + # Postgres 7.3 does not support concatenating of different types, so we + # need to cast both parameters to text. Version 7.4 seems to handle this + # properly, so when we stop support 7.3, this can be removed. + return '(CAST(' . join(' AS text) || CAST(', @params) . ' AS text))'; } # Tell us whether or not a particular sequence exists in the DB. sub bz_sequence_exists { - my ($self, $seq_name) = @_; - my $exists = $self->selectrow_array( - 'SELECT 1 FROM pg_statio_user_sequences WHERE relname = ?', - undef, $seq_name); - return $exists || 0; + my ($self, $seq_name) = @_; + my $exists + = $self->selectrow_array( + 'SELECT 1 FROM pg_statio_user_sequences WHERE relname = ?', + undef, $seq_name); + return $exists || 0; } sub bz_explain { - my ($self, $sql) = @_; - my $explain = $self->selectcol_arrayref("EXPLAIN ANALYZE $sql"); - return join("\n", @$explain); + my ($self, $sql) = @_; + my $explain = $self->selectcol_arrayref("EXPLAIN ANALYZE $sql"); + return join("\n", @$explain); } ##################################################################### @@ -248,42 +260,49 @@ sub bz_explain { ##################################################################### sub bz_check_server_version { - my $self = shift; - my ($db) = @_; - my $server_version = $self->SUPER::bz_check_server_version(@_); - my ($major_version, $minor_version) = $server_version =~ /^0*(\d+)\.0*(\d+)/; - # Pg 9.0 requires DBD::Pg 2.17.2 in order to properly read bytea values. - # Pg 9.2 requires DBD::Pg 2.19.3 as spclocation no longer exists. - if ($major_version >= 9) { - local $db->{dbd}->{version} = ($minor_version >= 2) ? '2.19.3' : '2.17.2'; - local $db->{name} = $db->{name} . " ${major_version}.$minor_version"; - Bugzilla::DB::_bz_check_dbd(@_); - } + my $self = shift; + my ($db) = @_; + my $server_version = $self->SUPER::bz_check_server_version(@_); + my ($major_version, $minor_version) = $server_version =~ /^0*(\d+)\.0*(\d+)/; + + # Pg 9.0 requires DBD::Pg 2.17.2 in order to properly read bytea values. + # Pg 9.2 requires DBD::Pg 2.19.3 as spclocation no longer exists. + if ($major_version >= 9) { + local $db->{dbd}->{version} = ($minor_version >= 2) ? '2.19.3' : '2.17.2'; + local $db->{name} = $db->{name} . " ${major_version}.$minor_version"; + Bugzilla::DB::_bz_check_dbd(@_); + } } sub bz_setup_database { - my $self = shift; - $self->SUPER::bz_setup_database(@_); - - my ($has_plpgsql) = $self->selectrow_array("SELECT COUNT(*) FROM pg_language WHERE lanname = 'plpgsql'"); - $self->do('CREATE LANGUAGE plpgsql') unless $has_plpgsql; - - if (vers_cmp($self->bz_server_version, 9) < 0) { - # Custom Functions for Postgres 8 - my $function = 'array_accum'; - my $array_accum = $self->selectrow_array( - 'SELECT 1 FROM pg_proc WHERE proname = ?', undef, $function); - if (!$array_accum) { - print "Creating function $function...\n"; - $self->do("CREATE AGGREGATE array_accum ( + my $self = shift; + $self->SUPER::bz_setup_database(@_); + + my ($has_plpgsql) + = $self->selectrow_array( + "SELECT COUNT(*) FROM pg_language WHERE lanname = 'plpgsql'"); + $self->do('CREATE LANGUAGE plpgsql') unless $has_plpgsql; + + if (vers_cmp($self->bz_server_version, 9) < 0) { + + # Custom Functions for Postgres 8 + my $function = 'array_accum'; + my $array_accum + = $self->selectrow_array('SELECT 1 FROM pg_proc WHERE proname = ?', + undef, $function); + if (!$array_accum) { + print "Creating function $function...\n"; + $self->do( + "CREATE AGGREGATE array_accum ( SFUNC = array_append, BASETYPE = anyelement, STYPE = anyarray, INITCOND = '{}' - )"); - } + )" + ); + } - $self->do(<<'END'); + $self->do(<<'END'); CREATE OR REPLACE FUNCTION array_sort(ANYARRAY) RETURNS ANYARRAY LANGUAGE SQL IMMUTABLE STRICT @@ -296,31 +315,32 @@ SELECT ARRAY( ); $$; END - } - else { - # Custom functions for Postgres 9.0+ - - # -Copyright © 2013 Joshua D. Burns (JDBurnZ) and Message In Action LLC - # JDBurnZ: https://github.com/JDBurnZ - # Message In Action: https://www.messageinaction.com - # - #Permission is hereby granted, free of charge, to any person obtaining a copy of - #this software and associated documentation files (the "Software"), to deal in - #the Software without restriction, including without limitation the rights to - #use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of - #the Software, and to permit persons to whom the Software is furnished to do so, - #subject to the following conditions: - # - #The above copyright notice and this permission notice shall be included in all - #copies or substantial portions of the Software. - # - #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - #FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - #COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER - #IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - #CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - $self->do(q| + } + else { + # Custom functions for Postgres 9.0+ + + # -Copyright © 2013 Joshua D. Burns (JDBurnZ) and Message In Action LLC + # JDBurnZ: https://github.com/JDBurnZ + # Message In Action: https://www.messageinaction.com + # + #Permission is hereby granted, free of charge, to any person obtaining a copy of + #this software and associated documentation files (the "Software"), to deal in + #the Software without restriction, including without limitation the rights to + #use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + #the Software, and to permit persons to whom the Software is furnished to do so, + #subject to the following conditions: + # + #The above copyright notice and this permission notice shall be included in all + #copies or substantial portions of the Software. + # + #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + #FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + #COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + #IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + #CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + $self->do( + q| DROP FUNCTION IF EXISTS anyarray_uniq(anyarray); CREATE OR REPLACE FUNCTION anyarray_uniq(with_array anyarray) RETURNS anyarray AS $BODY$ @@ -345,135 +365,152 @@ END RETURN return_array; END; $BODY$ LANGUAGE plpgsql; - |); + | + ); + } + + # PostgreSQL doesn't like having *any* index on the thetext + # field, because it can't have index data longer than 2770 + # characters on that field. + $self->bz_drop_index('longdescs', 'longdescs_thetext_idx'); + + # Same for all the comments fields in the fulltext table. + $self->bz_drop_index('bugs_fulltext', 'bugs_fulltext_comments_idx'); + $self->bz_drop_index('bugs_fulltext', 'bugs_fulltext_comments_noprivate_idx'); + + # PostgreSQL also wants an index for calling LOWER on + # login_name, which we do with sql_istrcmp all over the place. + $self->bz_add_index( + 'profiles', + 'profiles_login_name_lower_idx', + {FIELDS => ['LOWER(login_name)'], TYPE => 'UNIQUE'} + ); + + # Now that Bugzilla::Object uses sql_istrcmp, other tables + # also need a LOWER() index. + _fix_case_differences('fielddefs', 'name'); + $self->bz_add_index('fielddefs', 'fielddefs_name_lower_idx', + {FIELDS => ['LOWER(name)'], TYPE => 'UNIQUE'}); + _fix_case_differences('keyworddefs', 'name'); + $self->bz_add_index('keyworddefs', 'keyworddefs_name_lower_idx', + {FIELDS => ['LOWER(name)'], TYPE => 'UNIQUE'}); + _fix_case_differences('products', 'name'); + $self->bz_add_index('products', 'products_name_lower_idx', + {FIELDS => ['LOWER(name)'], TYPE => 'UNIQUE'}); + + # bz_rename_column and bz_rename_table didn't correctly rename + # the sequence. + $self->_fix_bad_sequence('fielddefs', 'id', 'fielddefs_fieldid_seq', + 'fielddefs_id_seq'); + + # If the 'tags' table still exists, then bz_rename_table() + # will fix the sequence for us. + if (!$self->bz_table_info('tags')) { + my $res = $self->_fix_bad_sequence('tag', 'id', 'tags_id_seq', 'tag_id_seq'); + + # If $res is true, then the sequence has been renamed, meaning that + # the primary key must be renamed too. + if ($res) { + $self->do('ALTER INDEX tags_pkey RENAME TO tag_pkey'); } - - # PostgreSQL doesn't like having *any* index on the thetext - # field, because it can't have index data longer than 2770 - # characters on that field. - $self->bz_drop_index('longdescs', 'longdescs_thetext_idx'); - # Same for all the comments fields in the fulltext table. - $self->bz_drop_index('bugs_fulltext', 'bugs_fulltext_comments_idx'); - $self->bz_drop_index('bugs_fulltext', - 'bugs_fulltext_comments_noprivate_idx'); - - # PostgreSQL also wants an index for calling LOWER on - # login_name, which we do with sql_istrcmp all over the place. - $self->bz_add_index('profiles', 'profiles_login_name_lower_idx', - {FIELDS => ['LOWER(login_name)'], TYPE => 'UNIQUE'}); - - # Now that Bugzilla::Object uses sql_istrcmp, other tables - # also need a LOWER() index. - _fix_case_differences('fielddefs', 'name'); - $self->bz_add_index('fielddefs', 'fielddefs_name_lower_idx', - {FIELDS => ['LOWER(name)'], TYPE => 'UNIQUE'}); - _fix_case_differences('keyworddefs', 'name'); - $self->bz_add_index('keyworddefs', 'keyworddefs_name_lower_idx', - {FIELDS => ['LOWER(name)'], TYPE => 'UNIQUE'}); - _fix_case_differences('products', 'name'); - $self->bz_add_index('products', 'products_name_lower_idx', - {FIELDS => ['LOWER(name)'], TYPE => 'UNIQUE'}); - - # bz_rename_column and bz_rename_table didn't correctly rename - # the sequence. - $self->_fix_bad_sequence('fielddefs', 'id', 'fielddefs_fieldid_seq', 'fielddefs_id_seq'); - # If the 'tags' table still exists, then bz_rename_table() - # will fix the sequence for us. - if (!$self->bz_table_info('tags')) { - my $res = $self->_fix_bad_sequence('tag', 'id', 'tags_id_seq', 'tag_id_seq'); - # If $res is true, then the sequence has been renamed, meaning that - # the primary key must be renamed too. - if ($res) { - $self->do('ALTER INDEX tags_pkey RENAME TO tag_pkey'); - } - } - - # Certain sequences got upgraded before we required Pg 8.3, and - # so they were not properly associated with their columns. - my @tables = $self->bz_table_list_real; - foreach my $table (@tables) { - my @columns = $self->bz_table_columns_real($table); - foreach my $column (@columns) { - # All our SERIAL pks have "id" in their name at the end. - next unless $column =~ /id$/; - my $sequence = "${table}_${column}_seq"; - if ($self->bz_sequence_exists($sequence)) { - my $is_associated = $self->selectrow_array( - 'SELECT pg_get_serial_sequence(?,?)', - undef, $table, $column); - next if $is_associated; - print "Fixing $sequence to be associated" - . " with $table.$column...\n"; - $self->do("ALTER SEQUENCE $sequence OWNED BY $table.$column"); - # In order to produce an exactly identical schema to what - # a brand-new checksetup.pl run would produce, we also need - # to re-set the default on this column. - $self->do("ALTER TABLE $table + } + + # Certain sequences got upgraded before we required Pg 8.3, and + # so they were not properly associated with their columns. + my @tables = $self->bz_table_list_real; + foreach my $table (@tables) { + my @columns = $self->bz_table_columns_real($table); + foreach my $column (@columns) { + + # All our SERIAL pks have "id" in their name at the end. + next unless $column =~ /id$/; + my $sequence = "${table}_${column}_seq"; + if ($self->bz_sequence_exists($sequence)) { + my $is_associated = $self->selectrow_array('SELECT pg_get_serial_sequence(?,?)', + undef, $table, $column); + next if $is_associated; + print "Fixing $sequence to be associated" . " with $table.$column...\n"; + $self->do("ALTER SEQUENCE $sequence OWNED BY $table.$column"); + + # In order to produce an exactly identical schema to what + # a brand-new checksetup.pl run would produce, we also need + # to re-set the default on this column. + $self->do( + "ALTER TABLE $table ALTER COLUMN $column - SET DEFAULT nextval('$sequence')"); - } - } + SET DEFAULT nextval('$sequence')" + ); + } } + } } sub _fix_bad_sequence { - my ($self, $table, $column, $old_seq, $new_seq) = @_; - if ($self->bz_column_info($table, $column) - && $self->bz_sequence_exists($old_seq)) - { - print "Fixing $old_seq sequence...\n"; - $self->do("ALTER SEQUENCE $old_seq RENAME TO $new_seq"); - $self->do("ALTER TABLE $table ALTER COLUMN $column - SET DEFAULT NEXTVAL('$new_seq')"); - return 1; - } - return 0; + my ($self, $table, $column, $old_seq, $new_seq) = @_; + if ( $self->bz_column_info($table, $column) + && $self->bz_sequence_exists($old_seq)) + { + print "Fixing $old_seq sequence...\n"; + $self->do("ALTER SEQUENCE $old_seq RENAME TO $new_seq"); + $self->do( + "ALTER TABLE $table ALTER COLUMN $column + SET DEFAULT NEXTVAL('$new_seq')" + ); + return 1; + } + return 0; } # Renames things that differ only in case. sub _fix_case_differences { - my ($table, $field) = @_; - my $dbh = Bugzilla->dbh; - - my $duplicates = $dbh->selectcol_arrayref( - "SELECT DISTINCT LOWER($field) FROM $table - GROUP BY LOWER($field) HAVING COUNT(LOWER($field)) > 1"); - - foreach my $name (@$duplicates) { - my $dups = $dbh->selectcol_arrayref( - "SELECT $field FROM $table WHERE LOWER($field) = ?", - undef, $name); - my $primary = shift @$dups; - foreach my $dup (@$dups) { - my $new_name = "${dup}_"; - # Make sure the new name isn't *also* a duplicate. - while (1) { - last if (!$dbh->selectrow_array( - "SELECT 1 FROM $table WHERE LOWER($field) = ?", - undef, lc($new_name))); - $new_name .= "_"; - } - print "$table '$primary' and '$dup' have names that differ", - " only in case.\nRenaming '$dup' to '$new_name'...\n"; - $dbh->do("UPDATE $table SET $field = ? WHERE $field = ?", - undef, $new_name, $dup); - } + my ($table, $field) = @_; + my $dbh = Bugzilla->dbh; + + my $duplicates = $dbh->selectcol_arrayref( + "SELECT DISTINCT LOWER($field) FROM $table + GROUP BY LOWER($field) HAVING COUNT(LOWER($field)) > 1" + ); + + foreach my $name (@$duplicates) { + my $dups + = $dbh->selectcol_arrayref( + "SELECT $field FROM $table WHERE LOWER($field) = ?", + undef, $name); + my $primary = shift @$dups; + foreach my $dup (@$dups) { + my $new_name = "${dup}_"; + + # Make sure the new name isn't *also* a duplicate. + while (1) { + last + if (!$dbh->selectrow_array( + "SELECT 1 FROM $table WHERE LOWER($field) = ?", + undef, lc($new_name) + )); + $new_name .= "_"; + } + print "$table '$primary' and '$dup' have names that differ", + " only in case.\nRenaming '$dup' to '$new_name'...\n"; + $dbh->do("UPDATE $table SET $field = ? WHERE $field = ?", + undef, $new_name, $dup); } + } } ##################################################################### # Custom Schema Information Functions ##################################################################### -# Pg includes the PostgreSQL system tables in table_list_real, so +# Pg includes the PostgreSQL system tables in table_list_real, so # we need to remove those. sub bz_table_list_real { - my $self = shift; + my $self = shift; + + my @full_table_list = $self->SUPER::bz_table_list_real(@_); - my @full_table_list = $self->SUPER::bz_table_list_real(@_); - # All PostgreSQL system tables start with "pg_" or "sql_" - my @table_list = grep(!/(^pg_)|(^sql_)/, @full_table_list); - return @table_list; + # All PostgreSQL system tables start with "pg_" or "sql_" + my @table_list = grep(!/(^pg_)|(^sql_)/, @full_table_list); + return @table_list; } 1; diff --git a/Bugzilla/DB/Schema.pm b/Bugzilla/DB/Schema.pm index d1c1dc7e9..4fc9ce8e2 100644 --- a/Bugzilla/DB/Schema.pm +++ b/Bugzilla/DB/Schema.pm @@ -29,6 +29,7 @@ use Digest::MD5 qw(md5_hex); use Hash::Util qw(lock_value unlock_hash lock_keys unlock_keys); use List::MoreUtils qw(firstidx natatime); use Safe; + # Historical, needed for SCHEMA_VERSION = '1.00' use Storable qw(dclone freeze thaw); @@ -197,1596 +198,1544 @@ update this column in this table." =cut -use constant SCHEMA_VERSION => 3; -use constant ADD_COLUMN => 'ADD COLUMN'; +use constant SCHEMA_VERSION => 3; +use constant ADD_COLUMN => 'ADD COLUMN'; + # Multiple FKs can be added using ALTER TABLE ADD CONSTRAINT in one # SQL statement. This isn't true for all databases. use constant MULTIPLE_FKS_IN_ALTER => 1; + # This is a reasonable default that's true for both PostgreSQL and MySQL. use constant MAX_IDENTIFIER_LEN => 63; use constant FIELD_TABLE_SCHEMA => { + FIELDS => [ + id => {TYPE => 'SMALLSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + value => {TYPE => 'varchar(64)', NOTNULL => 1}, + sortkey => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0}, + isactive => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}, + visibility_value_id => {TYPE => 'INT2'}, + ], + + # Note that bz_add_field_table should prepend the table name + # to these index names. + INDEXES => [ + value_idx => {FIELDS => ['value'], TYPE => 'UNIQUE'}, + sortkey_idx => ['sortkey', 'value'], + visibility_value_id_idx => ['visibility_value_id'], + ], +}; + +use constant ABSTRACT_SCHEMA => { + + # BUG-RELATED TABLES + # ------------------ + + # General Bug Information + # ----------------------- + bugs => { FIELDS => [ - id => {TYPE => 'SMALLSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - value => {TYPE => 'varchar(64)', NOTNULL => 1}, - sortkey => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0}, - isactive => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'TRUE'}, - visibility_value_id => {TYPE => 'INT2'}, + bug_id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + assigned_to => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid'} + }, + bug_file_loc => {TYPE => 'MEDIUMTEXT', NOTNULL => 1, DEFAULT => "''"}, + bug_severity => {TYPE => 'varchar(64)', NOTNULL => 1}, + bug_status => {TYPE => 'varchar(64)', NOTNULL => 1}, + creation_ts => {TYPE => 'DATETIME'}, + delta_ts => {TYPE => 'DATETIME', NOTNULL => 1}, + short_desc => {TYPE => 'varchar(255)', NOTNULL => 1}, + op_sys => {TYPE => 'varchar(64)', NOTNULL => 1}, + priority => {TYPE => 'varchar(64)', NOTNULL => 1}, + product_id => { + TYPE => 'INT2', + NOTNULL => 1, + REFERENCES => {TABLE => 'products', COLUMN => 'id'} + }, + rep_platform => {TYPE => 'varchar(64)', NOTNULL => 1}, + reporter => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid'} + }, + version => {TYPE => 'varchar(64)', NOTNULL => 1}, + component_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'components', COLUMN => 'id'} + }, + resolution => {TYPE => 'varchar(64)', NOTNULL => 1, DEFAULT => "''"}, + target_milestone => {TYPE => 'varchar(64)', NOTNULL => 1, DEFAULT => "'---'"}, + qa_contact => + {TYPE => 'INT3', REFERENCES => {TABLE => 'profiles', COLUMN => 'userid'}}, + status_whiteboard => {TYPE => 'MEDIUMTEXT', NOTNULL => 1, DEFAULT => "''"}, + lastdiffed => {TYPE => 'DATETIME'}, + everconfirmed => {TYPE => 'BOOLEAN', NOTNULL => 1}, + reporter_accessible => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}, + cclist_accessible => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}, + estimated_time => {TYPE => 'decimal(7,2)', NOTNULL => 1, DEFAULT => '0'}, + remaining_time => {TYPE => 'decimal(7,2)', NOTNULL => 1, DEFAULT => '0'}, + deadline => {TYPE => 'DATETIME'}, ], - # Note that bz_add_field_table should prepend the table name - # to these index names. INDEXES => [ - value_idx => {FIELDS => ['value'], TYPE => 'UNIQUE'}, - sortkey_idx => ['sortkey', 'value'], - visibility_value_id_idx => ['visibility_value_id'], + bugs_assigned_to_idx => ['assigned_to'], + bugs_creation_ts_idx => ['creation_ts'], + bugs_delta_ts_idx => ['delta_ts'], + bugs_bug_severity_idx => ['bug_severity'], + bugs_bug_status_idx => ['bug_status'], + bugs_op_sys_idx => ['op_sys'], + bugs_priority_idx => ['priority'], + bugs_product_id_idx => ['product_id'], + bugs_reporter_idx => ['reporter'], + bugs_version_idx => ['version'], + bugs_component_id_idx => ['component_id'], + bugs_resolution_idx => ['resolution'], + bugs_target_milestone_idx => ['target_milestone'], + bugs_qa_contact_idx => ['qa_contact'], ], -}; + }, -use constant ABSTRACT_SCHEMA => { + bugs_fulltext => { + FIELDS => [ + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + PRIMARYKEY => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + short_desc => {TYPE => 'varchar(255)', NOTNULL => 1}, + + # Comments are stored all together in one column for searching. + # This allows us to examine all comments together when deciding + # the relevance of a bug in fulltext search. + comments => {TYPE => 'LONGTEXT'}, + comments_noprivate => {TYPE => 'LONGTEXT'}, + ], + INDEXES => [ + bugs_fulltext_short_desc_idx => {FIELDS => ['short_desc'], TYPE => 'FULLTEXT'}, + bugs_fulltext_comments_idx => {FIELDS => ['comments'], TYPE => 'FULLTEXT'}, + bugs_fulltext_comments_noprivate_idx => + {FIELDS => ['comments_noprivate'], TYPE => 'FULLTEXT'}, + ], + }, - # BUG-RELATED TABLES - # ------------------ - - # General Bug Information - # ----------------------- - bugs => { - FIELDS => [ - bug_id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - assigned_to => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid'}}, - bug_file_loc => {TYPE => 'MEDIUMTEXT', - NOTNULL => 1, DEFAULT => "''"}, - bug_severity => {TYPE => 'varchar(64)', NOTNULL => 1}, - bug_status => {TYPE => 'varchar(64)', NOTNULL => 1}, - creation_ts => {TYPE => 'DATETIME'}, - delta_ts => {TYPE => 'DATETIME', NOTNULL => 1}, - short_desc => {TYPE => 'varchar(255)', NOTNULL => 1}, - op_sys => {TYPE => 'varchar(64)', NOTNULL => 1}, - priority => {TYPE => 'varchar(64)', NOTNULL => 1}, - product_id => {TYPE => 'INT2', NOTNULL => 1, - REFERENCES => {TABLE => 'products', - COLUMN => 'id'}}, - rep_platform => {TYPE => 'varchar(64)', NOTNULL => 1}, - reporter => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid'}}, - version => {TYPE => 'varchar(64)', NOTNULL => 1}, - component_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'components', - COLUMN => 'id'}}, - resolution => {TYPE => 'varchar(64)', - NOTNULL => 1, DEFAULT => "''"}, - target_milestone => {TYPE => 'varchar(64)', - NOTNULL => 1, DEFAULT => "'---'"}, - qa_contact => {TYPE => 'INT3', - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid'}}, - status_whiteboard => {TYPE => 'MEDIUMTEXT', NOTNULL => 1, - DEFAULT => "''"}, - lastdiffed => {TYPE => 'DATETIME'}, - everconfirmed => {TYPE => 'BOOLEAN', NOTNULL => 1}, - reporter_accessible => {TYPE => 'BOOLEAN', - NOTNULL => 1, DEFAULT => 'TRUE'}, - cclist_accessible => {TYPE => 'BOOLEAN', - NOTNULL => 1, DEFAULT => 'TRUE'}, - estimated_time => {TYPE => 'decimal(7,2)', - NOTNULL => 1, DEFAULT => '0'}, - remaining_time => {TYPE => 'decimal(7,2)', - NOTNULL => 1, DEFAULT => '0'}, - deadline => {TYPE => 'DATETIME'}, - ], - INDEXES => [ - bugs_assigned_to_idx => ['assigned_to'], - bugs_creation_ts_idx => ['creation_ts'], - bugs_delta_ts_idx => ['delta_ts'], - bugs_bug_severity_idx => ['bug_severity'], - bugs_bug_status_idx => ['bug_status'], - bugs_op_sys_idx => ['op_sys'], - bugs_priority_idx => ['priority'], - bugs_product_id_idx => ['product_id'], - bugs_reporter_idx => ['reporter'], - bugs_version_idx => ['version'], - bugs_component_id_idx => ['component_id'], - bugs_resolution_idx => ['resolution'], - bugs_target_milestone_idx => ['target_milestone'], - bugs_qa_contact_idx => ['qa_contact'], - ], - }, - - bugs_fulltext => { - FIELDS => [ - bug_id => {TYPE => 'INT3', NOTNULL => 1, PRIMARYKEY => 1, - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - short_desc => {TYPE => 'varchar(255)', NOTNULL => 1}, - # Comments are stored all together in one column for searching. - # This allows us to examine all comments together when deciding - # the relevance of a bug in fulltext search. - comments => {TYPE => 'LONGTEXT'}, - comments_noprivate => {TYPE => 'LONGTEXT'}, - ], - INDEXES => [ - bugs_fulltext_short_desc_idx => {FIELDS => ['short_desc'], - TYPE => 'FULLTEXT'}, - bugs_fulltext_comments_idx => {FIELDS => ['comments'], - TYPE => 'FULLTEXT'}, - bugs_fulltext_comments_noprivate_idx => { - FIELDS => ['comments_noprivate'], TYPE => 'FULLTEXT'}, - ], - }, - - bugs_activity => { - FIELDS => [ - id => {TYPE => 'INTSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - bug_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - attach_id => {TYPE => 'INT3', - REFERENCES => {TABLE => 'attachments', - COLUMN => 'attach_id', - DELETE => 'CASCADE'}}, - who => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid'}}, - bug_when => {TYPE => 'DATETIME', NOTNULL => 1}, - fieldid => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'fielddefs', - COLUMN => 'id'}}, - added => {TYPE => 'varchar(255)'}, - removed => {TYPE => 'varchar(255)'}, - comment_id => {TYPE => 'INT4', - REFERENCES => { TABLE => 'longdescs', - COLUMN => 'comment_id', - DELETE => 'CASCADE'}}, - ], - INDEXES => [ - bugs_activity_bug_id_idx => ['bug_id'], - bugs_activity_who_idx => ['who'], - bugs_activity_bug_when_idx => ['bug_when'], - bugs_activity_fieldid_idx => ['fieldid'], - bugs_activity_added_idx => ['added'], - bugs_activity_removed_idx => ['removed'], - ], - }, - - bugs_aliases => { - FIELDS => [ - alias => {TYPE => 'varchar(40)', NOTNULL => 1}, - bug_id => {TYPE => 'INT3', - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - ], - INDEXES => [ - bugs_aliases_bug_id_idx => ['bug_id'], - bugs_aliases_alias_idx => {FIELDS => ['alias'], - TYPE => 'UNIQUE'}, - ], - }, - - cc => { - FIELDS => [ - bug_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - who => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - ], - INDEXES => [ - cc_bug_id_idx => {FIELDS => [qw(bug_id who)], - TYPE => 'UNIQUE'}, - cc_who_idx => ['who'], - ], - }, - - longdescs => { - FIELDS => [ - comment_id => {TYPE => 'INTSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - bug_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - who => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid'}}, - bug_when => {TYPE => 'DATETIME', NOTNULL => 1}, - work_time => {TYPE => 'decimal(7,2)', NOTNULL => 1, - DEFAULT => '0'}, - thetext => {TYPE => 'LONGTEXT', NOTNULL => 1}, - isprivate => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - already_wrapped => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - type => {TYPE => 'INT2', NOTNULL => 1, - DEFAULT => '0'}, - extra_data => {TYPE => 'varchar(255)'} - ], - INDEXES => [ - longdescs_bug_id_idx => [qw(bug_id work_time)], - longdescs_who_idx => [qw(who bug_id)], - longdescs_bug_when_idx => ['bug_when'], - ], - }, - - longdescs_tags => { - FIELDS => [ - id => { TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1 }, - comment_id => { TYPE => 'INT4', - REFERENCES => { TABLE => 'longdescs', - COLUMN => 'comment_id', - DELETE => 'CASCADE' }}, - tag => { TYPE => 'varchar(24)', NOTNULL => 1 }, - ], - INDEXES => [ - longdescs_tags_idx => { FIELDS => ['comment_id', 'tag'], TYPE => 'UNIQUE' }, - ], - }, - - longdescs_tags_weights => { - FIELDS => [ - id => { TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1 }, - tag => { TYPE => 'varchar(24)', NOTNULL => 1 }, - weight => { TYPE => 'INT3', NOTNULL => 1 }, - ], - INDEXES => [ - longdescs_tags_weights_tag_idx => { FIELDS => ['tag'], TYPE => 'UNIQUE' }, - ], - }, - - longdescs_tags_activity => { - FIELDS => [ - id => { TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1 }, - bug_id => { TYPE => 'INT3', NOTNULL => 1, - REFERENCES => { TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE' }}, - comment_id => { TYPE => 'INT4', - REFERENCES => { TABLE => 'longdescs', - COLUMN => 'comment_id', - DELETE => 'CASCADE' }}, - who => { TYPE => 'INT3', NOTNULL => 1, - REFERENCES => { TABLE => 'profiles', - COLUMN => 'userid' }}, - bug_when => { TYPE => 'DATETIME', NOTNULL => 1 }, - added => { TYPE => 'varchar(24)' }, - removed => { TYPE => 'varchar(24)' }, - ], - INDEXES => [ - longdescs_tags_activity_bug_id_idx => ['bug_id'], - ], - }, - - dependencies => { - FIELDS => [ - blocked => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - dependson => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - ], - INDEXES => [ - dependencies_blocked_idx => {FIELDS => [qw(blocked dependson)], - TYPE => 'UNIQUE'}, - dependencies_dependson_idx => ['dependson'], - ], - }, - - attachments => { - FIELDS => [ - attach_id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - bug_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - creation_ts => {TYPE => 'DATETIME', NOTNULL => 1}, - modification_time => {TYPE => 'DATETIME', NOTNULL => 1}, - description => {TYPE => 'TINYTEXT', NOTNULL => 1}, - mimetype => {TYPE => 'TINYTEXT', NOTNULL => 1}, - ispatch => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - filename => {TYPE => 'varchar(255)', NOTNULL => 1}, - submitter_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid'}}, - isobsolete => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - isprivate => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - ], - INDEXES => [ - attachments_bug_id_idx => ['bug_id'], - attachments_creation_ts_idx => ['creation_ts'], - attachments_modification_time_idx => ['modification_time'], - attachments_submitter_id_idx => ['submitter_id', 'bug_id'], - ], - }, - attach_data => { - FIELDS => [ - id => {TYPE => 'INT3', NOTNULL => 1, - PRIMARYKEY => 1, - REFERENCES => {TABLE => 'attachments', - COLUMN => 'attach_id', - DELETE => 'CASCADE'}}, - thedata => {TYPE => 'LONGBLOB', NOTNULL => 1}, - ], - }, - - duplicates => { - FIELDS => [ - dupe_of => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - dupe => {TYPE => 'INT3', NOTNULL => 1, - PRIMARYKEY => 1, - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - ], - }, - - bug_see_also => { - FIELDS => [ - id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - bug_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - value => {TYPE => 'varchar(255)', NOTNULL => 1}, - class => {TYPE => 'varchar(255)', NOTNULL => 1, DEFAULT => "''"}, - ], - INDEXES => [ - bug_see_also_bug_id_idx => {FIELDS => [qw(bug_id value)], - TYPE => 'UNIQUE'}, - ], - }, - - # Auditing - # -------- - - audit_log => { - FIELDS => [ - user_id => {TYPE => 'INT3', - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'SET NULL'}}, - class => {TYPE => 'varchar(255)', NOTNULL => 1}, - object_id => {TYPE => 'INT4', NOTNULL => 1}, - field => {TYPE => 'varchar(64)', NOTNULL => 1}, - removed => {TYPE => 'MEDIUMTEXT'}, - added => {TYPE => 'MEDIUMTEXT'}, - at_time => {TYPE => 'DATETIME', NOTNULL => 1}, - ], - INDEXES => [ - audit_log_class_idx => ['class', 'at_time'], - ], - }, - - # Keywords - # -------- - - keyworddefs => { - FIELDS => [ - id => {TYPE => 'SMALLSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - name => {TYPE => 'varchar(64)', NOTNULL => 1}, - description => {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, - ], - INDEXES => [ - keyworddefs_name_idx => {FIELDS => ['name'], - TYPE => 'UNIQUE'}, - ], - }, - - keywords => { - FIELDS => [ - bug_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - keywordid => {TYPE => 'INT2', NOTNULL => 1, - REFERENCES => {TABLE => 'keyworddefs', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - - ], - INDEXES => [ - keywords_bug_id_idx => {FIELDS => [qw(bug_id keywordid)], - TYPE => 'UNIQUE'}, - keywords_keywordid_idx => ['keywordid'], - ], - }, - - # Flags - # ----- - - # "flags" stores one record for each flag on each bug/attachment. - flags => { - FIELDS => [ - id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - type_id => {TYPE => 'INT2', NOTNULL => 1, - REFERENCES => {TABLE => 'flagtypes', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - status => {TYPE => 'char(1)', NOTNULL => 1}, - bug_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - attach_id => {TYPE => 'INT3', - REFERENCES => {TABLE => 'attachments', - COLUMN => 'attach_id', - DELETE => 'CASCADE'}}, - creation_date => {TYPE => 'DATETIME', NOTNULL => 1}, - modification_date => {TYPE => 'DATETIME'}, - setter_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid'}}, - requestee_id => {TYPE => 'INT3', - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid'}}, - ], - INDEXES => [ - flags_bug_id_idx => [qw(bug_id attach_id)], - flags_setter_id_idx => ['setter_id'], - flags_requestee_id_idx => ['requestee_id'], - flags_type_id_idx => ['type_id'], - ], - }, - - # "flagtypes" defines the types of flags that can be set. - flagtypes => { - FIELDS => [ - id => {TYPE => 'SMALLSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - name => {TYPE => 'varchar(50)', NOTNULL => 1}, - description => {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, - cc_list => {TYPE => 'varchar(200)'}, - target_type => {TYPE => 'char(1)', NOTNULL => 1, - DEFAULT => "'b'"}, - is_active => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'TRUE'}, - is_requestable => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - is_requesteeble => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - is_multiplicable => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - sortkey => {TYPE => 'INT2', NOTNULL => 1, - DEFAULT => '0'}, - grant_group_id => {TYPE => 'INT3', - REFERENCES => {TABLE => 'groups', - COLUMN => 'id', - DELETE => 'SET NULL'}}, - request_group_id => {TYPE => 'INT3', - REFERENCES => {TABLE => 'groups', - COLUMN => 'id', - DELETE => 'SET NULL'}}, - ], - }, - - # "flaginclusions" and "flagexclusions" specify the products/components - # a bug/attachment must belong to in order for flags of a given type - # to be set for them. - flaginclusions => { - FIELDS => [ - type_id => {TYPE => 'INT2', NOTNULL => 1, - REFERENCES => {TABLE => 'flagtypes', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - product_id => {TYPE => 'INT2', - REFERENCES => {TABLE => 'products', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - component_id => {TYPE => 'INT3', - REFERENCES => {TABLE => 'components', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - ], - INDEXES => [ - flaginclusions_type_id_idx => { FIELDS => [qw(type_id product_id component_id)], - TYPE => 'UNIQUE' }, - ], - }, - - flagexclusions => { - FIELDS => [ - type_id => {TYPE => 'INT2', NOTNULL => 1, - REFERENCES => {TABLE => 'flagtypes', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - product_id => {TYPE => 'INT2', - REFERENCES => {TABLE => 'products', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - component_id => {TYPE => 'INT3', - REFERENCES => {TABLE => 'components', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - ], - INDEXES => [ - flagexclusions_type_id_idx => { FIELDS => [qw(type_id product_id component_id)], - TYPE => 'UNIQUE' }, - ], - }, - - # General Field Information - # ------------------------- - - fielddefs => { - FIELDS => [ - id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - name => {TYPE => 'varchar(64)', NOTNULL => 1}, - type => {TYPE => 'INT2', NOTNULL => 1, - DEFAULT => FIELD_TYPE_UNKNOWN}, - custom => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - description => {TYPE => 'TINYTEXT', NOTNULL => 1}, - long_desc => {TYPE => 'varchar(255)', NOTNULL => 1, DEFAULT => "''"}, - mailhead => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - sortkey => {TYPE => 'INT2', NOTNULL => 1}, - obsolete => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - enter_bug => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - buglist => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - visibility_field_id => {TYPE => 'INT3', - REFERENCES => {TABLE => 'fielddefs', - COLUMN => 'id'}}, - value_field_id => {TYPE => 'INT3', - REFERENCES => {TABLE => 'fielddefs', - COLUMN => 'id'}}, - reverse_desc => {TYPE => 'TINYTEXT'}, - is_mandatory => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - is_numeric => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - ], - INDEXES => [ - fielddefs_name_idx => {FIELDS => ['name'], - TYPE => 'UNIQUE'}, - fielddefs_sortkey_idx => ['sortkey'], - fielddefs_value_field_id_idx => ['value_field_id'], - fielddefs_is_mandatory_idx => ['is_mandatory'], - ], - }, - - # Field Visibility Information - # ------------------------- - - field_visibility => { - FIELDS => [ - field_id => {TYPE => 'INT3', - REFERENCES => {TABLE => 'fielddefs', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - value_id => {TYPE => 'INT2', NOTNULL => 1} - ], - INDEXES => [ - field_visibility_field_id_idx => { - FIELDS => [qw(field_id value_id)], - TYPE => 'UNIQUE' - }, - ], - }, - - # Per-product Field Values - # ------------------------ - - versions => { - FIELDS => [ - id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - value => {TYPE => 'varchar(64)', NOTNULL => 1}, - product_id => {TYPE => 'INT2', NOTNULL => 1, - REFERENCES => {TABLE => 'products', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - isactive => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'TRUE'}, - ], - INDEXES => [ - versions_product_id_idx => {FIELDS => [qw(product_id value)], - TYPE => 'UNIQUE'}, - ], - }, - - milestones => { - FIELDS => [ - id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - product_id => {TYPE => 'INT2', NOTNULL => 1, - REFERENCES => {TABLE => 'products', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - value => {TYPE => 'varchar(64)', NOTNULL => 1}, - sortkey => {TYPE => 'INT2', NOTNULL => 1, - DEFAULT => 0}, - isactive => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'TRUE'}, - ], - INDEXES => [ - milestones_product_id_idx => {FIELDS => [qw(product_id value)], - TYPE => 'UNIQUE'}, - ], - }, - - # Global Field Values - # ------------------- - - bug_status => { - FIELDS => [ - @{ dclone(FIELD_TABLE_SCHEMA->{FIELDS}) }, - is_open => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}, - - ], - INDEXES => [ - bug_status_value_idx => {FIELDS => ['value'], - TYPE => 'UNIQUE'}, - bug_status_sortkey_idx => ['sortkey', 'value'], - bug_status_visibility_value_id_idx => ['visibility_value_id'], - ], - }, - - resolution => { - FIELDS => dclone(FIELD_TABLE_SCHEMA->{FIELDS}), - INDEXES => [ - resolution_value_idx => {FIELDS => ['value'], - TYPE => 'UNIQUE'}, - resolution_sortkey_idx => ['sortkey', 'value'], - resolution_visibility_value_id_idx => ['visibility_value_id'], - ], - }, - - bug_severity => { - FIELDS => dclone(FIELD_TABLE_SCHEMA->{FIELDS}), - INDEXES => [ - bug_severity_value_idx => {FIELDS => ['value'], - TYPE => 'UNIQUE'}, - bug_severity_sortkey_idx => ['sortkey', 'value'], - bug_severity_visibility_value_id_idx => ['visibility_value_id'], - ], - }, - - priority => { - FIELDS => dclone(FIELD_TABLE_SCHEMA->{FIELDS}), - INDEXES => [ - priority_value_idx => {FIELDS => ['value'], - TYPE => 'UNIQUE'}, - priority_sortkey_idx => ['sortkey', 'value'], - priority_visibility_value_id_idx => ['visibility_value_id'], - ], - }, - - rep_platform => { - FIELDS => dclone(FIELD_TABLE_SCHEMA->{FIELDS}), - INDEXES => [ - rep_platform_value_idx => {FIELDS => ['value'], - TYPE => 'UNIQUE'}, - rep_platform_sortkey_idx => ['sortkey', 'value'], - rep_platform_visibility_value_id_idx => ['visibility_value_id'], - ], - }, - - op_sys => { - FIELDS => dclone(FIELD_TABLE_SCHEMA->{FIELDS}), - INDEXES => [ - op_sys_value_idx => {FIELDS => ['value'], - TYPE => 'UNIQUE'}, - op_sys_sortkey_idx => ['sortkey', 'value'], - op_sys_visibility_value_id_idx => ['visibility_value_id'], - ], - }, - - status_workflow => { - FIELDS => [ - # On bug creation, there is no old value. - old_status => {TYPE => 'INT2', - REFERENCES => {TABLE => 'bug_status', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - new_status => {TYPE => 'INT2', NOTNULL => 1, - REFERENCES => {TABLE => 'bug_status', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - require_comment => {TYPE => 'INT1', NOTNULL => 1, DEFAULT => 0}, - ], - INDEXES => [ - status_workflow_idx => {FIELDS => ['old_status', 'new_status'], - TYPE => 'UNIQUE'}, - ], - }, - - # USER INFO - # --------- - - # General User Information - # ------------------------ - - profiles => { - FIELDS => [ - userid => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - login_name => {TYPE => 'varchar(255)', NOTNULL => 1}, - cryptpassword => {TYPE => 'varchar(128)'}, - realname => {TYPE => 'varchar(255)', NOTNULL => 1, - DEFAULT => "''"}, - disabledtext => {TYPE => 'MEDIUMTEXT', NOTNULL => 1, - DEFAULT => "''"}, - disable_mail => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - mybugslink => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'TRUE'}, - extern_id => {TYPE => 'varchar(64)'}, - is_enabled => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'TRUE'}, - last_seen_date => {TYPE => 'DATETIME'}, - ], - INDEXES => [ - profiles_login_name_idx => {FIELDS => ['login_name'], - TYPE => 'UNIQUE'}, - profiles_extern_id_idx => {FIELDS => ['extern_id'], - TYPE => 'UNIQUE'} - ], - }, - - profile_search => { - FIELDS => [ - id => {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, - user_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - bug_list => {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, - list_order => {TYPE => 'MEDIUMTEXT'}, - ], - INDEXES => [ - profile_search_user_id_idx => [qw(user_id)], - ], - }, - - profiles_activity => { - FIELDS => [ - id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - userid => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - who => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid'}}, - profiles_when => {TYPE => 'DATETIME', NOTNULL => 1}, - fieldid => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'fielddefs', - COLUMN => 'id'}}, - oldvalue => {TYPE => 'TINYTEXT'}, - newvalue => {TYPE => 'TINYTEXT'}, - ], - INDEXES => [ - profiles_activity_userid_idx => ['userid'], - profiles_activity_profiles_when_idx => ['profiles_when'], - profiles_activity_fieldid_idx => ['fieldid'], - ], - }, - - email_setting => { - FIELDS => [ - user_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - relationship => {TYPE => 'INT1', NOTNULL => 1}, - event => {TYPE => 'INT1', NOTNULL => 1}, - ], - INDEXES => [ - email_setting_user_id_idx => - {FIELDS => [qw(user_id relationship event)], - TYPE => 'UNIQUE'}, - ], - }, - - email_bug_ignore => { - FIELDS => [ - user_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - bug_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - ], - INDEXES => [ - email_bug_ignore_user_id_idx => {FIELDS => [qw(user_id bug_id)], - TYPE => 'UNIQUE'}, - ], - }, - - watch => { - FIELDS => [ - watcher => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - watched => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - ], - INDEXES => [ - watch_watcher_idx => {FIELDS => [qw(watcher watched)], - TYPE => 'UNIQUE'}, - watch_watched_idx => ['watched'], - ], - }, - - namedqueries => { - FIELDS => [ - id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - userid => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - name => {TYPE => 'varchar(64)', NOTNULL => 1}, - query => {TYPE => 'LONGTEXT', NOTNULL => 1}, - ], - INDEXES => [ - namedqueries_userid_idx => {FIELDS => [qw(userid name)], - TYPE => 'UNIQUE'}, - ], - }, - - namedqueries_link_in_footer => { - FIELDS => [ - namedquery_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'namedqueries', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - user_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - ], - INDEXES => [ - namedqueries_link_in_footer_id_idx => {FIELDS => [qw(namedquery_id user_id)], - TYPE => 'UNIQUE'}, - namedqueries_link_in_footer_userid_idx => ['user_id'], - ], - }, - - tag => { - FIELDS => [ - id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, - name => {TYPE => 'varchar(64)', NOTNULL => 1}, - user_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - ], - INDEXES => [ - tag_user_id_idx => {FIELDS => [qw(user_id name)], TYPE => 'UNIQUE'}, - ], - }, - - bug_tag => { - FIELDS => [ - bug_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - tag_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'tag', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - ], - INDEXES => [ - bug_tag_bug_id_idx => {FIELDS => [qw(bug_id tag_id)], TYPE => 'UNIQUE'}, - ], - }, - - reports => { - FIELDS => [ - id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - user_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - name => {TYPE => 'varchar(64)', NOTNULL => 1}, - query => {TYPE => 'LONGTEXT', NOTNULL => 1}, - ], - INDEXES => [ - reports_user_id_idx => {FIELDS => [qw(user_id name)], - TYPE => 'UNIQUE'}, - ], - }, - - component_cc => { - - FIELDS => [ - user_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - component_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'components', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - ], - INDEXES => [ - component_cc_user_id_idx => {FIELDS => [qw(component_id user_id)], - TYPE => 'UNIQUE'}, - ], - }, - - # Authentication - # -------------- - - logincookies => { - FIELDS => [ - cookie => {TYPE => 'varchar(16)', NOTNULL => 1, - PRIMARYKEY => 1}, - userid => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - ipaddr => {TYPE => 'varchar(40)'}, - lastused => {TYPE => 'DATETIME', NOTNULL => 1}, - ], - INDEXES => [ - logincookies_lastused_idx => ['lastused'], - ], - }, - - login_failure => { - FIELDS => [ - user_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - login_time => {TYPE => 'DATETIME', NOTNULL => 1}, - ip_addr => {TYPE => 'varchar(40)', NOTNULL => 1}, - ], - INDEXES => [ - # We do lookups by every item in the table simultaneously, but - # having an index with all three items would be the same size as - # the table. So instead we have an index on just the smallest item, - # to speed lookups. - login_failure_user_id_idx => ['user_id'], - ], - }, - - - # "tokens" stores the tokens users receive when a password or email - # change is requested. Tokens provide an extra measure of security - # for these changes. - tokens => { - FIELDS => [ - userid => {TYPE => 'INT3', REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - issuedate => {TYPE => 'DATETIME', NOTNULL => 1} , - token => {TYPE => 'varchar(16)', NOTNULL => 1, - PRIMARYKEY => 1}, - tokentype => {TYPE => 'varchar(16)', NOTNULL => 1} , - eventdata => {TYPE => 'TINYTEXT'}, - ], - INDEXES => [ - tokens_userid_idx => ['userid'], - ], - }, - - # GROUPS - # ------ - - groups => { - FIELDS => [ - id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - name => {TYPE => 'varchar(255)', NOTNULL => 1}, - description => {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, - isbuggroup => {TYPE => 'BOOLEAN', NOTNULL => 1}, - userregexp => {TYPE => 'TINYTEXT', NOTNULL => 1, - DEFAULT => "''"}, - isactive => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'TRUE'}, - icon_url => {TYPE => 'TINYTEXT'}, - ], - INDEXES => [ - groups_name_idx => {FIELDS => ['name'], TYPE => 'UNIQUE'}, - ], - }, - - group_control_map => { - FIELDS => [ - group_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'groups', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - product_id => {TYPE => 'INT2', NOTNULL => 1, - REFERENCES => {TABLE => 'products', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - entry => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - membercontrol => {TYPE => 'INT1', NOTNULL => 1, - DEFAULT => CONTROLMAPNA}, - othercontrol => {TYPE => 'INT1', NOTNULL => 1, - DEFAULT => CONTROLMAPNA}, - canedit => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - editcomponents => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - editbugs => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - canconfirm => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - ], - INDEXES => [ - group_control_map_product_id_idx => - {FIELDS => [qw(product_id group_id)], TYPE => 'UNIQUE'}, - group_control_map_group_id_idx => ['group_id'], - ], - }, - - # "user_group_map" determines the groups that a user belongs to - # directly or due to regexp and which groups can be blessed by a user. - # - # grant_type: - # if GRANT_DIRECT - record was explicitly granted - # if GRANT_DERIVED - record was derived from expanding a group hierarchy - # if GRANT_REGEXP - record was created by evaluating a regexp - user_group_map => { - FIELDS => [ - user_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - group_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'groups', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - isbless => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - grant_type => {TYPE => 'INT1', NOTNULL => 1, - DEFAULT => GRANT_DIRECT}, - ], - INDEXES => [ - user_group_map_user_id_idx => - {FIELDS => [qw(user_id group_id grant_type isbless)], - TYPE => 'UNIQUE'}, - ], - }, - - # This table determines which groups are made a member of another - # group, given the ability to bless another group, or given - # visibility to another groups existence and membership - # grant_type: - # if GROUP_MEMBERSHIP - member groups are made members of grantor - # if GROUP_BLESS - member groups may grant membership in grantor - # if GROUP_VISIBLE - member groups may see grantor group - group_group_map => { - FIELDS => [ - member_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'groups', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - grantor_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'groups', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - grant_type => {TYPE => 'INT1', NOTNULL => 1, - DEFAULT => GROUP_MEMBERSHIP}, - ], - INDEXES => [ - group_group_map_member_id_idx => - {FIELDS => [qw(member_id grantor_id grant_type)], - TYPE => 'UNIQUE'}, - ], - }, - - # This table determines which groups a user must be a member of - # in order to see a bug. - bug_group_map => { - FIELDS => [ - bug_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - group_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'groups', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - ], - INDEXES => [ - bug_group_map_bug_id_idx => - {FIELDS => [qw(bug_id group_id)], TYPE => 'UNIQUE'}, - bug_group_map_group_id_idx => ['group_id'], - ], - }, - - # This table determines which groups a user must be a member of - # in order to see a named query somebody else shares. - namedquery_group_map => { - FIELDS => [ - namedquery_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'namedqueries', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - group_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'groups', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - ], - INDEXES => [ - namedquery_group_map_namedquery_id_idx => - {FIELDS => [qw(namedquery_id)], TYPE => 'UNIQUE'}, - namedquery_group_map_group_id_idx => ['group_id'], - ], - }, - - category_group_map => { - FIELDS => [ - category_id => {TYPE => 'INT2', NOTNULL => 1, - REFERENCES => {TABLE => 'series_categories', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - group_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'groups', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - ], - INDEXES => [ - category_group_map_category_id_idx => - {FIELDS => [qw(category_id group_id)], TYPE => 'UNIQUE'}, - ], - }, - - - # PRODUCTS - # -------- - - classifications => { - FIELDS => [ - id => {TYPE => 'SMALLSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - name => {TYPE => 'varchar(64)', NOTNULL => 1}, - description => {TYPE => 'MEDIUMTEXT'}, - sortkey => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '0'}, - ], - INDEXES => [ - classifications_name_idx => {FIELDS => ['name'], - TYPE => 'UNIQUE'}, - ], - }, - - products => { - FIELDS => [ - id => {TYPE => 'SMALLSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - name => {TYPE => 'varchar(64)', NOTNULL => 1}, - classification_id => {TYPE => 'INT2', NOTNULL => 1, - DEFAULT => '1', - REFERENCES => {TABLE => 'classifications', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - description => {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, - isactive => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 1}, - defaultmilestone => {TYPE => 'varchar(64)', - NOTNULL => 1, DEFAULT => "'---'"}, - allows_unconfirmed => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'TRUE'}, - ], - INDEXES => [ - products_name_idx => {FIELDS => ['name'], - TYPE => 'UNIQUE'}, - ], - }, - - components => { - FIELDS => [ - id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - name => {TYPE => 'varchar(64)', NOTNULL => 1}, - product_id => {TYPE => 'INT2', NOTNULL => 1, - REFERENCES => {TABLE => 'products', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - initialowner => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid'}}, - initialqacontact => {TYPE => 'INT3', - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'SET NULL'}}, - description => {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, - isactive => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'TRUE'}, - ], - INDEXES => [ - components_product_id_idx => {FIELDS => [qw(product_id name)], - TYPE => 'UNIQUE'}, - components_name_idx => ['name'], - ], - }, - - # CHARTS - # ------ - - series => { - FIELDS => [ - series_id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - creator => {TYPE => 'INT3', - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - category => {TYPE => 'INT2', NOTNULL => 1, - REFERENCES => {TABLE => 'series_categories', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - subcategory => {TYPE => 'INT2', NOTNULL => 1, - REFERENCES => {TABLE => 'series_categories', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - name => {TYPE => 'varchar(64)', NOTNULL => 1}, - frequency => {TYPE => 'INT2', NOTNULL => 1}, - query => {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, - is_public => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - ], - INDEXES => [ - series_creator_idx => ['creator'], - series_category_idx => {FIELDS => [qw(category subcategory name)], - TYPE => 'UNIQUE'}, - ], - }, - - series_data => { - FIELDS => [ - series_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'series', - COLUMN => 'series_id', - DELETE => 'CASCADE'}}, - series_date => {TYPE => 'DATETIME', NOTNULL => 1}, - series_value => {TYPE => 'INT3', NOTNULL => 1}, - ], - INDEXES => [ - series_data_series_id_idx => - {FIELDS => [qw(series_id series_date)], - TYPE => 'UNIQUE'}, - ], - }, - - series_categories => { - FIELDS => [ - id => {TYPE => 'SMALLSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - name => {TYPE => 'varchar(64)', NOTNULL => 1}, - ], - INDEXES => [ - series_categories_name_idx => {FIELDS => ['name'], - TYPE => 'UNIQUE'}, - ], - }, - - # WHINE SYSTEM - # ------------ - - whine_queries => { - FIELDS => [ - id => {TYPE => 'MEDIUMSERIAL', PRIMARYKEY => 1, - NOTNULL => 1}, - eventid => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'whine_events', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - query_name => {TYPE => 'varchar(64)', NOTNULL => 1, - DEFAULT => "''"}, - sortkey => {TYPE => 'INT2', NOTNULL => 1, - DEFAULT => '0'}, - onemailperbug => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - title => {TYPE => 'varchar(128)', NOTNULL => 1, - DEFAULT => "''"}, - ], - INDEXES => [ - whine_queries_eventid_idx => ['eventid'], - ], - }, - - whine_schedules => { - FIELDS => [ - id => {TYPE => 'MEDIUMSERIAL', PRIMARYKEY => 1, - NOTNULL => 1}, - eventid => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'whine_events', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - run_day => {TYPE => 'varchar(32)'}, - run_time => {TYPE => 'varchar(32)'}, - run_next => {TYPE => 'DATETIME'}, - mailto => {TYPE => 'INT3', NOTNULL => 1}, - mailto_type => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '0'}, - ], - INDEXES => [ - whine_schedules_run_next_idx => ['run_next'], - whine_schedules_eventid_idx => ['eventid'], - ], - }, - - whine_events => { - FIELDS => [ - id => {TYPE => 'MEDIUMSERIAL', PRIMARYKEY => 1, - NOTNULL => 1}, - owner_userid => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - subject => {TYPE => 'varchar(128)'}, - body => {TYPE => 'MEDIUMTEXT'}, - mailifnobugs => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - ], - }, - - # QUIPS - # ----- - - quips => { - FIELDS => [ - quipid => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - userid => {TYPE => 'INT3', - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'SET NULL'}}, - quip => {TYPE => 'varchar(512)', NOTNULL => 1}, - approved => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'TRUE'}, - ], - }, - - # SETTINGS - # -------- - # setting - each global setting will have exactly one entry - # in this table. - # setting_value - stores the list of acceptable values for each - # setting, and a sort index that controls the order - # in which the values are displayed. - # profile_setting - If a user has chosen to use a value other than the - # global default for a given setting, it will be - # stored in this table. Note: even if a setting is - # later changed so is_enabled = false, the stored - # value will remain in case it is ever enabled again. - # - setting => { - FIELDS => [ - name => {TYPE => 'varchar(32)', NOTNULL => 1, - PRIMARYKEY => 1}, - default_value => {TYPE => 'varchar(32)', NOTNULL => 1}, - is_enabled => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'TRUE'}, - subclass => {TYPE => 'varchar(32)'}, - ], - }, - - setting_value => { - FIELDS => [ - name => {TYPE => 'varchar(32)', NOTNULL => 1, - REFERENCES => {TABLE => 'setting', - COLUMN => 'name', - DELETE => 'CASCADE'}}, - value => {TYPE => 'varchar(32)', NOTNULL => 1}, - sortindex => {TYPE => 'INT2', NOTNULL => 1}, - ], - INDEXES => [ - setting_value_nv_unique_idx => {FIELDS => [qw(name value)], - TYPE => 'UNIQUE'}, - setting_value_ns_unique_idx => {FIELDS => [qw(name sortindex)], - TYPE => 'UNIQUE'}, - ], - }, - - profile_setting => { - FIELDS => [ - user_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - setting_name => {TYPE => 'varchar(32)', NOTNULL => 1, - REFERENCES => {TABLE => 'setting', - COLUMN => 'name', - DELETE => 'CASCADE'}}, - setting_value => {TYPE => 'varchar(32)', NOTNULL => 1}, - ], - INDEXES => [ - profile_setting_value_unique_idx => {FIELDS => [qw(user_id setting_name)], - TYPE => 'UNIQUE'}, - ], - }, - - # BUGMAIL - # ------- - - mail_staging => { - FIELDS => [ - id => {TYPE => 'INTSERIAL', PRIMARYKEY => 1, NOTNULL => 1}, - message => {TYPE => 'LONGBLOB', NOTNULL => 1}, - ], - }, - - # THESCHWARTZ TABLES - # ------------------ - # Note: In the standard TheSchwartz schema, most integers are unsigned, - # but we didn't implement unsigned ints for Bugzilla schemas, so we - # just create signed ints, which should be fine. - - ts_funcmap => { - FIELDS => [ - funcid => {TYPE => 'INTSERIAL', PRIMARYKEY => 1, NOTNULL => 1}, - funcname => {TYPE => 'varchar(255)', NOTNULL => 1}, - ], - INDEXES => [ - ts_funcmap_funcname_idx => {FIELDS => ['funcname'], - TYPE => 'UNIQUE'}, - ], - }, - - ts_job => { - FIELDS => [ - # In a standard TheSchwartz schema, this is a BIGINT, but we - # don't have those and I didn't want to add them just for this. - jobid => {TYPE => 'INTSERIAL', PRIMARYKEY => 1, - NOTNULL => 1}, - funcid => {TYPE => 'INT4', NOTNULL => 1}, - # In standard TheSchwartz, this is a MEDIUMBLOB. - arg => {TYPE => 'LONGBLOB'}, - uniqkey => {TYPE => 'varchar(255)'}, - insert_time => {TYPE => 'INT4'}, - run_after => {TYPE => 'INT4', NOTNULL => 1}, - grabbed_until => {TYPE => 'INT4', NOTNULL => 1}, - priority => {TYPE => 'INT2'}, - coalesce => {TYPE => 'varchar(255)'}, - ], - INDEXES => [ - ts_job_funcid_idx => {FIELDS => [qw(funcid uniqkey)], - TYPE => 'UNIQUE'}, - # In a standard TheSchewartz schema, these both go in the other - # direction, but there's no reason to have three indexes that - # all start with the same column, and our naming scheme doesn't - # allow it anyhow. - ts_job_run_after_idx => [qw(run_after funcid)], - ts_job_coalesce_idx => [qw(coalesce funcid)], - ], - }, - - ts_note => { - FIELDS => [ - # This is a BIGINT in standard TheSchwartz schemas. - jobid => {TYPE => 'INT4', NOTNULL => 1}, - notekey => {TYPE => 'varchar(255)'}, - value => {TYPE => 'LONGBLOB'}, - ], - INDEXES => [ - ts_note_jobid_idx => {FIELDS => [qw(jobid notekey)], - TYPE => 'UNIQUE'}, - ], - }, - - ts_error => { - FIELDS => [ - error_time => {TYPE => 'INT4', NOTNULL => 1}, - jobid => {TYPE => 'INT4', NOTNULL => 1}, - message => {TYPE => 'varchar(255)', NOTNULL => 1}, - funcid => {TYPE => 'INT4', NOTNULL => 1, DEFAULT => 0}, - ], - INDEXES => [ - ts_error_funcid_idx => [qw(funcid error_time)], - ts_error_error_time_idx => ['error_time'], - ts_error_jobid_idx => ['jobid'], - ], - }, - - ts_exitstatus => { - FIELDS => [ - jobid => {TYPE => 'INTSERIAL', PRIMARYKEY => 1, - NOTNULL => 1}, - funcid => {TYPE => 'INT4', NOTNULL => 1, DEFAULT => 0}, - status => {TYPE => 'INT2'}, - completion_time => {TYPE => 'INT4'}, - delete_after => {TYPE => 'INT4'}, - ], - INDEXES => [ - ts_exitstatus_funcid_idx => ['funcid'], - ts_exitstatus_delete_after_idx => ['delete_after'], - ], - }, - - # SCHEMA STORAGE - # -------------- - - bz_schema => { - FIELDS => [ - schema_data => {TYPE => 'LONGBLOB', NOTNULL => 1}, - version => {TYPE => 'decimal(3,2)', NOTNULL => 1}, - ], - }, - - bug_user_last_visit => { - FIELDS => [ - id => {TYPE => 'INTSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - user_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - bug_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - last_visit_ts => {TYPE => 'DATETIME', NOTNULL => 1}, - ], - INDEXES => [ - bug_user_last_visit_idx => {FIELDS => ['user_id', 'bug_id'], - TYPE => 'UNIQUE'}, - bug_user_last_visit_last_visit_ts_idx => ['last_visit_ts'], - ], - }, - - user_api_keys => { - FIELDS => [ - id => {TYPE => 'INTSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - user_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - api_key => {TYPE => 'VARCHAR(40)', NOTNULL => 1}, - description => {TYPE => 'VARCHAR(255)'}, - revoked => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - last_used => {TYPE => 'DATETIME'}, - ], - INDEXES => [ - user_api_keys_api_key_idx => {FIELDS => ['api_key'], TYPE => 'UNIQUE'}, - user_api_keys_user_id_idx => ['user_id'], - ], - }, -}; + bugs_activity => { + FIELDS => [ + id => {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + attach_id => { + TYPE => 'INT3', + REFERENCES => + {TABLE => 'attachments', COLUMN => 'attach_id', DELETE => 'CASCADE'} + }, + who => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid'} + }, + bug_when => {TYPE => 'DATETIME', NOTNULL => 1}, + fieldid => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'fielddefs', COLUMN => 'id'} + }, + added => {TYPE => 'varchar(255)'}, + removed => {TYPE => 'varchar(255)'}, + comment_id => { + TYPE => 'INT4', + REFERENCES => + {TABLE => 'longdescs', COLUMN => 'comment_id', DELETE => 'CASCADE'} + }, + ], + INDEXES => [ + bugs_activity_bug_id_idx => ['bug_id'], + bugs_activity_who_idx => ['who'], + bugs_activity_bug_when_idx => ['bug_when'], + bugs_activity_fieldid_idx => ['fieldid'], + bugs_activity_added_idx => ['added'], + bugs_activity_removed_idx => ['removed'], + ], + }, -# Foreign Keys are added in Bugzilla::DB::bz_add_field_tables -use constant MULTI_SELECT_VALUE_TABLE => { + bugs_aliases => { + FIELDS => [ + alias => {TYPE => 'varchar(40)', NOTNULL => 1}, + bug_id => { + TYPE => 'INT3', + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + ], + INDEXES => [ + bugs_aliases_bug_id_idx => ['bug_id'], + bugs_aliases_alias_idx => {FIELDS => ['alias'], TYPE => 'UNIQUE'}, + ], + }, + + cc => { + FIELDS => [ + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + who => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + ], + INDEXES => [ + cc_bug_id_idx => {FIELDS => [qw(bug_id who)], TYPE => 'UNIQUE'}, + cc_who_idx => ['who'], + ], + }, + + longdescs => { + FIELDS => [ + comment_id => {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + who => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid'} + }, + bug_when => {TYPE => 'DATETIME', NOTNULL => 1}, + work_time => {TYPE => 'decimal(7,2)', NOTNULL => 1, DEFAULT => '0'}, + thetext => {TYPE => 'LONGTEXT', NOTNULL => 1}, + isprivate => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + already_wrapped => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + type => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '0'}, + extra_data => {TYPE => 'varchar(255)'} + ], + INDEXES => [ + longdescs_bug_id_idx => [qw(bug_id work_time)], + longdescs_who_idx => [qw(who bug_id)], + longdescs_bug_when_idx => ['bug_when'], + ], + }, + + longdescs_tags => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + comment_id => { + TYPE => 'INT4', + REFERENCES => + {TABLE => 'longdescs', COLUMN => 'comment_id', DELETE => 'CASCADE'} + }, + tag => {TYPE => 'varchar(24)', NOTNULL => 1}, + ], + INDEXES => + [longdescs_tags_idx => {FIELDS => ['comment_id', 'tag'], TYPE => 'UNIQUE'},], + }, + + longdescs_tags_weights => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + tag => {TYPE => 'varchar(24)', NOTNULL => 1}, + weight => {TYPE => 'INT3', NOTNULL => 1}, + ], + INDEXES => + [longdescs_tags_weights_tag_idx => {FIELDS => ['tag'], TYPE => 'UNIQUE'},], + }, + + longdescs_tags_activity => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + comment_id => { + TYPE => 'INT4', + REFERENCES => + {TABLE => 'longdescs', COLUMN => 'comment_id', DELETE => 'CASCADE'} + }, + who => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid'} + }, + bug_when => {TYPE => 'DATETIME', NOTNULL => 1}, + added => {TYPE => 'varchar(24)'}, + removed => {TYPE => 'varchar(24)'}, + ], + INDEXES => [longdescs_tags_activity_bug_id_idx => ['bug_id'],], + }, + + dependencies => { + FIELDS => [ + blocked => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + dependson => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + ], + INDEXES => [ + dependencies_blocked_idx => + {FIELDS => [qw(blocked dependson)], TYPE => 'UNIQUE'}, + dependencies_dependson_idx => ['dependson'], + ], + }, + + attachments => { + FIELDS => [ + attach_id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + creation_ts => {TYPE => 'DATETIME', NOTNULL => 1}, + modification_time => {TYPE => 'DATETIME', NOTNULL => 1}, + description => {TYPE => 'TINYTEXT', NOTNULL => 1}, + mimetype => {TYPE => 'TINYTEXT', NOTNULL => 1}, + ispatch => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + filename => {TYPE => 'varchar(255)', NOTNULL => 1}, + submitter_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid'} + }, + isobsolete => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + isprivate => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + ], + INDEXES => [ + attachments_bug_id_idx => ['bug_id'], + attachments_creation_ts_idx => ['creation_ts'], + attachments_modification_time_idx => ['modification_time'], + attachments_submitter_id_idx => ['submitter_id', 'bug_id'], + ], + }, + attach_data => { FIELDS => [ - bug_id => {TYPE => 'INT3', NOTNULL => 1}, - value => {TYPE => 'varchar(64)', NOTNULL => 1}, + id => { + TYPE => 'INT3', + NOTNULL => 1, + PRIMARYKEY => 1, + REFERENCES => + {TABLE => 'attachments', COLUMN => 'attach_id', DELETE => 'CASCADE'} + }, + thedata => {TYPE => 'LONGBLOB', NOTNULL => 1}, + ], + }, + + duplicates => { + FIELDS => [ + dupe_of => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + dupe => { + TYPE => 'INT3', + NOTNULL => 1, + PRIMARYKEY => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + ], + }, + + bug_see_also => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + value => {TYPE => 'varchar(255)', NOTNULL => 1}, + class => {TYPE => 'varchar(255)', NOTNULL => 1, DEFAULT => "''"}, + ], + INDEXES => [ + bug_see_also_bug_id_idx => {FIELDS => [qw(bug_id value)], TYPE => 'UNIQUE'}, + ], + }, + + # Auditing + # -------- + + audit_log => { + FIELDS => [ + user_id => { + TYPE => 'INT3', + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'SET NULL'} + }, + class => {TYPE => 'varchar(255)', NOTNULL => 1}, + object_id => {TYPE => 'INT4', NOTNULL => 1}, + field => {TYPE => 'varchar(64)', NOTNULL => 1}, + removed => {TYPE => 'MEDIUMTEXT'}, + added => {TYPE => 'MEDIUMTEXT'}, + at_time => {TYPE => 'DATETIME', NOTNULL => 1}, + ], + INDEXES => [audit_log_class_idx => ['class', 'at_time'],], + }, + + # Keywords + # -------- + + keyworddefs => { + FIELDS => [ + id => {TYPE => 'SMALLSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + name => {TYPE => 'varchar(64)', NOTNULL => 1}, + description => {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, + ], + INDEXES => [keyworddefs_name_idx => {FIELDS => ['name'], TYPE => 'UNIQUE'},], + }, + + keywords => { + FIELDS => [ + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + keywordid => { + TYPE => 'INT2', + NOTNULL => 1, + REFERENCES => {TABLE => 'keyworddefs', COLUMN => 'id', DELETE => 'CASCADE'} + }, + + ], + INDEXES => [ + keywords_bug_id_idx => {FIELDS => [qw(bug_id keywordid)], TYPE => 'UNIQUE'}, + keywords_keywordid_idx => ['keywordid'], + ], + }, + + # Flags + # ----- + + # "flags" stores one record for each flag on each bug/attachment. + flags => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + type_id => { + TYPE => 'INT2', + NOTNULL => 1, + REFERENCES => {TABLE => 'flagtypes', COLUMN => 'id', DELETE => 'CASCADE'} + }, + status => {TYPE => 'char(1)', NOTNULL => 1}, + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + attach_id => { + TYPE => 'INT3', + REFERENCES => + {TABLE => 'attachments', COLUMN => 'attach_id', DELETE => 'CASCADE'} + }, + creation_date => {TYPE => 'DATETIME', NOTNULL => 1}, + modification_date => {TYPE => 'DATETIME'}, + setter_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid'} + }, + requestee_id => + {TYPE => 'INT3', REFERENCES => {TABLE => 'profiles', COLUMN => 'userid'}}, + ], + INDEXES => [ + flags_bug_id_idx => [qw(bug_id attach_id)], + flags_setter_id_idx => ['setter_id'], + flags_requestee_id_idx => ['requestee_id'], + flags_type_id_idx => ['type_id'], + ], + }, + + # "flagtypes" defines the types of flags that can be set. + flagtypes => { + FIELDS => [ + id => {TYPE => 'SMALLSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + name => {TYPE => 'varchar(50)', NOTNULL => 1}, + description => {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, + cc_list => {TYPE => 'varchar(200)'}, + target_type => {TYPE => 'char(1)', NOTNULL => 1, DEFAULT => "'b'"}, + is_active => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}, + is_requestable => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + is_requesteeble => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + is_multiplicable => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + sortkey => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '0'}, + grant_group_id => { + TYPE => 'INT3', + REFERENCES => {TABLE => 'groups', COLUMN => 'id', DELETE => 'SET NULL'} + }, + request_group_id => { + TYPE => 'INT3', + REFERENCES => {TABLE => 'groups', COLUMN => 'id', DELETE => 'SET NULL'} + }, + ], + }, + + # "flaginclusions" and "flagexclusions" specify the products/components + # a bug/attachment must belong to in order for flags of a given type + # to be set for them. + flaginclusions => { + FIELDS => [ + type_id => { + TYPE => 'INT2', + NOTNULL => 1, + REFERENCES => {TABLE => 'flagtypes', COLUMN => 'id', DELETE => 'CASCADE'} + }, + product_id => { + TYPE => 'INT2', + REFERENCES => {TABLE => 'products', COLUMN => 'id', DELETE => 'CASCADE'} + }, + component_id => { + TYPE => 'INT3', + REFERENCES => {TABLE => 'components', COLUMN => 'id', DELETE => 'CASCADE'} + }, ], INDEXES => [ - bug_id_idx => {FIELDS => [qw( bug_id value)], TYPE => 'UNIQUE'}, + flaginclusions_type_id_idx => + {FIELDS => [qw(type_id product_id component_id)], TYPE => 'UNIQUE'}, + ], + }, + + flagexclusions => { + FIELDS => [ + type_id => { + TYPE => 'INT2', + NOTNULL => 1, + REFERENCES => {TABLE => 'flagtypes', COLUMN => 'id', DELETE => 'CASCADE'} + }, + product_id => { + TYPE => 'INT2', + REFERENCES => {TABLE => 'products', COLUMN => 'id', DELETE => 'CASCADE'} + }, + component_id => { + TYPE => 'INT3', + REFERENCES => {TABLE => 'components', COLUMN => 'id', DELETE => 'CASCADE'} + }, + ], + INDEXES => [ + flagexclusions_type_id_idx => + {FIELDS => [qw(type_id product_id component_id)], TYPE => 'UNIQUE'}, + ], + }, + + # General Field Information + # ------------------------- + + fielddefs => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + name => {TYPE => 'varchar(64)', NOTNULL => 1}, + type => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => FIELD_TYPE_UNKNOWN}, + custom => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + description => {TYPE => 'TINYTEXT', NOTNULL => 1}, + long_desc => {TYPE => 'varchar(255)', NOTNULL => 1, DEFAULT => "''"}, + mailhead => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + sortkey => {TYPE => 'INT2', NOTNULL => 1}, + obsolete => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + enter_bug => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + buglist => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + visibility_field_id => + {TYPE => 'INT3', REFERENCES => {TABLE => 'fielddefs', COLUMN => 'id'}}, + value_field_id => + {TYPE => 'INT3', REFERENCES => {TABLE => 'fielddefs', COLUMN => 'id'}}, + reverse_desc => {TYPE => 'TINYTEXT'}, + is_mandatory => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + is_numeric => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + ], + INDEXES => [ + fielddefs_name_idx => {FIELDS => ['name'], TYPE => 'UNIQUE'}, + fielddefs_sortkey_idx => ['sortkey'], + fielddefs_value_field_id_idx => ['value_field_id'], + fielddefs_is_mandatory_idx => ['is_mandatory'], + ], + }, + + # Field Visibility Information + # ------------------------- + + field_visibility => { + FIELDS => [ + field_id => { + TYPE => 'INT3', + REFERENCES => {TABLE => 'fielddefs', COLUMN => 'id', DELETE => 'CASCADE'} + }, + value_id => {TYPE => 'INT2', NOTNULL => 1} + ], + INDEXES => [ + field_visibility_field_id_idx => + {FIELDS => [qw(field_id value_id)], TYPE => 'UNIQUE'}, + ], + }, + + # Per-product Field Values + # ------------------------ + + versions => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + value => {TYPE => 'varchar(64)', NOTNULL => 1}, + product_id => { + TYPE => 'INT2', + NOTNULL => 1, + REFERENCES => {TABLE => 'products', COLUMN => 'id', DELETE => 'CASCADE'} + }, + isactive => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}, + ], + INDEXES => [ + versions_product_id_idx => {FIELDS => [qw(product_id value)], TYPE => 'UNIQUE'}, + ], + }, + + milestones => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + product_id => { + TYPE => 'INT2', + NOTNULL => 1, + REFERENCES => {TABLE => 'products', COLUMN => 'id', DELETE => 'CASCADE'} + }, + value => {TYPE => 'varchar(64)', NOTNULL => 1}, + sortkey => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0}, + isactive => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}, + ], + INDEXES => [ + milestones_product_id_idx => + {FIELDS => [qw(product_id value)], TYPE => 'UNIQUE'}, + ], + }, + + # Global Field Values + # ------------------- + + bug_status => { + FIELDS => [ + @{dclone(FIELD_TABLE_SCHEMA->{FIELDS})}, + is_open => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}, + + ], + INDEXES => [ + bug_status_value_idx => {FIELDS => ['value'], TYPE => 'UNIQUE'}, + bug_status_sortkey_idx => ['sortkey', 'value'], + bug_status_visibility_value_id_idx => ['visibility_value_id'], + ], + }, + + resolution => { + FIELDS => dclone(FIELD_TABLE_SCHEMA->{FIELDS}), + INDEXES => [ + resolution_value_idx => {FIELDS => ['value'], TYPE => 'UNIQUE'}, + resolution_sortkey_idx => ['sortkey', 'value'], + resolution_visibility_value_id_idx => ['visibility_value_id'], + ], + }, + + bug_severity => { + FIELDS => dclone(FIELD_TABLE_SCHEMA->{FIELDS}), + INDEXES => [ + bug_severity_value_idx => {FIELDS => ['value'], TYPE => 'UNIQUE'}, + bug_severity_sortkey_idx => ['sortkey', 'value'], + bug_severity_visibility_value_id_idx => ['visibility_value_id'], + ], + }, + + priority => { + FIELDS => dclone(FIELD_TABLE_SCHEMA->{FIELDS}), + INDEXES => [ + priority_value_idx => {FIELDS => ['value'], TYPE => 'UNIQUE'}, + priority_sortkey_idx => ['sortkey', 'value'], + priority_visibility_value_id_idx => ['visibility_value_id'], + ], + }, + + rep_platform => { + FIELDS => dclone(FIELD_TABLE_SCHEMA->{FIELDS}), + INDEXES => [ + rep_platform_value_idx => {FIELDS => ['value'], TYPE => 'UNIQUE'}, + rep_platform_sortkey_idx => ['sortkey', 'value'], + rep_platform_visibility_value_id_idx => ['visibility_value_id'], + ], + }, + + op_sys => { + FIELDS => dclone(FIELD_TABLE_SCHEMA->{FIELDS}), + INDEXES => [ + op_sys_value_idx => {FIELDS => ['value'], TYPE => 'UNIQUE'}, + op_sys_sortkey_idx => ['sortkey', 'value'], + op_sys_visibility_value_id_idx => ['visibility_value_id'], + ], + }, + + status_workflow => { + FIELDS => [ + + # On bug creation, there is no old value. + old_status => { + TYPE => 'INT2', + REFERENCES => {TABLE => 'bug_status', COLUMN => 'id', DELETE => 'CASCADE'} + }, + new_status => { + TYPE => 'INT2', + NOTNULL => 1, + REFERENCES => {TABLE => 'bug_status', COLUMN => 'id', DELETE => 'CASCADE'} + }, + require_comment => {TYPE => 'INT1', NOTNULL => 1, DEFAULT => 0}, + ], + INDEXES => [ + status_workflow_idx => + {FIELDS => ['old_status', 'new_status'], TYPE => 'UNIQUE'}, + ], + }, + + # USER INFO + # --------- + + # General User Information + # ------------------------ + + profiles => { + FIELDS => [ + userid => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + login_name => {TYPE => 'varchar(255)', NOTNULL => 1}, + cryptpassword => {TYPE => 'varchar(128)'}, + realname => {TYPE => 'varchar(255)', NOTNULL => 1, DEFAULT => "''"}, + disabledtext => {TYPE => 'MEDIUMTEXT', NOTNULL => 1, DEFAULT => "''"}, + disable_mail => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + mybugslink => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}, + extern_id => {TYPE => 'varchar(64)'}, + is_enabled => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}, + last_seen_date => {TYPE => 'DATETIME'}, + ], + INDEXES => [ + profiles_login_name_idx => {FIELDS => ['login_name'], TYPE => 'UNIQUE'}, + profiles_extern_id_idx => {FIELDS => ['extern_id'], TYPE => 'UNIQUE'} + ], + }, + + profile_search => { + FIELDS => [ + id => {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + bug_list => {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, + list_order => {TYPE => 'MEDIUMTEXT'}, + ], + INDEXES => [profile_search_user_id_idx => [qw(user_id)],], + }, + + profiles_activity => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + userid => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + who => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid'} + }, + profiles_when => {TYPE => 'DATETIME', NOTNULL => 1}, + fieldid => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'fielddefs', COLUMN => 'id'} + }, + oldvalue => {TYPE => 'TINYTEXT'}, + newvalue => {TYPE => 'TINYTEXT'}, + ], + INDEXES => [ + profiles_activity_userid_idx => ['userid'], + profiles_activity_profiles_when_idx => ['profiles_when'], + profiles_activity_fieldid_idx => ['fieldid'], + ], + }, + + email_setting => { + FIELDS => [ + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + relationship => {TYPE => 'INT1', NOTNULL => 1}, + event => {TYPE => 'INT1', NOTNULL => 1}, + ], + INDEXES => [ + email_setting_user_id_idx => + {FIELDS => [qw(user_id relationship event)], TYPE => 'UNIQUE'}, + ], + }, + + email_bug_ignore => { + FIELDS => [ + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + ], + INDEXES => [ + email_bug_ignore_user_id_idx => + {FIELDS => [qw(user_id bug_id)], TYPE => 'UNIQUE'}, + ], + }, + + watch => { + FIELDS => [ + watcher => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + watched => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + ], + INDEXES => [ + watch_watcher_idx => {FIELDS => [qw(watcher watched)], TYPE => 'UNIQUE'}, + watch_watched_idx => ['watched'], + ], + }, + + namedqueries => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + userid => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + name => {TYPE => 'varchar(64)', NOTNULL => 1}, + query => {TYPE => 'LONGTEXT', NOTNULL => 1}, + ], + INDEXES => + [namedqueries_userid_idx => {FIELDS => [qw(userid name)], TYPE => 'UNIQUE'},], + }, + + namedqueries_link_in_footer => { + FIELDS => [ + namedquery_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'namedqueries', COLUMN => 'id', DELETE => 'CASCADE'} + }, + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + ], + INDEXES => [ + namedqueries_link_in_footer_id_idx => + {FIELDS => [qw(namedquery_id user_id)], TYPE => 'UNIQUE'}, + namedqueries_link_in_footer_userid_idx => ['user_id'], + ], + }, + + tag => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + name => {TYPE => 'varchar(64)', NOTNULL => 1}, + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + ], + INDEXES => + [tag_user_id_idx => {FIELDS => [qw(user_id name)], TYPE => 'UNIQUE'},], + }, + + bug_tag => { + FIELDS => [ + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + tag_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'tag', COLUMN => 'id', DELETE => 'CASCADE'} + }, + ], + INDEXES => + [bug_tag_bug_id_idx => {FIELDS => [qw(bug_id tag_id)], TYPE => 'UNIQUE'},], + }, + + reports => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + name => {TYPE => 'varchar(64)', NOTNULL => 1}, + query => {TYPE => 'LONGTEXT', NOTNULL => 1}, + ], + INDEXES => + [reports_user_id_idx => {FIELDS => [qw(user_id name)], TYPE => 'UNIQUE'},], + }, + + component_cc => { + + FIELDS => [ + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + component_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'components', COLUMN => 'id', DELETE => 'CASCADE'} + }, + ], + INDEXES => [ + component_cc_user_id_idx => + {FIELDS => [qw(component_id user_id)], TYPE => 'UNIQUE'}, + ], + }, + + # Authentication + # -------------- + + logincookies => { + FIELDS => [ + cookie => {TYPE => 'varchar(16)', NOTNULL => 1, PRIMARYKEY => 1}, + userid => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + ipaddr => {TYPE => 'varchar(40)'}, + lastused => {TYPE => 'DATETIME', NOTNULL => 1}, + ], + INDEXES => [logincookies_lastused_idx => ['lastused'],], + }, + + login_failure => { + FIELDS => [ + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + login_time => {TYPE => 'DATETIME', NOTNULL => 1}, + ip_addr => {TYPE => 'varchar(40)', NOTNULL => 1}, + ], + INDEXES => [ + + # We do lookups by every item in the table simultaneously, but + # having an index with all three items would be the same size as + # the table. So instead we have an index on just the smallest item, + # to speed lookups. + login_failure_user_id_idx => ['user_id'], + ], + }, + + + # "tokens" stores the tokens users receive when a password or email + # change is requested. Tokens provide an extra measure of security + # for these changes. + tokens => { + FIELDS => [ + userid => { + TYPE => 'INT3', + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + issuedate => {TYPE => 'DATETIME', NOTNULL => 1}, + token => {TYPE => 'varchar(16)', NOTNULL => 1, PRIMARYKEY => 1}, + tokentype => {TYPE => 'varchar(16)', NOTNULL => 1}, + eventdata => {TYPE => 'TINYTEXT'}, + ], + INDEXES => [tokens_userid_idx => ['userid'],], + }, + + # GROUPS + # ------ + + groups => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + name => {TYPE => 'varchar(255)', NOTNULL => 1}, + description => {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, + isbuggroup => {TYPE => 'BOOLEAN', NOTNULL => 1}, + userregexp => {TYPE => 'TINYTEXT', NOTNULL => 1, DEFAULT => "''"}, + isactive => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}, + icon_url => {TYPE => 'TINYTEXT'}, ], + INDEXES => [groups_name_idx => {FIELDS => ['name'], TYPE => 'UNIQUE'},], + }, + + group_control_map => { + FIELDS => [ + group_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'groups', COLUMN => 'id', DELETE => 'CASCADE'} + }, + product_id => { + TYPE => 'INT2', + NOTNULL => 1, + REFERENCES => {TABLE => 'products', COLUMN => 'id', DELETE => 'CASCADE'} + }, + entry => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + membercontrol => {TYPE => 'INT1', NOTNULL => 1, DEFAULT => CONTROLMAPNA}, + othercontrol => {TYPE => 'INT1', NOTNULL => 1, DEFAULT => CONTROLMAPNA}, + canedit => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + editcomponents => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + editbugs => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + canconfirm => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + ], + INDEXES => [ + group_control_map_product_id_idx => + {FIELDS => [qw(product_id group_id)], TYPE => 'UNIQUE'}, + group_control_map_group_id_idx => ['group_id'], + ], + }, + + # "user_group_map" determines the groups that a user belongs to + # directly or due to regexp and which groups can be blessed by a user. + # + # grant_type: + # if GRANT_DIRECT - record was explicitly granted + # if GRANT_DERIVED - record was derived from expanding a group hierarchy + # if GRANT_REGEXP - record was created by evaluating a regexp + user_group_map => { + FIELDS => [ + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + group_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'groups', COLUMN => 'id', DELETE => 'CASCADE'} + }, + isbless => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + grant_type => {TYPE => 'INT1', NOTNULL => 1, DEFAULT => GRANT_DIRECT}, + ], + INDEXES => [ + user_group_map_user_id_idx => + {FIELDS => [qw(user_id group_id grant_type isbless)], TYPE => 'UNIQUE'}, + ], + }, + + # This table determines which groups are made a member of another + # group, given the ability to bless another group, or given + # visibility to another groups existence and membership + # grant_type: + # if GROUP_MEMBERSHIP - member groups are made members of grantor + # if GROUP_BLESS - member groups may grant membership in grantor + # if GROUP_VISIBLE - member groups may see grantor group + group_group_map => { + FIELDS => [ + member_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'groups', COLUMN => 'id', DELETE => 'CASCADE'} + }, + grantor_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'groups', COLUMN => 'id', DELETE => 'CASCADE'} + }, + grant_type => {TYPE => 'INT1', NOTNULL => 1, DEFAULT => GROUP_MEMBERSHIP}, + ], + INDEXES => [ + group_group_map_member_id_idx => + {FIELDS => [qw(member_id grantor_id grant_type)], TYPE => 'UNIQUE'}, + ], + }, + + # This table determines which groups a user must be a member of + # in order to see a bug. + bug_group_map => { + FIELDS => [ + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + group_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'groups', COLUMN => 'id', DELETE => 'CASCADE'} + }, + ], + INDEXES => [ + bug_group_map_bug_id_idx => {FIELDS => [qw(bug_id group_id)], TYPE => 'UNIQUE'}, + bug_group_map_group_id_idx => ['group_id'], + ], + }, + + # This table determines which groups a user must be a member of + # in order to see a named query somebody else shares. + namedquery_group_map => { + FIELDS => [ + namedquery_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'namedqueries', COLUMN => 'id', DELETE => 'CASCADE'} + }, + group_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'groups', COLUMN => 'id', DELETE => 'CASCADE'} + }, + ], + INDEXES => [ + namedquery_group_map_namedquery_id_idx => + {FIELDS => [qw(namedquery_id)], TYPE => 'UNIQUE'}, + namedquery_group_map_group_id_idx => ['group_id'], + ], + }, + + category_group_map => { + FIELDS => [ + category_id => { + TYPE => 'INT2', + NOTNULL => 1, + REFERENCES => + {TABLE => 'series_categories', COLUMN => 'id', DELETE => 'CASCADE'} + }, + group_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'groups', COLUMN => 'id', DELETE => 'CASCADE'} + }, + ], + INDEXES => [ + category_group_map_category_id_idx => + {FIELDS => [qw(category_id group_id)], TYPE => 'UNIQUE'}, + ], + }, + + + # PRODUCTS + # -------- + + classifications => { + FIELDS => [ + id => {TYPE => 'SMALLSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + name => {TYPE => 'varchar(64)', NOTNULL => 1}, + description => {TYPE => 'MEDIUMTEXT'}, + sortkey => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '0'}, + ], + INDEXES => + [classifications_name_idx => {FIELDS => ['name'], TYPE => 'UNIQUE'},], + }, + + products => { + FIELDS => [ + id => {TYPE => 'SMALLSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + name => {TYPE => 'varchar(64)', NOTNULL => 1}, + classification_id => { + TYPE => 'INT2', + NOTNULL => 1, + DEFAULT => '1', + REFERENCES => {TABLE => 'classifications', COLUMN => 'id', DELETE => 'CASCADE'} + }, + description => {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, + isactive => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 1}, + defaultmilestone => {TYPE => 'varchar(64)', NOTNULL => 1, DEFAULT => "'---'"}, + allows_unconfirmed => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}, + ], + INDEXES => [products_name_idx => {FIELDS => ['name'], TYPE => 'UNIQUE'},], + }, + + components => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + name => {TYPE => 'varchar(64)', NOTNULL => 1}, + product_id => { + TYPE => 'INT2', + NOTNULL => 1, + REFERENCES => {TABLE => 'products', COLUMN => 'id', DELETE => 'CASCADE'} + }, + initialowner => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid'} + }, + initialqacontact => { + TYPE => 'INT3', + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'SET NULL'} + }, + description => {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, + isactive => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}, + ], + INDEXES => [ + components_product_id_idx => + {FIELDS => [qw(product_id name)], TYPE => 'UNIQUE'}, + components_name_idx => ['name'], + ], + }, + + # CHARTS + # ------ + + series => { + FIELDS => [ + series_id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + creator => { + TYPE => 'INT3', + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + category => { + TYPE => 'INT2', + NOTNULL => 1, + REFERENCES => + {TABLE => 'series_categories', COLUMN => 'id', DELETE => 'CASCADE'} + }, + subcategory => { + TYPE => 'INT2', + NOTNULL => 1, + REFERENCES => + {TABLE => 'series_categories', COLUMN => 'id', DELETE => 'CASCADE'} + }, + name => {TYPE => 'varchar(64)', NOTNULL => 1}, + frequency => {TYPE => 'INT2', NOTNULL => 1}, + query => {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, + is_public => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + ], + INDEXES => [ + series_creator_idx => ['creator'], + series_category_idx => + {FIELDS => [qw(category subcategory name)], TYPE => 'UNIQUE'}, + ], + }, + + series_data => { + FIELDS => [ + series_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'series', COLUMN => 'series_id', DELETE => 'CASCADE'} + }, + series_date => {TYPE => 'DATETIME', NOTNULL => 1}, + series_value => {TYPE => 'INT3', NOTNULL => 1}, + ], + INDEXES => [ + series_data_series_id_idx => + {FIELDS => [qw(series_id series_date)], TYPE => 'UNIQUE'}, + ], + }, + + series_categories => { + FIELDS => [ + id => {TYPE => 'SMALLSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + name => {TYPE => 'varchar(64)', NOTNULL => 1}, + ], + INDEXES => + [series_categories_name_idx => {FIELDS => ['name'], TYPE => 'UNIQUE'},], + }, + + # WHINE SYSTEM + # ------------ + + whine_queries => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', PRIMARYKEY => 1, NOTNULL => 1}, + eventid => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'whine_events', COLUMN => 'id', DELETE => 'CASCADE'} + }, + query_name => {TYPE => 'varchar(64)', NOTNULL => 1, DEFAULT => "''"}, + sortkey => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '0'}, + onemailperbug => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + title => {TYPE => 'varchar(128)', NOTNULL => 1, DEFAULT => "''"}, + ], + INDEXES => [whine_queries_eventid_idx => ['eventid'],], + }, + + whine_schedules => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', PRIMARYKEY => 1, NOTNULL => 1}, + eventid => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'whine_events', COLUMN => 'id', DELETE => 'CASCADE'} + }, + run_day => {TYPE => 'varchar(32)'}, + run_time => {TYPE => 'varchar(32)'}, + run_next => {TYPE => 'DATETIME'}, + mailto => {TYPE => 'INT3', NOTNULL => 1}, + mailto_type => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '0'}, + ], + INDEXES => [ + whine_schedules_run_next_idx => ['run_next'], + whine_schedules_eventid_idx => ['eventid'], + ], + }, + + whine_events => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', PRIMARYKEY => 1, NOTNULL => 1}, + owner_userid => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + subject => {TYPE => 'varchar(128)'}, + body => {TYPE => 'MEDIUMTEXT'}, + mailifnobugs => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + ], + }, + + # QUIPS + # ----- + + quips => { + FIELDS => [ + quipid => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + userid => { + TYPE => 'INT3', + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'SET NULL'} + }, + quip => {TYPE => 'varchar(512)', NOTNULL => 1}, + approved => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}, + ], + }, + + # SETTINGS + # -------- + # setting - each global setting will have exactly one entry + # in this table. + # setting_value - stores the list of acceptable values for each + # setting, and a sort index that controls the order + # in which the values are displayed. + # profile_setting - If a user has chosen to use a value other than the + # global default for a given setting, it will be + # stored in this table. Note: even if a setting is + # later changed so is_enabled = false, the stored + # value will remain in case it is ever enabled again. + # + setting => { + FIELDS => [ + name => {TYPE => 'varchar(32)', NOTNULL => 1, PRIMARYKEY => 1}, + default_value => {TYPE => 'varchar(32)', NOTNULL => 1}, + is_enabled => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}, + subclass => {TYPE => 'varchar(32)'}, + ], + }, + + setting_value => { + FIELDS => [ + name => { + TYPE => 'varchar(32)', + NOTNULL => 1, + REFERENCES => {TABLE => 'setting', COLUMN => 'name', DELETE => 'CASCADE'} + }, + value => {TYPE => 'varchar(32)', NOTNULL => 1}, + sortindex => {TYPE => 'INT2', NOTNULL => 1}, + ], + INDEXES => [ + setting_value_nv_unique_idx => {FIELDS => [qw(name value)], TYPE => 'UNIQUE'}, + setting_value_ns_unique_idx => + {FIELDS => [qw(name sortindex)], TYPE => 'UNIQUE'}, + ], + }, + + profile_setting => { + FIELDS => [ + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + setting_name => { + TYPE => 'varchar(32)', + NOTNULL => 1, + REFERENCES => {TABLE => 'setting', COLUMN => 'name', DELETE => 'CASCADE'} + }, + setting_value => {TYPE => 'varchar(32)', NOTNULL => 1}, + ], + INDEXES => [ + profile_setting_value_unique_idx => + {FIELDS => [qw(user_id setting_name)], TYPE => 'UNIQUE'}, + ], + }, + + # BUGMAIL + # ------- + + mail_staging => { + FIELDS => [ + id => {TYPE => 'INTSERIAL', PRIMARYKEY => 1, NOTNULL => 1}, + message => {TYPE => 'LONGBLOB', NOTNULL => 1}, + ], + }, + + # THESCHWARTZ TABLES + # ------------------ + # Note: In the standard TheSchwartz schema, most integers are unsigned, + # but we didn't implement unsigned ints for Bugzilla schemas, so we + # just create signed ints, which should be fine. + + ts_funcmap => { + FIELDS => [ + funcid => {TYPE => 'INTSERIAL', PRIMARYKEY => 1, NOTNULL => 1}, + funcname => {TYPE => 'varchar(255)', NOTNULL => 1}, + ], + INDEXES => + [ts_funcmap_funcname_idx => {FIELDS => ['funcname'], TYPE => 'UNIQUE'},], + }, + + ts_job => { + FIELDS => [ + + # In a standard TheSchwartz schema, this is a BIGINT, but we + # don't have those and I didn't want to add them just for this. + jobid => {TYPE => 'INTSERIAL', PRIMARYKEY => 1, NOTNULL => 1}, + funcid => {TYPE => 'INT4', NOTNULL => 1}, + + # In standard TheSchwartz, this is a MEDIUMBLOB. + arg => {TYPE => 'LONGBLOB'}, + uniqkey => {TYPE => 'varchar(255)'}, + insert_time => {TYPE => 'INT4'}, + run_after => {TYPE => 'INT4', NOTNULL => 1}, + grabbed_until => {TYPE => 'INT4', NOTNULL => 1}, + priority => {TYPE => 'INT2'}, + coalesce => {TYPE => 'varchar(255)'}, + ], + INDEXES => [ + ts_job_funcid_idx => {FIELDS => [qw(funcid uniqkey)], TYPE => 'UNIQUE'}, + + # In a standard TheSchewartz schema, these both go in the other + # direction, but there's no reason to have three indexes that + # all start with the same column, and our naming scheme doesn't + # allow it anyhow. + ts_job_run_after_idx => [qw(run_after funcid)], + ts_job_coalesce_idx => [qw(coalesce funcid)], + ], + }, + + ts_note => { + FIELDS => [ + + # This is a BIGINT in standard TheSchwartz schemas. + jobid => {TYPE => 'INT4', NOTNULL => 1}, + notekey => {TYPE => 'varchar(255)'}, + value => {TYPE => 'LONGBLOB'}, + ], + INDEXES => + [ts_note_jobid_idx => {FIELDS => [qw(jobid notekey)], TYPE => 'UNIQUE'},], + }, + + ts_error => { + FIELDS => [ + error_time => {TYPE => 'INT4', NOTNULL => 1}, + jobid => {TYPE => 'INT4', NOTNULL => 1}, + message => {TYPE => 'varchar(255)', NOTNULL => 1}, + funcid => {TYPE => 'INT4', NOTNULL => 1, DEFAULT => 0}, + ], + INDEXES => [ + ts_error_funcid_idx => [qw(funcid error_time)], + ts_error_error_time_idx => ['error_time'], + ts_error_jobid_idx => ['jobid'], + ], + }, + + ts_exitstatus => { + FIELDS => [ + jobid => {TYPE => 'INTSERIAL', PRIMARYKEY => 1, NOTNULL => 1}, + funcid => {TYPE => 'INT4', NOTNULL => 1, DEFAULT => 0}, + status => {TYPE => 'INT2'}, + completion_time => {TYPE => 'INT4'}, + delete_after => {TYPE => 'INT4'}, + ], + INDEXES => [ + ts_exitstatus_funcid_idx => ['funcid'], + ts_exitstatus_delete_after_idx => ['delete_after'], + ], + }, + + # SCHEMA STORAGE + # -------------- + + bz_schema => { + FIELDS => [ + schema_data => {TYPE => 'LONGBLOB', NOTNULL => 1}, + version => {TYPE => 'decimal(3,2)', NOTNULL => 1}, + ], + }, + + bug_user_last_visit => { + FIELDS => [ + id => {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + last_visit_ts => {TYPE => 'DATETIME', NOTNULL => 1}, + ], + INDEXES => [ + bug_user_last_visit_idx => {FIELDS => ['user_id', 'bug_id'], TYPE => 'UNIQUE'}, + bug_user_last_visit_last_visit_ts_idx => ['last_visit_ts'], + ], + }, + + user_api_keys => { + FIELDS => [ + id => {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + api_key => {TYPE => 'VARCHAR(40)', NOTNULL => 1}, + description => {TYPE => 'VARCHAR(255)'}, + revoked => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + last_used => {TYPE => 'DATETIME'}, + ], + INDEXES => [ + user_api_keys_api_key_idx => {FIELDS => ['api_key'], TYPE => 'UNIQUE'}, + user_api_keys_user_id_idx => ['user_id'], + ], + }, +}; + +# Foreign Keys are added in Bugzilla::DB::bz_add_field_tables +use constant MULTI_SELECT_VALUE_TABLE => { + FIELDS => [ + bug_id => {TYPE => 'INT3', NOTNULL => 1}, + value => {TYPE => 'varchar(64)', NOTNULL => 1}, + ], + INDEXES => [bug_id_idx => {FIELDS => [qw( bug_id value)], TYPE => 'UNIQUE'},], }; #-------------------------------------------------------------------------- @@ -1821,27 +1770,28 @@ sub new { =cut - my $this = shift; - my $class = ref($this) || $this; - my $driver = shift; + my $this = shift; + my $class = ref($this) || $this; + my $driver = shift; - if ($driver) { - (my $subclass = $driver) =~ s/^(\S)/\U$1/; - $class .= '::' . $subclass; - eval "require $class;"; - die "The $class class could not be found ($subclass " . - "not supported?): $@" if ($@); - } - die "$class is an abstract base class. Instantiate a subclass instead." - if ($class eq __PACKAGE__); + if ($driver) { + (my $subclass = $driver) =~ s/^(\S)/\U$1/; + $class .= '::' . $subclass; + eval "require $class;"; + die "The $class class could not be found ($subclass " . "not supported?): $@" + if ($@); + } + die "$class is an abstract base class. Instantiate a subclass instead." + if ($class eq __PACKAGE__); + + my $self = {}; + bless $self, $class; + $self = $self->_initialize(@_); - my $self = {}; - bless $self, $class; - $self = $self->_initialize(@_); + return ($self); - return($self); +} #eosub--new -} #eosub--new #-------------------------------------------------------------------------- sub _initialize { @@ -1864,33 +1814,34 @@ sub _initialize { =cut - my $self = shift; - my $abstract_schema = shift; + my $self = shift; + my $abstract_schema = shift; - if (!$abstract_schema) { - # While ABSTRACT_SCHEMA cannot be modified, $abstract_schema can be. - # So, we dclone it to prevent anything from mucking with the constant. - $abstract_schema = dclone(ABSTRACT_SCHEMA); + if (!$abstract_schema) { - # Let extensions add tables, but make sure they can't modify existing - # tables. If we don't lock/unlock keys, lock_value complains. - lock_keys(%$abstract_schema); - foreach my $table (keys %{ABSTRACT_SCHEMA()}) { - lock_value(%$abstract_schema, $table) - if exists $abstract_schema->{$table}; - } - unlock_keys(%$abstract_schema); - Bugzilla::Hook::process('db_schema_abstract_schema', - { schema => $abstract_schema }); - unlock_hash(%$abstract_schema); + # While ABSTRACT_SCHEMA cannot be modified, $abstract_schema can be. + # So, we dclone it to prevent anything from mucking with the constant. + $abstract_schema = dclone(ABSTRACT_SCHEMA); + + # Let extensions add tables, but make sure they can't modify existing + # tables. If we don't lock/unlock keys, lock_value complains. + lock_keys(%$abstract_schema); + foreach my $table (keys %{ABSTRACT_SCHEMA()}) { + lock_value(%$abstract_schema, $table) if exists $abstract_schema->{$table}; } + unlock_keys(%$abstract_schema); + Bugzilla::Hook::process('db_schema_abstract_schema', + {schema => $abstract_schema}); + unlock_hash(%$abstract_schema); + } + + $self->{schema} = dclone($abstract_schema); + $self->{abstract_schema} = $abstract_schema; - $self->{schema} = dclone($abstract_schema); - $self->{abstract_schema} = $abstract_schema; + return $self; - return $self; +} #eosub--_initialize -} #eosub--_initialize #-------------------------------------------------------------------------- sub _adjust_schema { @@ -1906,36 +1857,41 @@ sub _adjust_schema { =cut - my $self = shift; - - # The _initialize method has already set up the db_specific hash with - # the information on how to implement the abstract data types for the - # instantiated DBMS-specific subclass. - my $db_specific = $self->{db_specific}; - - # Loop over each table in the abstract database schema. - foreach my $table (keys %{ $self->{schema} }) { - my %fields = (@{ $self->{schema}{$table}{FIELDS} }); - # Loop over the field definitions in each table. - foreach my $field_def (values %fields) { - # If the field type is an abstract data type defined in the - # $db_specific hash, replace it with the DBMS-specific data type - # that implements it. - if (exists($db_specific->{$field_def->{TYPE}})) { - $field_def->{TYPE} = $db_specific->{$field_def->{TYPE}}; - } - # Replace abstract default values (such as 'TRUE' and 'FALSE') - # with their database-specific implementations. - if (exists($field_def->{DEFAULT}) - && exists($db_specific->{$field_def->{DEFAULT}})) { - $field_def->{DEFAULT} = $db_specific->{$field_def->{DEFAULT}}; - } - } + my $self = shift; + + # The _initialize method has already set up the db_specific hash with + # the information on how to implement the abstract data types for the + # instantiated DBMS-specific subclass. + my $db_specific = $self->{db_specific}; + + # Loop over each table in the abstract database schema. + foreach my $table (keys %{$self->{schema}}) { + my %fields = (@{$self->{schema}{$table}{FIELDS}}); + + # Loop over the field definitions in each table. + foreach my $field_def (values %fields) { + + # If the field type is an abstract data type defined in the + # $db_specific hash, replace it with the DBMS-specific data type + # that implements it. + if (exists($db_specific->{$field_def->{TYPE}})) { + $field_def->{TYPE} = $db_specific->{$field_def->{TYPE}}; + } + + # Replace abstract default values (such as 'TRUE' and 'FALSE') + # with their database-specific implementations. + if ( exists($field_def->{DEFAULT}) + && exists($db_specific->{$field_def->{DEFAULT}})) + { + $field_def->{DEFAULT} = $db_specific->{$field_def->{DEFAULT}}; + } } + } + + return $self; - return $self; +} #eosub--_adjust_schema -} #eosub--_adjust_schema #-------------------------------------------------------------------------- sub get_type_ddl { @@ -1969,30 +1925,34 @@ C SQL statement =cut - my $self = shift; - my $finfo = (@_ == 1 && ref($_[0]) eq 'HASH') ? $_[0] : { @_ }; - my $type = $finfo->{TYPE}; - confess "A valid TYPE was not specified for this column (got " - . Dumper($finfo) . ")" unless ($type); - - my $default = $finfo->{DEFAULT}; - # Replace any abstract default value (such as 'TRUE' or 'FALSE') - # with its database-specific implementation. - if ( defined $default && exists($self->{db_specific}->{$default}) ) { - $default = $self->{db_specific}->{$default}; - } + my $self = shift; + my $finfo = (@_ == 1 && ref($_[0]) eq 'HASH') ? $_[0] : {@_}; + my $type = $finfo->{TYPE}; + confess "A valid TYPE was not specified for this column (got " + . Dumper($finfo) . ")" + unless ($type); + + my $default = $finfo->{DEFAULT}; + + # Replace any abstract default value (such as 'TRUE' or 'FALSE') + # with its database-specific implementation. + if (defined $default && exists($self->{db_specific}->{$default})) { + $default = $self->{db_specific}->{$default}; + } + + my $type_ddl = $self->convert_type($type); + + # DEFAULT attribute must appear before any column constraints + # (e.g., NOT NULL), for Oracle + $type_ddl .= " DEFAULT $default" if (defined($default)); - my $type_ddl = $self->convert_type($type); - # DEFAULT attribute must appear before any column constraints - # (e.g., NOT NULL), for Oracle - $type_ddl .= " DEFAULT $default" if (defined($default)); - # PRIMARY KEY must appear before NOT NULL for SQLite. - $type_ddl .= " PRIMARY KEY" if ($finfo->{PRIMARYKEY}); - $type_ddl .= " NOT NULL" if ($finfo->{NOTNULL}); + # PRIMARY KEY must appear before NOT NULL for SQLite. + $type_ddl .= " PRIMARY KEY" if ($finfo->{PRIMARYKEY}); + $type_ddl .= " NOT NULL" if ($finfo->{NOTNULL}); - return($type_ddl); + return ($type_ddl); -} #eosub--get_type_ddl +} #eosub--get_type_ddl sub get_fk_ddl { @@ -2026,78 +1986,80 @@ is undefined. =cut - my ($self, $table, $column, $references) = @_; - return "" if !$references; + my ($self, $table, $column, $references) = @_; + return "" if !$references; - my $update = $references->{UPDATE} || 'CASCADE'; - my $delete = $references->{DELETE} || 'RESTRICT'; - my $to_table = $references->{TABLE} || confess "No table in reference"; - my $to_column = $references->{COLUMN} || confess "No column in reference"; - my $fk_name = $self->_get_fk_name($table, $column, $references); + my $update = $references->{UPDATE} || 'CASCADE'; + my $delete = $references->{DELETE} || 'RESTRICT'; + my $to_table = $references->{TABLE} || confess "No table in reference"; + my $to_column = $references->{COLUMN} || confess "No column in reference"; + my $fk_name = $self->_get_fk_name($table, $column, $references); - return "\n CONSTRAINT $fk_name FOREIGN KEY ($column)\n" - . " REFERENCES $to_table($to_column)\n" - . " ON UPDATE $update ON DELETE $delete"; + return + "\n CONSTRAINT $fk_name FOREIGN KEY ($column)\n" + . " REFERENCES $to_table($to_column)\n" + . " ON UPDATE $update ON DELETE $delete"; } # Generates a name for a Foreign Key. It's separate from get_fk_ddl # so that certain databases can override it (for shorter identifiers or # other reasons). sub _get_fk_name { - my ($self, $table, $column, $references) = @_; - my $to_table = $references->{TABLE}; - my $to_column = $references->{COLUMN}; - my $name = "fk_${table}_${column}_${to_table}_${to_column}"; + my ($self, $table, $column, $references) = @_; + my $to_table = $references->{TABLE}; + my $to_column = $references->{COLUMN}; + my $name = "fk_${table}_${column}_${to_table}_${to_column}"; - if (length($name) > $self->MAX_IDENTIFIER_LEN) { - $name = 'fk_' . $self->_hash_identifier($name); - } + if (length($name) > $self->MAX_IDENTIFIER_LEN) { + $name = 'fk_' . $self->_hash_identifier($name); + } - return $name; + return $name; } sub _hash_identifier { - my ($invocant, $value) = @_; - # We do -7 to allow prefixes like "idx_" or "fk_", or perhaps something - # longer in the future. - return substr(md5_hex($value), 0, $invocant->MAX_IDENTIFIER_LEN - 7); + my ($invocant, $value) = @_; + + # We do -7 to allow prefixes like "idx_" or "fk_", or perhaps something + # longer in the future. + return substr(md5_hex($value), 0, $invocant->MAX_IDENTIFIER_LEN - 7); } sub get_add_fks_sql { - my ($self, $table, $column_fks) = @_; - - my @add = $self->_column_fks_to_ddl($table, $column_fks); - - my @sql; - if ($self->MULTIPLE_FKS_IN_ALTER) { - my $alter = "ALTER TABLE $table ADD " . join(', ADD ', @add); - push(@sql, $alter); + my ($self, $table, $column_fks) = @_; + + my @add = $self->_column_fks_to_ddl($table, $column_fks); + + my @sql; + if ($self->MULTIPLE_FKS_IN_ALTER) { + my $alter = "ALTER TABLE $table ADD " . join(', ADD ', @add); + push(@sql, $alter); + } + else { + foreach my $fk_string (@add) { + push(@sql, "ALTER TABLE $table ADD $fk_string"); } - else { - foreach my $fk_string (@add) { - push(@sql, "ALTER TABLE $table ADD $fk_string"); - } - } - return @sql; + } + return @sql; } sub _column_fks_to_ddl { - my ($self, $table, $column_fks) = @_; - my @ddl; - foreach my $column (keys %$column_fks) { - my $def = $column_fks->{$column}; - my $fk_string = $self->get_fk_ddl($table, $column, $def); - push(@ddl, $fk_string); - } - return @ddl; + my ($self, $table, $column_fks) = @_; + my @ddl; + foreach my $column (keys %$column_fks) { + my $def = $column_fks->{$column}; + my $fk_string = $self->get_fk_ddl($table, $column, $def); + push(@ddl, $fk_string); + } + return @ddl; } -sub get_drop_fk_sql { - my ($self, $table, $column, $references) = @_; - my $fk_name = $self->_get_fk_name($table, $column, $references); +sub get_drop_fk_sql { + my ($self, $table, $column, $references) = @_; + my $fk_name = $self->_get_fk_name($table, $column, $references); - return ("ALTER TABLE $table DROP CONSTRAINT $fk_name"); + return ("ALTER TABLE $table DROP CONSTRAINT $fk_name"); } sub convert_type { @@ -2108,8 +2070,8 @@ Converts a TYPE from the L format into the real SQL type. =cut - my ($self, $type) = @_; - return $self->{db_specific}->{$type} || $type; + my ($self, $type) = @_; + return $self->{db_specific}->{$type} || $type; } sub get_column { @@ -2126,16 +2088,16 @@ sub get_column { =cut - my($self, $table, $column) = @_; + my ($self, $table, $column) = @_; - # Prevent a possible dereferencing of an undef hash, if the - # table doesn't exist. - if (exists $self->{schema}->{$table}) { - my %fields = (@{ $self->{schema}{$table}{FIELDS} }); - return $fields{$column}; - } - return undef; -} #eosub--get_column + # Prevent a possible dereferencing of an undef hash, if the + # table doesn't exist. + if (exists $self->{schema}->{$table}) { + my %fields = (@{$self->{schema}{$table}{FIELDS}}); + return $fields{$column}; + } + return undef; +} #eosub--get_column sub get_table_list { @@ -2150,8 +2112,8 @@ sub get_table_list { =cut - my $self = shift; - return sort keys %{$self->{schema}}; + my $self = shift; + return sort keys %{$self->{schema}}; } sub get_table_columns { @@ -2165,34 +2127,33 @@ sub get_table_columns { =cut - my($self, $table) = @_; - my @ddl = (); + my ($self, $table) = @_; + my @ddl = (); - my $thash = $self->{schema}{$table}; - die "Table $table does not exist in the database schema." - unless (ref($thash)); + my $thash = $self->{schema}{$table}; + die "Table $table does not exist in the database schema." unless (ref($thash)); - my @columns = (); - my @fields = @{ $thash->{FIELDS} }; - while (@fields) { - push(@columns, shift(@fields)); - shift(@fields); - } + my @columns = (); + my @fields = @{$thash->{FIELDS}}; + while (@fields) { + push(@columns, shift(@fields)); + shift(@fields); + } - return @columns; + return @columns; -} #eosub--get_table_columns +} #eosub--get_table_columns sub get_table_indexes_abstract { - my ($self, $table) = @_; - my $table_def = $self->get_table_abstract($table); - my %indexes = @{$table_def->{INDEXES} || []}; - return \%indexes; + my ($self, $table) = @_; + my $table_def = $self->get_table_abstract($table); + my %indexes = @{$table_def->{INDEXES} || []}; + return \%indexes; } sub get_create_database_sql { - my ($self, $name) = @_; - return ("CREATE DATABASE $name"); + my ($self, $name) = @_; + return ("CREATE DATABASE $name"); } sub get_table_ddl { @@ -2209,30 +2170,29 @@ sub get_table_ddl { =cut - my($self, $table) = @_; - my @ddl = (); + my ($self, $table) = @_; + my @ddl = (); - die "Table $table does not exist in the database schema." - unless (ref($self->{schema}{$table})); + die "Table $table does not exist in the database schema." + unless (ref($self->{schema}{$table})); - my $create_table = $self->_get_create_table_ddl($table); - push(@ddl, $create_table) if $create_table; + my $create_table = $self->_get_create_table_ddl($table); + push(@ddl, $create_table) if $create_table; - my @indexes = @{ $self->{schema}{$table}{INDEXES} || [] }; - while (@indexes) { - my $index_name = shift(@indexes); - my $index_info = shift(@indexes); - my $index_sql = $self->get_add_index_ddl($table, $index_name, - $index_info); - push(@ddl, $index_sql) if $index_sql; - } + my @indexes = @{$self->{schema}{$table}{INDEXES} || []}; + while (@indexes) { + my $index_name = shift(@indexes); + my $index_info = shift(@indexes); + my $index_sql = $self->get_add_index_ddl($table, $index_name, $index_info); + push(@ddl, $index_sql) if $index_sql; + } - push(@ddl, @{ $self->{schema}{$table}{DB_EXTRAS} }) - if (ref($self->{schema}{$table}{DB_EXTRAS})); + push(@ddl, @{$self->{schema}{$table}{DB_EXTRAS}}) + if (ref($self->{schema}{$table}{DB_EXTRAS})); - return @ddl; + return @ddl; -} #eosub--get_table_ddl +} #eosub--get_table_ddl sub _get_create_table_ddl { @@ -2245,30 +2205,29 @@ sub _get_create_table_ddl { =cut - my($self, $table) = @_; - - my $thash = $self->{schema}{$table}; - die "Table $table does not exist in the database schema." - unless ref $thash; - - my (@col_lines, @fk_lines); - my @fields = @{ $thash->{FIELDS} }; - while (@fields) { - my $field = shift(@fields); - my $finfo = shift(@fields); - push(@col_lines, "\t$field\t" . $self->get_type_ddl($finfo)); - if ($self->FK_ON_CREATE and $finfo->{REFERENCES}) { - my $fk = $finfo->{REFERENCES}; - my $fk_ddl = $self->get_fk_ddl($table, $field, $fk); - push(@fk_lines, $fk_ddl); - } + my ($self, $table) = @_; + + my $thash = $self->{schema}{$table}; + die "Table $table does not exist in the database schema." unless ref $thash; + + my (@col_lines, @fk_lines); + my @fields = @{$thash->{FIELDS}}; + while (@fields) { + my $field = shift(@fields); + my $finfo = shift(@fields); + push(@col_lines, "\t$field\t" . $self->get_type_ddl($finfo)); + if ($self->FK_ON_CREATE and $finfo->{REFERENCES}) { + my $fk = $finfo->{REFERENCES}; + my $fk_ddl = $self->get_fk_ddl($table, $field, $fk); + push(@fk_lines, $fk_ddl); } - - my $sql = "CREATE TABLE $table (\n" . join(",\n", @col_lines, @fk_lines) - . "\n)"; - return $sql + } -} + my $sql + = "CREATE TABLE $table (\n" . join(",\n", @col_lines, @fk_lines) . "\n)"; + return $sql; + +} sub _get_create_index_ddl { @@ -2284,16 +2243,17 @@ sub _get_create_index_ddl { =cut - my ($self, $table_name, $index_name, $index_fields, $index_type) = @_; + my ($self, $table_name, $index_name, $index_fields, $index_type) = @_; + + my $sql = "CREATE "; + $sql .= "$index_type " if ($index_type && $index_type eq 'UNIQUE'); + $sql + .= "INDEX $index_name ON $table_name \(" . join(", ", @$index_fields) . "\)"; - my $sql = "CREATE "; - $sql .= "$index_type " if ($index_type && $index_type eq 'UNIQUE'); - $sql .= "INDEX $index_name ON $table_name \(" . - join(", ", @$index_fields) . "\)"; + return ($sql); - return($sql); +} #eosub--_get_create_index_ddl -} #eosub--_get_create_index_ddl #-------------------------------------------------------------------------- sub get_add_column_ddl { @@ -2312,22 +2272,25 @@ sub get_add_column_ddl { =cut - my ($self, $table, $column, $definition, $init_value) = @_; - my @statements; - push(@statements, "ALTER TABLE $table ". $self->ADD_COLUMN ." $column " . - $self->get_type_ddl($definition)); - - # XXX - Note that although this works for MySQL, most databases will fail - # before this point, if we haven't set a default. - (push(@statements, "UPDATE $table SET $column = $init_value")) - if defined $init_value; - - if (defined $definition->{REFERENCES}) { - push(@statements, $self->get_add_fks_sql($table, { $column => - $definition->{REFERENCES} })); - } - - return (@statements); + my ($self, $table, $column, $definition, $init_value) = @_; + my @statements; + push(@statements, + "ALTER TABLE $table " + . $self->ADD_COLUMN + . " $column " + . $self->get_type_ddl($definition)); + + # XXX - Note that although this works for MySQL, most databases will fail + # before this point, if we haven't set a default. + (push(@statements, "UPDATE $table SET $column = $init_value")) + if defined $init_value; + + if (defined $definition->{REFERENCES}) { + push(@statements, + $self->get_add_fks_sql($table, {$column => $definition->{REFERENCES}})); + } + + return (@statements); } sub get_add_index_ddl { @@ -2348,20 +2311,21 @@ sub get_add_index_ddl { =cut - my ($self, $table, $name, $definition) = @_; + my ($self, $table, $name, $definition) = @_; - my ($index_fields, $index_type); - # Index defs can be arrays or hashes - if (ref($definition) eq 'HASH') { - $index_fields = $definition->{FIELDS}; - $index_type = $definition->{TYPE}; - } else { - $index_fields = $definition; - $index_type = ''; - } - - return $self->_get_create_index_ddl($table, $name, $index_fields, - $index_type); + my ($index_fields, $index_type); + + # Index defs can be arrays or hashes + if (ref($definition) eq 'HASH') { + $index_fields = $definition->{FIELDS}; + $index_type = $definition->{TYPE}; + } + else { + $index_fields = $definition; + $index_type = ''; + } + + return $self->_get_create_index_ddl($table, $name, $index_fields, $index_type); } sub get_alter_column_ddl { @@ -2384,85 +2348,88 @@ sub get_alter_column_ddl { =cut - my $self = shift; - my ($table, $column, $new_def, $set_nulls_to) = @_; - - my @statements; - my $old_def = $self->get_column_abstract($table, $column); - my $specific = $self->{db_specific}; - - # If the types have changed, we have to deal with that. - if (uc(trim($old_def->{TYPE})) ne uc(trim($new_def->{TYPE}))) { - push(@statements, $self->_get_alter_type_sql($table, $column, - $new_def, $old_def)); - } - - my $default = $new_def->{DEFAULT}; - my $default_old = $old_def->{DEFAULT}; - - if (defined $default) { - $default = $specific->{$default} if exists $specific->{$default}; - } - # This first condition prevents "uninitialized value" errors. - if (!defined $default && !defined $default_old) { - # Do Nothing - } - # If we went from having a default to not having one - elsif (!defined $default && defined $default_old) { - push(@statements, "ALTER TABLE $table ALTER COLUMN $column" - . " DROP DEFAULT"); - } - # If we went from no default to a default, or we changed the default. - elsif ( (defined $default && !defined $default_old) || - ($default ne $default_old) ) - { - push(@statements, "ALTER TABLE $table ALTER COLUMN $column " - . " SET DEFAULT $default"); - } - - # If we went from NULL to NOT NULL. - if (!$old_def->{NOTNULL} && $new_def->{NOTNULL}) { - push(@statements, $self->_set_nulls_sql(@_)); - push(@statements, "ALTER TABLE $table ALTER COLUMN $column" - . " SET NOT NULL"); - } - # If we went from NOT NULL to NULL - elsif ($old_def->{NOTNULL} && !$new_def->{NOTNULL}) { - push(@statements, "ALTER TABLE $table ALTER COLUMN $column" - . " DROP NOT NULL"); - } - - # If we went from not being a PRIMARY KEY to being a PRIMARY KEY. - if (!$old_def->{PRIMARYKEY} && $new_def->{PRIMARYKEY}) { - push(@statements, "ALTER TABLE $table ADD PRIMARY KEY ($column)"); - } - # If we went from being a PK to not being a PK - elsif ( $old_def->{PRIMARYKEY} && !$new_def->{PRIMARYKEY} ) { - push(@statements, "ALTER TABLE $table DROP PRIMARY KEY"); - } - - return @statements; + my $self = shift; + my ($table, $column, $new_def, $set_nulls_to) = @_; + + my @statements; + my $old_def = $self->get_column_abstract($table, $column); + my $specific = $self->{db_specific}; + + # If the types have changed, we have to deal with that. + if (uc(trim($old_def->{TYPE})) ne uc(trim($new_def->{TYPE}))) { + push(@statements, + $self->_get_alter_type_sql($table, $column, $new_def, $old_def)); + } + + my $default = $new_def->{DEFAULT}; + my $default_old = $old_def->{DEFAULT}; + + if (defined $default) { + $default = $specific->{$default} if exists $specific->{$default}; + } + + # This first condition prevents "uninitialized value" errors. + if (!defined $default && !defined $default_old) { + + # Do Nothing + } + + # If we went from having a default to not having one + elsif (!defined $default && defined $default_old) { + push(@statements, "ALTER TABLE $table ALTER COLUMN $column" . " DROP DEFAULT"); + } + + # If we went from no default to a default, or we changed the default. + elsif ((defined $default && !defined $default_old) + || ($default ne $default_old)) + { + push(@statements, + "ALTER TABLE $table ALTER COLUMN $column " . " SET DEFAULT $default"); + } + + # If we went from NULL to NOT NULL. + if (!$old_def->{NOTNULL} && $new_def->{NOTNULL}) { + push(@statements, $self->_set_nulls_sql(@_)); + push(@statements, "ALTER TABLE $table ALTER COLUMN $column" . " SET NOT NULL"); + } + + # If we went from NOT NULL to NULL + elsif ($old_def->{NOTNULL} && !$new_def->{NOTNULL}) { + push(@statements, "ALTER TABLE $table ALTER COLUMN $column" . " DROP NOT NULL"); + } + + # If we went from not being a PRIMARY KEY to being a PRIMARY KEY. + if (!$old_def->{PRIMARYKEY} && $new_def->{PRIMARYKEY}) { + push(@statements, "ALTER TABLE $table ADD PRIMARY KEY ($column)"); + } + + # If we went from being a PK to not being a PK + elsif ($old_def->{PRIMARYKEY} && !$new_def->{PRIMARYKEY}) { + push(@statements, "ALTER TABLE $table DROP PRIMARY KEY"); + } + + return @statements; } # Helps handle any fields that were NULL before, if we have a default, # when doing an ALTER COLUMN. sub _set_nulls_sql { - my ($self, $table, $column, $new_def, $set_nulls_to) = @_; - my $default = $new_def->{DEFAULT}; - # If we have a set_nulls_to, that overrides the DEFAULT - # (although nobody would usually specify both a default and - # a set_nulls_to.) - $default = $set_nulls_to if defined $set_nulls_to; - if (defined $default) { - my $specific = $self->{db_specific}; - $default = $specific->{$default} if exists $specific->{$default}; - } - my @sql; - if (defined $default) { - push(@sql, "UPDATE $table SET $column = $default" - . " WHERE $column IS NULL"); - } - return @sql; + my ($self, $table, $column, $new_def, $set_nulls_to) = @_; + my $default = $new_def->{DEFAULT}; + + # If we have a set_nulls_to, that overrides the DEFAULT + # (although nobody would usually specify both a default and + # a set_nulls_to.) + $default = $set_nulls_to if defined $set_nulls_to; + if (defined $default) { + my $specific = $self->{db_specific}; + $default = $specific->{$default} if exists $specific->{$default}; + } + my @sql; + if (defined $default) { + push(@sql, "UPDATE $table SET $column = $default" . " WHERE $column IS NULL"); + } + return @sql; } sub get_drop_index_ddl { @@ -2476,11 +2443,11 @@ sub get_drop_index_ddl { =cut - my ($self, $table, $name) = @_; + my ($self, $table, $name) = @_; - # Although ANSI SQL-92 doesn't specify a method of dropping an index, - # many DBs support this syntax. - return ("DROP INDEX $name"); + # Although ANSI SQL-92 doesn't specify a method of dropping an index, + # many DBs support this syntax. + return ("DROP INDEX $name"); } sub get_drop_column_ddl { @@ -2494,8 +2461,8 @@ sub get_drop_column_ddl { =cut - my ($self, $table, $column) = @_; - return ("ALTER TABLE $table DROP COLUMN $column"); + my ($self, $table, $column) = @_; + return ("ALTER TABLE $table DROP COLUMN $column"); } =item C @@ -2507,8 +2474,8 @@ sub get_drop_column_ddl { =cut sub get_drop_table_ddl { - my ($self, $table) = @_; - return ("DROP TABLE $table"); + my ($self, $table) = @_; + return ("DROP TABLE $table"); } sub get_rename_column_ddl { @@ -2526,8 +2493,8 @@ sub get_rename_column_ddl { =cut - die "ANSI SQL has no way to rename a column, and your database driver\n" - . " has not implemented a method."; + die "ANSI SQL has no way to rename a column, and your database driver\n" + . " has not implemented a method."; } @@ -2557,8 +2524,8 @@ Gets SQL to rename a table in the database. =cut - my ($self, $old_name, $new_name) = @_; - return ("ALTER TABLE $old_name RENAME TO $new_name"); + my ($self, $old_name, $new_name) = @_; + return ("ALTER TABLE $old_name RENAME TO $new_name"); } =item C @@ -2571,13 +2538,13 @@ Gets SQL to rename a table in the database. =cut sub delete_table { - my ($self, $name) = @_; + my ($self, $name) = @_; - die "Attempted to delete nonexistent table '$name'." unless - $self->get_table_abstract($name); + die "Attempted to delete nonexistent table '$name'." + unless $self->get_table_abstract($name); - delete $self->{abstract_schema}->{$name}; - delete $self->{schema}->{$name}; + delete $self->{abstract_schema}->{$name}; + delete $self->{schema}->{$name}; } sub get_column_abstract { @@ -2594,15 +2561,15 @@ sub get_column_abstract { =cut - my ($self, $table, $column) = @_; + my ($self, $table, $column) = @_; - # Prevent a possible dereferencing of an undef hash, if the - # table doesn't exist. - if ($self->get_table_abstract($table)) { - my %fields = (@{ $self->{abstract_schema}{$table}{FIELDS} }); - return $fields{$column}; - } - return undef; + # Prevent a possible dereferencing of an undef hash, if the + # table doesn't exist. + if ($self->get_table_abstract($table)) { + my %fields = (@{$self->{abstract_schema}{$table}{FIELDS}}); + return $fields{$column}; + } + return undef; } =item C @@ -2620,29 +2587,31 @@ sub get_column_abstract { =cut sub get_indexes_on_column_abstract { - my ($self, $table, $column) = @_; - my %ret_hash; - - my $table_def = $self->get_table_abstract($table); - if ($table_def && exists $table_def->{INDEXES}) { - my %indexes = (@{ $table_def->{INDEXES} }); - foreach my $index_name (keys %indexes) { - my $col_list; - # Get the column list, depending on whether the index - # is in hashref or arrayref format. - if (ref($indexes{$index_name}) eq 'HASH') { - $col_list = $indexes{$index_name}->{FIELDS}; - } else { - $col_list = $indexes{$index_name}; - } - - if(grep($_ eq $column, @$col_list)) { - $ret_hash{$index_name} = dclone($indexes{$index_name}); - } - } + my ($self, $table, $column) = @_; + my %ret_hash; + + my $table_def = $self->get_table_abstract($table); + if ($table_def && exists $table_def->{INDEXES}) { + my %indexes = (@{$table_def->{INDEXES}}); + foreach my $index_name (keys %indexes) { + my $col_list; + + # Get the column list, depending on whether the index + # is in hashref or arrayref format. + if (ref($indexes{$index_name}) eq 'HASH') { + $col_list = $indexes{$index_name}->{FIELDS}; + } + else { + $col_list = $indexes{$index_name}; + } + + if (grep($_ eq $column, @$col_list)) { + $ret_hash{$index_name} = dclone($indexes{$index_name}); + } } + } - return %ret_hash; + return %ret_hash; } sub get_index_abstract { @@ -2658,16 +2627,16 @@ sub get_index_abstract { =cut - my ($self, $table, $index) = @_; + my ($self, $table, $index) = @_; - # Prevent a possible dereferencing of an undef hash, if the - # table doesn't exist. - my $index_table = $self->get_table_abstract($table); - if ($index_table && exists $index_table->{INDEXES}) { - my %indexes = (@{ $index_table->{INDEXES} }); - return $indexes{$index}; - } - return undef; + # Prevent a possible dereferencing of an undef hash, if the + # table doesn't exist. + my $index_table = $self->get_table_abstract($table); + if ($index_table && exists $index_table->{INDEXES}) { + my %indexes = (@{$index_table->{INDEXES}}); + return $indexes{$index}; + } + return undef; } =item C @@ -2681,8 +2650,8 @@ sub get_index_abstract { =cut sub get_table_abstract { - my ($self, $table) = @_; - return $self->{abstract_schema}->{$table}; + my ($self, $table) = @_; + return $self->{abstract_schema}->{$table}; } =item C @@ -2698,22 +2667,20 @@ sub get_table_abstract { =cut sub add_table { - my ($self, $name, $definition) = @_; - (die "Table already exists: $name") - if exists $self->{abstract_schema}->{$name}; - if ($definition) { - $self->{abstract_schema}->{$name} = dclone($definition); - $self->{schema} = dclone($self->{abstract_schema}); - $self->_adjust_schema(); - } - else { - $self->{abstract_schema}->{$name} = {FIELDS => []}; - $self->{schema}->{$name} = {FIELDS => []}; - } + my ($self, $name, $definition) = @_; + (die "Table already exists: $name") if exists $self->{abstract_schema}->{$name}; + if ($definition) { + $self->{abstract_schema}->{$name} = dclone($definition); + $self->{schema} = dclone($self->{abstract_schema}); + $self->_adjust_schema(); + } + else { + $self->{abstract_schema}->{$name} = {FIELDS => []}; + $self->{schema}->{$name} = {FIELDS => []}; + } } - sub rename_table { =item C @@ -2723,10 +2690,10 @@ Renames a table from C<$old_name> to C<$new_name> in this Schema object. =cut - my ($self, $old_name, $new_name) = @_; - my $table = $self->get_table_abstract($old_name); - $self->delete_table($old_name); - $self->add_table($new_name, $table); + my ($self, $old_name, $new_name) = @_; + my $table = $self->get_table_abstract($old_name); + $self->delete_table($old_name); + $self->add_table($new_name, $table); } sub delete_column { @@ -2741,17 +2708,18 @@ sub delete_column { =cut - my ($self, $table, $column) = @_; + my ($self, $table, $column) = @_; - my $abstract_fields = $self->{abstract_schema}{$table}{FIELDS}; - my $name_position = firstidx { $_ eq $column } @$abstract_fields; - die "Attempted to delete nonexistent column ${table}.${column}" - if $name_position == -1; - # Delete the key/value pair from the array. - splice(@$abstract_fields, $name_position, 2); + my $abstract_fields = $self->{abstract_schema}{$table}{FIELDS}; + my $name_position = firstidx { $_ eq $column } @$abstract_fields; + die "Attempted to delete nonexistent column ${table}.${column}" + if $name_position == -1; - $self->{schema} = dclone($self->{abstract_schema}); - $self->_adjust_schema(); + # Delete the key/value pair from the array. + splice(@$abstract_fields, $name_position, 2); + + $self->{schema} = dclone($self->{abstract_schema}); + $self->_adjust_schema(); } sub rename_column { @@ -2767,11 +2735,11 @@ sub rename_column { =cut - my ($self, $table, $old_name, $new_name) = @_; - my $def = $self->get_column_abstract($table, $old_name); - die "Renaming a column that doesn't exist" if !$def; - $self->delete_column($table, $old_name); - $self->set_column($table, $new_name, $def); + my ($self, $table, $old_name, $new_name) = @_; + my $def = $self->get_column_abstract($table, $old_name); + die "Renaming a column that doesn't exist" if !$def; + $self->delete_column($table, $old_name); + $self->set_column($table, $new_name, $def); } sub set_column { @@ -2792,10 +2760,10 @@ sub set_column { =cut - my ($self, $table, $column, $new_def) = @_; + my ($self, $table, $column, $new_def) = @_; - my $fields = $self->{abstract_schema}{$table}{FIELDS}; - $self->_set_object($table, $column, $new_def, $fields); + my $fields = $self->{abstract_schema}{$table}{FIELDS}; + $self->_set_object($table, $column, $new_def, $fields); } =item C @@ -2805,19 +2773,20 @@ Sets the C item on the specified column. =cut sub set_fk { - my ($self, $table, $column, $fk_def) = @_; - # Don't want to modify the source def before we explicitly set it below. - # This is just us being extra-cautious. - my $column_def = dclone($self->get_column_abstract($table, $column)); - die "Tried to set an fk on $table.$column, but that column doesn't exist" - if !$column_def; - if ($fk_def) { - $column_def->{REFERENCES} = $fk_def; - } - else { - delete $column_def->{REFERENCES}; - } - $self->set_column($table, $column, $column_def); + my ($self, $table, $column, $fk_def) = @_; + + # Don't want to modify the source def before we explicitly set it below. + # This is just us being extra-cautious. + my $column_def = dclone($self->get_column_abstract($table, $column)); + die "Tried to set an fk on $table.$column, but that column doesn't exist" + if !$column_def; + if ($fk_def) { + $column_def->{REFERENCES} = $fk_def; + } + else { + delete $column_def->{REFERENCES}; + } + $self->set_column($table, $column, $column_def); } sub set_index { @@ -2838,36 +2807,39 @@ sub set_index { =cut - my ($self, $table, $name, $definition) = @_; + my ($self, $table, $name, $definition) = @_; - if ( exists $self->{abstract_schema}{$table} - && !exists $self->{abstract_schema}{$table}{INDEXES} ) { - $self->{abstract_schema}{$table}{INDEXES} = []; - } + if (exists $self->{abstract_schema}{$table} + && !exists $self->{abstract_schema}{$table}{INDEXES}) + { + $self->{abstract_schema}{$table}{INDEXES} = []; + } - my $indexes = $self->{abstract_schema}{$table}{INDEXES}; - $self->_set_object($table, $name, $definition, $indexes); + my $indexes = $self->{abstract_schema}{$table}{INDEXES}; + $self->_set_object($table, $name, $definition, $indexes); } # A private helper for set_index and set_column. # This does the actual "work" of those two functions. # $array_to_change is an arrayref. sub _set_object { - my ($self, $table, $name, $definition, $array_to_change) = @_; + my ($self, $table, $name, $definition, $array_to_change) = @_; - my $obj_position = (firstidx { $_ eq $name } @$array_to_change) + 1; - # If the object doesn't exist, then add it. - if (!$obj_position) { - push(@$array_to_change, $name); - push(@$array_to_change, $definition); - } - # We're modifying an existing object in the Schema. - else { - splice(@$array_to_change, $obj_position, 1, $definition); - } + my $obj_position = (firstidx { $_ eq $name } @$array_to_change) + 1; - $self->{schema} = dclone($self->{abstract_schema}); - $self->_adjust_schema(); + # If the object doesn't exist, then add it. + if (!$obj_position) { + push(@$array_to_change, $name); + push(@$array_to_change, $definition); + } + + # We're modifying an existing object in the Schema. + else { + splice(@$array_to_change, $obj_position, 1, $definition); + } + + $self->{schema} = dclone($self->{abstract_schema}); + $self->_adjust_schema(); } =item C @@ -2885,16 +2857,17 @@ sub _set_object { =cut sub delete_index { - my ($self, $table, $name) = @_; - - my $indexes = $self->{abstract_schema}{$table}{INDEXES}; - my $name_position = firstidx { $_ eq $name } @$indexes; - die "Attempted to delete nonexistent index $name on the $table table" - if $name_position == -1; - # Delete the key/value pair from the array. - splice(@$indexes, $name_position, 2); - $self->{schema} = dclone($self->{abstract_schema}); - $self->_adjust_schema(); + my ($self, $table, $name) = @_; + + my $indexes = $self->{abstract_schema}{$table}{INDEXES}; + my $name_position = firstidx { $_ eq $name } @$indexes; + die "Attempted to delete nonexistent index $name on the $table table" + if $name_position == -1; + + # Delete the key/value pair from the array. + splice(@$indexes, $name_position, 2); + $self->{schema} = dclone($self->{abstract_schema}); + $self->_adjust_schema(); } sub columns_equal { @@ -2912,24 +2885,24 @@ sub columns_equal { =cut - my $self = shift; - my $col_one = dclone(shift); - my $col_two = dclone(shift); + my $self = shift; + my $col_one = dclone(shift); + my $col_two = dclone(shift); - $col_one->{TYPE} = uc($col_one->{TYPE}); - $col_two->{TYPE} = uc($col_two->{TYPE}); + $col_one->{TYPE} = uc($col_one->{TYPE}); + $col_two->{TYPE} = uc($col_two->{TYPE}); - # We don't care about foreign keys when comparing column definitions. - delete $col_one->{REFERENCES}; - delete $col_two->{REFERENCES}; + # We don't care about foreign keys when comparing column definitions. + delete $col_one->{REFERENCES}; + delete $col_two->{REFERENCES}; - my @col_one_array = %$col_one; - my @col_two_array = %$col_two; + my @col_one_array = %$col_one; + my @col_two_array = %$col_two; - my ($removed, $added) = diff_arrays(\@col_one_array, \@col_two_array); + my ($removed, $added) = diff_arrays(\@col_one_array, \@col_two_array); - # If there are no differences between the arrays, then they are equal. - return !scalar(@$removed) && !scalar(@$added) ? 1 : 0; + # If there are no differences between the arrays, then they are equal. + return !scalar(@$removed) && !scalar(@$added) ? 1 : 0; } @@ -2953,18 +2926,18 @@ sub columns_equal { =cut sub serialize_abstract { - my ($self) = @_; - - # Make it ok to eval - local $Data::Dumper::Purity = 1; - - # Avoid cross-refs - local $Data::Dumper::Deepcopy = 1; - - # Always sort keys to allow textual compare - local $Data::Dumper::Sortkeys = 1; - - return Dumper($self->{abstract_schema}); + my ($self) = @_; + + # Make it ok to eval + local $Data::Dumper::Purity = 1; + + # Avoid cross-refs + local $Data::Dumper::Deepcopy = 1; + + # Always sort keys to allow textual compare + local $Data::Dumper::Sortkeys = 1; + + return Dumper($self->{abstract_schema}); } =item C @@ -2983,36 +2956,34 @@ sub serialize_abstract { =cut sub deserialize_abstract { - my ($class, $serialized, $version) = @_; - - my $thawed_hash; - if ($version < 2) { - $thawed_hash = thaw($serialized); - } - else { - my $cpt = new Safe; - $cpt->reval($serialized) || - die "Unable to restore cached schema: " . $@; - $thawed_hash = ${$cpt->varglob('VAR1')}; - } - - # Version 2 didn't have the "created" key for REFERENCES items. - if ($version < 3) { - my $standard = $class->new()->{abstract_schema}; - foreach my $table_name (keys %$thawed_hash) { - my %standard_fields = - @{ $standard->{$table_name}->{FIELDS} || [] }; - my $table = $thawed_hash->{$table_name}; - my %fields = @{ $table->{FIELDS} || [] }; - while (my ($field, $def) = each %fields) { - if (exists $def->{REFERENCES}) { - $def->{REFERENCES}->{created} = 1; - } - } + my ($class, $serialized, $version) = @_; + + my $thawed_hash; + if ($version < 2) { + $thawed_hash = thaw($serialized); + } + else { + my $cpt = new Safe; + $cpt->reval($serialized) || die "Unable to restore cached schema: " . $@; + $thawed_hash = ${$cpt->varglob('VAR1')}; + } + + # Version 2 didn't have the "created" key for REFERENCES items. + if ($version < 3) { + my $standard = $class->new()->{abstract_schema}; + foreach my $table_name (keys %$thawed_hash) { + my %standard_fields = @{$standard->{$table_name}->{FIELDS} || []}; + my $table = $thawed_hash->{$table_name}; + my %fields = @{$table->{FIELDS} || []}; + while (my ($field, $def) = each %fields) { + if (exists $def->{REFERENCES}) { + $def->{REFERENCES}->{created} = 1; } + } } + } - return $class->new(undef, $thawed_hash); + return $class->new(undef, $thawed_hash); } ##################################################################### @@ -3040,8 +3011,8 @@ object. =cut sub get_empty_schema { - my ($class) = @_; - return $class->deserialize_abstract(Dumper({}), SCHEMA_VERSION); + my ($class) = @_; + return $class->deserialize_abstract(Dumper({}), SCHEMA_VERSION); } 1; diff --git a/Bugzilla/DB/Schema/Mysql.pm b/Bugzilla/DB/Schema/Mysql.pm index 7ff8ade9f..fe2191486 100644 --- a/Bugzilla/DB/Schema/Mysql.pm +++ b/Bugzilla/DB/Schema/Mysql.pm @@ -21,7 +21,7 @@ use Bugzilla::Error; use parent qw(Bugzilla::DB::Schema); -# This is for column_info_to_column, to know when a tinyint is a +# This is for column_info_to_column, to know when a tinyint is a # boolean and when it's really a tinyint. This only has to be accurate # up to and through 2.19.3, because that's the only time we need # column_info_to_column. @@ -30,50 +30,59 @@ use parent qw(Bugzilla::DB::Schema); # that should be interpreted as a BOOLEAN instead of as an INT1 when # reading in the Schema from the disk. The values are discarded; I just # used "1" for simplicity. -# +# # THIS CONSTANT IS ONLY USED FOR UPGRADES FROM 2.18 OR EARLIER. DON'T # UPDATE IT TO MODERN COLUMN NAMES OR DEFINITIONS. use constant BOOLEAN_MAP => { - bugs => {everconfirmed => 1, reporter_accessible => 1, - cclist_accessible => 1, qacontact_accessible => 1, - assignee_accessible => 1}, - longdescs => {isprivate => 1, already_wrapped => 1}, - attachments => {ispatch => 1, isobsolete => 1, isprivate => 1}, - flags => {is_active => 1}, - flagtypes => {is_active => 1, is_requestable => 1, - is_requesteeble => 1, is_multiplicable => 1}, - fielddefs => {mailhead => 1, obsolete => 1}, - bug_status => {isactive => 1}, - resolution => {isactive => 1}, - bug_severity => {isactive => 1}, - priority => {isactive => 1}, - rep_platform => {isactive => 1}, - op_sys => {isactive => 1}, - profiles => {mybugslink => 1, newemailtech => 1}, - namedqueries => {linkinfooter => 1, watchfordiffs => 1}, - groups => {isbuggroup => 1, isactive => 1}, - group_control_map => {entry => 1, membercontrol => 1, othercontrol => 1, - canedit => 1}, - group_group_map => {isbless => 1}, - user_group_map => {isbless => 1, isderived => 1}, - products => {disallownew => 1}, - series => {public => 1}, - whine_queries => {onemailperbug => 1}, - quips => {approved => 1}, - setting => {is_enabled => 1} + bugs => { + everconfirmed => 1, + reporter_accessible => 1, + cclist_accessible => 1, + qacontact_accessible => 1, + assignee_accessible => 1 + }, + longdescs => {isprivate => 1, already_wrapped => 1}, + attachments => {ispatch => 1, isobsolete => 1, isprivate => 1}, + flags => {is_active => 1}, + flagtypes => { + is_active => 1, + is_requestable => 1, + is_requesteeble => 1, + is_multiplicable => 1 + }, + fielddefs => {mailhead => 1, obsolete => 1}, + bug_status => {isactive => 1}, + resolution => {isactive => 1}, + bug_severity => {isactive => 1}, + priority => {isactive => 1}, + rep_platform => {isactive => 1}, + op_sys => {isactive => 1}, + profiles => {mybugslink => 1, newemailtech => 1}, + namedqueries => {linkinfooter => 1, watchfordiffs => 1}, + groups => {isbuggroup => 1, isactive => 1}, + group_control_map => + {entry => 1, membercontrol => 1, othercontrol => 1, canedit => 1}, + group_group_map => {isbless => 1}, + user_group_map => {isbless => 1, isderived => 1}, + products => {disallownew => 1}, + series => {public => 1}, + whine_queries => {onemailperbug => 1}, + quips => {approved => 1}, + setting => {is_enabled => 1} }; # Maps the db_specific hash backwards, for use in column_info_to_column. use constant REVERSE_MAPPING => { - # Boolean and the SERIAL fields are handled in column_info_to_column, - # and so don't have an entry here. - TINYINT => 'INT1', - SMALLINT => 'INT2', - MEDIUMINT => 'INT3', - INTEGER => 'INT4', - - # All the other types have the same name in their abstract version - # as in their db-specific version, so no reverse mapping is needed. + + # Boolean and the SERIAL fields are handled in column_info_to_column, + # and so don't have an entry here. + TINYINT => 'INT1', + SMALLINT => 'INT2', + MEDIUMINT => 'INT3', + INTEGER => 'INT4', + + # All the other types have the same name in their abstract version + # as in their db-specific version, so no reverse mapping is needed. }; use constant MYISAM_TABLES => qw(bugs_fulltext); @@ -81,181 +90,196 @@ use constant MYISAM_TABLES => qw(bugs_fulltext); #------------------------------------------------------------------------------ sub _initialize { - my $self = shift; + my $self = shift; + + $self = $self->SUPER::_initialize(@_); - $self = $self->SUPER::_initialize(@_); + $self->{db_specific} = { - $self->{db_specific} = { + BOOLEAN => 'tinyint', + FALSE => '0', + TRUE => '1', - BOOLEAN => 'tinyint', - FALSE => '0', - TRUE => '1', + INT1 => 'tinyint', + INT2 => 'smallint', + INT3 => 'mediumint', + INT4 => 'integer', - INT1 => 'tinyint', - INT2 => 'smallint', - INT3 => 'mediumint', - INT4 => 'integer', + SMALLSERIAL => 'smallint auto_increment', + MEDIUMSERIAL => 'mediumint auto_increment', + INTSERIAL => 'integer auto_increment', - SMALLSERIAL => 'smallint auto_increment', - MEDIUMSERIAL => 'mediumint auto_increment', - INTSERIAL => 'integer auto_increment', + TINYTEXT => 'tinytext', + MEDIUMTEXT => 'mediumtext', + LONGTEXT => 'mediumtext', - TINYTEXT => 'tinytext', - MEDIUMTEXT => 'mediumtext', - LONGTEXT => 'mediumtext', + LONGBLOB => 'longblob', - LONGBLOB => 'longblob', + DATETIME => 'datetime', + DATE => 'date', + }; - DATETIME => 'datetime', - DATE => 'date', - }; + $self->_adjust_schema; - $self->_adjust_schema; + return $self; - return $self; +} #eosub--_initialize -} #eosub--_initialize #------------------------------------------------------------------------------ sub _get_create_table_ddl { - # Extend superclass method to specify the MYISAM storage engine. - # Returns a "create table" SQL statement. - my($self, $table) = @_; + # Extend superclass method to specify the MYISAM storage engine. + # Returns a "create table" SQL statement. - my $charset = Bugzilla->dbh->bz_db_is_utf8 ? "CHARACTER SET utf8" : ''; - my $type = grep($_ eq $table, MYISAM_TABLES) ? 'MYISAM' : 'InnoDB'; - return($self->SUPER::_get_create_table_ddl($table) - . " ENGINE = $type $charset"); + my ($self, $table) = @_; + + my $charset = Bugzilla->dbh->bz_db_is_utf8 ? "CHARACTER SET utf8" : ''; + my $type = grep($_ eq $table, MYISAM_TABLES) ? 'MYISAM' : 'InnoDB'; + return ( + $self->SUPER::_get_create_table_ddl($table) . " ENGINE = $type $charset"); + +} #eosub--_get_create_table_ddl -} #eosub--_get_create_table_ddl #------------------------------------------------------------------------------ sub _get_create_index_ddl { - # Extend superclass method to create FULLTEXT indexes on text fields. - # Returns a "create index" SQL statement. - my($self, $table_name, $index_name, $index_fields, $index_type) = @_; + # Extend superclass method to create FULLTEXT indexes on text fields. + # Returns a "create index" SQL statement. + + my ($self, $table_name, $index_name, $index_fields, $index_type) = @_; + + my $sql = "CREATE "; + $sql .= "$index_type " + if ($index_type eq 'UNIQUE' || $index_type eq 'FULLTEXT'); + $sql .= "INDEX \`$index_name\` ON $table_name \(" + . join(", ", @$index_fields) . "\)"; - my $sql = "CREATE "; - $sql .= "$index_type " if ($index_type eq 'UNIQUE' - || $index_type eq 'FULLTEXT'); - $sql .= "INDEX \`$index_name\` ON $table_name \(" . - join(", ", @$index_fields) . "\)"; + return ($sql); - return($sql); +} #eosub--_get_create_index_ddl -} #eosub--_get_create_index_ddl #-------------------------------------------------------------------- sub get_create_database_sql { - my ($self, $name) = @_; - # We only create as utf8 if we have no params (meaning we're doing - # a new installation) or if the utf8 param is on. - my $create_utf8 = Bugzilla->params->{'utf8'} - || !defined Bugzilla->params->{'utf8'}; - my $charset = $create_utf8 ? "CHARACTER SET utf8" : ''; - return ("CREATE DATABASE $name $charset"); + my ($self, $name) = @_; + + # We only create as utf8 if we have no params (meaning we're doing + # a new installation) or if the utf8 param is on. + my $create_utf8 + = Bugzilla->params->{'utf8'} || !defined Bugzilla->params->{'utf8'}; + my $charset = $create_utf8 ? "CHARACTER SET utf8" : ''; + return ("CREATE DATABASE $name $charset"); } # MySQL has a simpler ALTER TABLE syntax than ANSI. sub get_alter_column_ddl { - my ($self, $table, $column, $new_def, $set_nulls_to) = @_; - my $old_def = $self->get_column($table, $column); - my %new_def_copy = %$new_def; - if ($old_def->{PRIMARYKEY} && $new_def->{PRIMARYKEY}) { - # If a column stays a primary key do NOT specify PRIMARY KEY in the - # ALTER TABLE statement. This avoids a MySQL error that two primary - # keys are not allowed. - delete $new_def_copy{PRIMARYKEY}; - } - - my @statements; - - push(@statements, "UPDATE $table SET $column = $set_nulls_to - WHERE $column IS NULL") if defined $set_nulls_to; - - # Calling SET DEFAULT or DROP DEFAULT is *way* faster than calling - # CHANGE COLUMN, so just do that if we're just changing the default. - my %old_defaultless = %$old_def; - my %new_defaultless = %$new_def; - delete $old_defaultless{DEFAULT}; - delete $new_defaultless{DEFAULT}; - if (!$self->columns_equal($old_def, $new_def) - && $self->columns_equal(\%new_defaultless, \%old_defaultless)) - { - if (!defined $new_def->{DEFAULT}) { - push(@statements, - "ALTER TABLE $table ALTER COLUMN $column DROP DEFAULT"); - } - else { - push(@statements, "ALTER TABLE $table ALTER COLUMN $column - SET DEFAULT " . $new_def->{DEFAULT}); - } + my ($self, $table, $column, $new_def, $set_nulls_to) = @_; + my $old_def = $self->get_column($table, $column); + my %new_def_copy = %$new_def; + if ($old_def->{PRIMARYKEY} && $new_def->{PRIMARYKEY}) { + + # If a column stays a primary key do NOT specify PRIMARY KEY in the + # ALTER TABLE statement. This avoids a MySQL error that two primary + # keys are not allowed. + delete $new_def_copy{PRIMARYKEY}; + } + + my @statements; + + push( + @statements, "UPDATE $table SET $column = $set_nulls_to + WHERE $column IS NULL" + ) if defined $set_nulls_to; + + # Calling SET DEFAULT or DROP DEFAULT is *way* faster than calling + # CHANGE COLUMN, so just do that if we're just changing the default. + my %old_defaultless = %$old_def; + my %new_defaultless = %$new_def; + delete $old_defaultless{DEFAULT}; + delete $new_defaultless{DEFAULT}; + if (!$self->columns_equal($old_def, $new_def) + && $self->columns_equal(\%new_defaultless, \%old_defaultless)) + { + if (!defined $new_def->{DEFAULT}) { + push(@statements, "ALTER TABLE $table ALTER COLUMN $column DROP DEFAULT"); } else { - my $new_ddl = $self->get_type_ddl(\%new_def_copy); - push(@statements, "ALTER TABLE $table CHANGE COLUMN - $column $column $new_ddl"); - } - - if ($old_def->{PRIMARYKEY} && !$new_def->{PRIMARYKEY}) { - # Dropping a PRIMARY KEY needs an explicit DROP PRIMARY KEY - push(@statements, "ALTER TABLE $table DROP PRIMARY KEY"); + push( + @statements, "ALTER TABLE $table ALTER COLUMN $column + SET DEFAULT " . $new_def->{DEFAULT} + ); } - - return @statements; + } + else { + my $new_ddl = $self->get_type_ddl(\%new_def_copy); + push( + @statements, "ALTER TABLE $table CHANGE COLUMN + $column $column $new_ddl" + ); + } + + if ($old_def->{PRIMARYKEY} && !$new_def->{PRIMARYKEY}) { + + # Dropping a PRIMARY KEY needs an explicit DROP PRIMARY KEY + push(@statements, "ALTER TABLE $table DROP PRIMARY KEY"); + } + + return @statements; } sub get_drop_fk_sql { - my ($self, $table, $column, $references) = @_; - my $fk_name = $self->_get_fk_name($table, $column, $references); - my @sql = ("ALTER TABLE $table DROP FOREIGN KEY $fk_name"); - my $dbh = Bugzilla->dbh; - - # MySQL requires, and will create, an index on any column with - # an FK. It will name it after the fk, which we never do. - # So if there's an index named after the fk, we also have to delete it. - if ($dbh->bz_index_info_real($table, $fk_name)) { - push(@sql, $self->get_drop_index_ddl($table, $fk_name)); - } - - return @sql; + my ($self, $table, $column, $references) = @_; + my $fk_name = $self->_get_fk_name($table, $column, $references); + my @sql = ("ALTER TABLE $table DROP FOREIGN KEY $fk_name"); + my $dbh = Bugzilla->dbh; + + # MySQL requires, and will create, an index on any column with + # an FK. It will name it after the fk, which we never do. + # So if there's an index named after the fk, we also have to delete it. + if ($dbh->bz_index_info_real($table, $fk_name)) { + push(@sql, $self->get_drop_index_ddl($table, $fk_name)); + } + + return @sql; } sub get_drop_index_ddl { - my ($self, $table, $name) = @_; - return ("DROP INDEX \`$name\` ON $table"); + my ($self, $table, $name) = @_; + return ("DROP INDEX \`$name\` ON $table"); } # A special function for MySQL, for renaming a lot of indexes. -# Index renames is a hash, where the key is a string - the +# Index renames is a hash, where the key is a string - the # old names of the index, and the value is a hash - the index # definition that we're renaming to, with an extra key of "NAME" # that contains the new index name. # The indexes in %indexes must be in hashref format. sub get_rename_indexes_ddl { - my ($self, $table, %indexes) = @_; - my @keys = keys %indexes or return (); - - my $sql = "ALTER TABLE $table "; - - foreach my $old_name (@keys) { - my $name = $indexes{$old_name}->{NAME}; - my $type = $indexes{$old_name}->{TYPE}; - $type ||= 'INDEX'; - my $fields = join(',', @{$indexes{$old_name}->{FIELDS}}); - # $old_name needs to be escaped, sometimes, because it was - # a reserved word. - $old_name = '`' . $old_name . '`'; - $sql .= " ADD $type $name ($fields), DROP INDEX $old_name,"; - } - # Remove the last comma. - chop($sql); - return ($sql); + my ($self, $table, %indexes) = @_; + my @keys = keys %indexes or return (); + + my $sql = "ALTER TABLE $table "; + + foreach my $old_name (@keys) { + my $name = $indexes{$old_name}->{NAME}; + my $type = $indexes{$old_name}->{TYPE}; + $type ||= 'INDEX'; + my $fields = join(',', @{$indexes{$old_name}->{FIELDS}}); + + # $old_name needs to be escaped, sometimes, because it was + # a reserved word. + $old_name = '`' . $old_name . '`'; + $sql .= " ADD $type $name ($fields), DROP INDEX $old_name,"; + } + + # Remove the last comma. + chop($sql); + return ($sql); } sub get_set_serial_sql { - my ($self, $table, $column, $value) = @_; - return ("ALTER TABLE $table AUTO_INCREMENT = $value"); + my ($self, $table, $column, $value) = @_; + return ("ALTER TABLE $table AUTO_INCREMENT = $value"); } # Converts a DBI column_info output to an abstract column definition. @@ -263,124 +287,137 @@ sub get_set_serial_sql { # although there's a chance that it will also work properly if called # elsewhere. sub column_info_to_column { - my ($self, $column_info) = @_; - - # Unfortunately, we have to break Schema's normal "no database" - # barrier a few times in this function. - my $dbh = Bugzilla->dbh; - - my $table = $column_info->{TABLE_NAME}; - my $col_name = $column_info->{COLUMN_NAME}; - - my $column = {}; - - ($column->{NOTNULL} = 1) if $column_info->{NULLABLE} == 0; - - if ($column_info->{mysql_is_pri_key}) { - # In MySQL, if a table has no PK, but it has a UNIQUE index, - # that index will show up as the PK. So we have to eliminate - # that possibility. - # Unfortunately, the only way to definitely solve this is - # to break Schema's standard of not touching the live database - # and check if the index called PRIMARY is on that field. - my $pri_index = $dbh->bz_index_info_real($table, 'PRIMARY'); - if ( $pri_index && grep($_ eq $col_name, @{$pri_index->{FIELDS}}) ) { - $column->{PRIMARYKEY} = 1; - } - } + my ($self, $column_info) = @_; - # MySQL frequently defines a default for a field even when we - # didn't explicitly set one. So we have to have some special - # hacks to determine whether or not we should actually put - # a default in the abstract schema for this field. - if (defined $column_info->{COLUMN_DEF}) { - # The defaults that MySQL inputs automatically are usually - # something that would be considered "false" by perl, either - # a 0 or an empty string. (Except for datetime and decimal - # fields, which have their own special auto-defaults.) - # - # Here's how we handle this: If it exists in the schema - # without a default, then we don't use the default. If it - # doesn't exist in the schema, then we're either going to - # be dropping it soon, or it's a custom end-user column, in which - # case having a bogus default won't harm anything. - my $schema_column = $self->get_column($table, $col_name); - unless ( (!$column_info->{COLUMN_DEF} - || $column_info->{COLUMN_DEF} eq '0000-00-00 00:00:00' - || $column_info->{COLUMN_DEF} eq '0.00') - && $schema_column - && !exists $schema_column->{DEFAULT}) { - - my $default = $column_info->{COLUMN_DEF}; - # Schema uses '0' for the defaults for decimal fields. - $default = 0 if $default =~ /^0\.0+$/; - # If we're not a number, we're a string and need to be - # quoted. - $default = $dbh->quote($default) if !($default =~ /^(-)?([0-9]+)(\.[0-9]+)?$/); - $column->{DEFAULT} = $default; - } - } + # Unfortunately, we have to break Schema's normal "no database" + # barrier a few times in this function. + my $dbh = Bugzilla->dbh; - my $type = $column_info->{TYPE_NAME}; + my $table = $column_info->{TABLE_NAME}; + my $col_name = $column_info->{COLUMN_NAME}; - # Certain types of columns need the size/precision appended. - if ($type =~ /CHAR$/ || $type eq 'DECIMAL') { - # This is nicely lowercase and has the size/precision appended. - $type = $column_info->{mysql_type_name}; - } + my $column = {}; - # If we're a tinyint, we could be either a BOOLEAN or an INT1. - # Only the BOOLEAN_MAP knows the difference. - elsif ($type eq 'TINYINT' && exists BOOLEAN_MAP->{$table} - && exists BOOLEAN_MAP->{$table}->{$col_name}) { - $type = 'BOOLEAN'; - if (exists $column->{DEFAULT}) { - $column->{DEFAULT} = $column->{DEFAULT} ? 'TRUE' : 'FALSE'; - } - } + ($column->{NOTNULL} = 1) if $column_info->{NULLABLE} == 0; - # We also need to check if we're an auto_increment field. - elsif ($type =~ /INT/) { - # Unfortunately, the only way to do this in DBI is to query the - # database, so we have to break the rule here that Schema normally - # doesn't touch the live DB. - my $ref_sth = $dbh->prepare( - "SELECT $col_name FROM $table LIMIT 1"); - $ref_sth->execute; - if ($ref_sth->{mysql_is_auto_increment}->[0]) { - if ($type eq 'MEDIUMINT') { - $type = 'MEDIUMSERIAL'; - } - elsif ($type eq 'SMALLINT') { - $type = 'SMALLSERIAL'; - } - else { - $type = 'INTSERIAL'; - } - } - $ref_sth->finish; + if ($column_info->{mysql_is_pri_key}) { + # In MySQL, if a table has no PK, but it has a UNIQUE index, + # that index will show up as the PK. So we have to eliminate + # that possibility. + # Unfortunately, the only way to definitely solve this is + # to break Schema's standard of not touching the live database + # and check if the index called PRIMARY is on that field. + my $pri_index = $dbh->bz_index_info_real($table, 'PRIMARY'); + if ($pri_index && grep($_ eq $col_name, @{$pri_index->{FIELDS}})) { + $column->{PRIMARYKEY} = 1; } + } + + # MySQL frequently defines a default for a field even when we + # didn't explicitly set one. So we have to have some special + # hacks to determine whether or not we should actually put + # a default in the abstract schema for this field. + if (defined $column_info->{COLUMN_DEF}) { + + # The defaults that MySQL inputs automatically are usually + # something that would be considered "false" by perl, either + # a 0 or an empty string. (Except for datetime and decimal + # fields, which have their own special auto-defaults.) + # + # Here's how we handle this: If it exists in the schema + # without a default, then we don't use the default. If it + # doesn't exist in the schema, then we're either going to + # be dropping it soon, or it's a custom end-user column, in which + # case having a bogus default won't harm anything. + my $schema_column = $self->get_column($table, $col_name); + unless ( + ( + !$column_info->{COLUMN_DEF} + || $column_info->{COLUMN_DEF} eq '0000-00-00 00:00:00' + || $column_info->{COLUMN_DEF} eq '0.00' + ) + && $schema_column + && !exists $schema_column->{DEFAULT} + ) + { - # For all other db-specific types, check if they exist in - # REVERSE_MAPPING and use the type found there. - if (exists REVERSE_MAPPING->{$type}) { - $type = REVERSE_MAPPING->{$type}; + my $default = $column_info->{COLUMN_DEF}; + + # Schema uses '0' for the defaults for decimal fields. + $default = 0 if $default =~ /^0\.0+$/; + + # If we're not a number, we're a string and need to be + # quoted. + $default = $dbh->quote($default) if !($default =~ /^(-)?([0-9]+)(\.[0-9]+)?$/); + $column->{DEFAULT} = $default; + } + } + + my $type = $column_info->{TYPE_NAME}; + + # Certain types of columns need the size/precision appended. + if ($type =~ /CHAR$/ || $type eq 'DECIMAL') { + + # This is nicely lowercase and has the size/precision appended. + $type = $column_info->{mysql_type_name}; + } + + # If we're a tinyint, we could be either a BOOLEAN or an INT1. + # Only the BOOLEAN_MAP knows the difference. + elsif ($type eq 'TINYINT' + && exists BOOLEAN_MAP->{$table} + && exists BOOLEAN_MAP->{$table}->{$col_name}) + { + $type = 'BOOLEAN'; + if (exists $column->{DEFAULT}) { + $column->{DEFAULT} = $column->{DEFAULT} ? 'TRUE' : 'FALSE'; + } + } + + # We also need to check if we're an auto_increment field. + elsif ($type =~ /INT/) { + + # Unfortunately, the only way to do this in DBI is to query the + # database, so we have to break the rule here that Schema normally + # doesn't touch the live DB. + my $ref_sth = $dbh->prepare("SELECT $col_name FROM $table LIMIT 1"); + $ref_sth->execute; + if ($ref_sth->{mysql_is_auto_increment}->[0]) { + if ($type eq 'MEDIUMINT') { + $type = 'MEDIUMSERIAL'; + } + elsif ($type eq 'SMALLINT') { + $type = 'SMALLSERIAL'; + } + else { + $type = 'INTSERIAL'; + } } + $ref_sth->finish; - $column->{TYPE} = $type; + } - #print "$table.$col_name: " . Data::Dumper->Dump([$column]) . "\n"; + # For all other db-specific types, check if they exist in + # REVERSE_MAPPING and use the type found there. + if (exists REVERSE_MAPPING->{$type}) { + $type = REVERSE_MAPPING->{$type}; + } - return $column; + $column->{TYPE} = $type; + + #print "$table.$col_name: " . Data::Dumper->Dump([$column]) . "\n"; + + return $column; } sub get_rename_column_ddl { - my ($self, $table, $old_name, $new_name) = @_; - my $def = $self->get_type_ddl($self->get_column($table, $old_name)); - # MySQL doesn't like having the PRIMARY KEY statement in a rename. - $def =~ s/PRIMARY KEY//i; - return ("ALTER TABLE $table CHANGE COLUMN $old_name $new_name $def"); + my ($self, $table, $old_name, $new_name) = @_; + my $def = $self->get_type_ddl($self->get_column($table, $old_name)); + + # MySQL doesn't like having the PRIMARY KEY statement in a rename. + $def =~ s/PRIMARY KEY//i; + return ("ALTER TABLE $table CHANGE COLUMN $old_name $new_name $def"); } 1; diff --git a/Bugzilla/DB/Schema/Oracle.pm b/Bugzilla/DB/Schema/Oracle.pm index 8fb5479b1..0cf2b6c4b 100644 --- a/Bugzilla/DB/Schema/Oracle.pm +++ b/Bugzilla/DB/Schema/Oracle.pm @@ -21,8 +21,9 @@ use parent qw(Bugzilla::DB::Schema); use Carp qw(confess); use Bugzilla::Util; -use constant ADD_COLUMN => 'ADD'; +use constant ADD_COLUMN => 'ADD'; use constant MULTIPLE_FKS_IN_ALTER => 0; + # Whether this is true or not, this is what it needs to be in order for # hash_identifier to maintain backwards compatibility with versions before # 3.2rc2. @@ -31,123 +32,128 @@ use constant MAX_IDENTIFIER_LEN => 27; #------------------------------------------------------------------------------ sub _initialize { - my $self = shift; + my $self = shift; + + $self = $self->SUPER::_initialize(@_); - $self = $self->SUPER::_initialize(@_); + $self->{db_specific} = { - $self->{db_specific} = { + BOOLEAN => 'integer', + FALSE => '0', + TRUE => '1', - BOOLEAN => 'integer', - FALSE => '0', - TRUE => '1', + INT1 => 'integer', + INT2 => 'integer', + INT3 => 'integer', + INT4 => 'integer', - INT1 => 'integer', - INT2 => 'integer', - INT3 => 'integer', - INT4 => 'integer', + SMALLSERIAL => 'integer', + MEDIUMSERIAL => 'integer', + INTSERIAL => 'integer', - SMALLSERIAL => 'integer', - MEDIUMSERIAL => 'integer', - INTSERIAL => 'integer', + TINYTEXT => 'varchar(255)', + MEDIUMTEXT => 'varchar(4000)', + LONGTEXT => 'clob', - TINYTEXT => 'varchar(255)', - MEDIUMTEXT => 'varchar(4000)', - LONGTEXT => 'clob', + LONGBLOB => 'blob', - LONGBLOB => 'blob', + DATETIME => 'date', + DATE => 'date', + }; - DATETIME => 'date', - DATE => 'date', - }; + $self->_adjust_schema; - $self->_adjust_schema; + return $self; - return $self; +} #eosub--_initialize -} #eosub--_initialize #-------------------------------------------------------------------- sub get_table_ddl { - my $self = shift; - my $table = shift; - unshift @_, $table; - my @ddl = $self->SUPER::get_table_ddl(@_); - - my @fields = @{ $self->{abstract_schema}{$table}{FIELDS} || [] }; - while (@fields) { - my $field_name = shift @fields; - my $field_info = shift @fields; - # Create triggers to deal with empty string. - if ( $field_info->{TYPE} =~ /varchar|TEXT/i - && $field_info->{NOTNULL} ) { - push (@ddl, _get_notnull_trigger_ddl($table, $field_name)); - } - # Create sequences and triggers to emulate SERIAL datatypes. - if ( $field_info->{TYPE} =~ /SERIAL/i ) { - push (@ddl, $self->_get_create_seq_ddl($table, $field_name)); - } + my $self = shift; + my $table = shift; + unshift @_, $table; + my @ddl = $self->SUPER::get_table_ddl(@_); + + my @fields = @{$self->{abstract_schema}{$table}{FIELDS} || []}; + while (@fields) { + my $field_name = shift @fields; + my $field_info = shift @fields; + + # Create triggers to deal with empty string. + if ($field_info->{TYPE} =~ /varchar|TEXT/i && $field_info->{NOTNULL}) { + push(@ddl, _get_notnull_trigger_ddl($table, $field_name)); } - return @ddl; -} #eosub--get_table_ddl + # Create sequences and triggers to emulate SERIAL datatypes. + if ($field_info->{TYPE} =~ /SERIAL/i) { + push(@ddl, $self->_get_create_seq_ddl($table, $field_name)); + } + } + return @ddl; -# Extend superclass method to create Oracle Text indexes if index type +} #eosub--get_table_ddl + +# Extend superclass method to create Oracle Text indexes if index type # is FULLTEXT from schema. Returns a "create index" SQL statement. sub _get_create_index_ddl { - my ($self, $table_name, $index_name, $index_fields, $index_type) = @_; - $index_name = "idx_" . $self->_hash_identifier($index_name); - if ($index_type eq 'FULLTEXT') { - my $sql = "CREATE INDEX $index_name ON $table_name (" - . join(',',@$index_fields) - . ") INDEXTYPE IS CTXSYS.CONTEXT " - . " PARAMETERS('LEXER BZ_LEX SYNC(ON COMMIT)')" ; - return $sql; - } - - return($self->SUPER::_get_create_index_ddl($table_name, $index_name, - $index_fields, $index_type)); + my ($self, $table_name, $index_name, $index_fields, $index_type) = @_; + $index_name = "idx_" . $self->_hash_identifier($index_name); + if ($index_type eq 'FULLTEXT') { + my $sql + = "CREATE INDEX $index_name ON $table_name (" + . join(',', @$index_fields) + . ") INDEXTYPE IS CTXSYS.CONTEXT " + . " PARAMETERS('LEXER BZ_LEX SYNC(ON COMMIT)')"; + return $sql; + } + + return ($self->SUPER::_get_create_index_ddl( + $table_name, $index_name, $index_fields, $index_type + )); } sub get_drop_index_ddl { - my $self = shift; - my ($table, $name) = @_; + my $self = shift; + my ($table, $name) = @_; - $name = 'idx_' . $self->_hash_identifier($name); - return $self->SUPER::get_drop_index_ddl($table, $name); + $name = 'idx_' . $self->_hash_identifier($name); + return $self->SUPER::get_drop_index_ddl($table, $name); } -# Oracle supports the use of FOREIGN KEY integrity constraints +# Oracle supports the use of FOREIGN KEY integrity constraints # to define the referential integrity actions, including: # - Update and delete No Action (default) # - Delete CASCADE # - Delete SET NULL sub get_fk_ddl { - my $self = shift; - my $ddl = $self->SUPER::get_fk_ddl(@_); + my $self = shift; + my $ddl = $self->SUPER::get_fk_ddl(@_); - # iThe Bugzilla Oracle driver implements UPDATE via a trigger. - $ddl =~ s/ON UPDATE \S+//i; - # RESTRICT is the default for DELETE on Oracle and may not be specified. - $ddl =~ s/ON DELETE RESTRICT//i; + # iThe Bugzilla Oracle driver implements UPDATE via a trigger. + $ddl =~ s/ON UPDATE \S+//i; - return $ddl; + # RESTRICT is the default for DELETE on Oracle and may not be specified. + $ddl =~ s/ON DELETE RESTRICT//i; + + return $ddl; } sub get_add_fks_sql { - my $self = shift; - my ($table, $column_fks) = @_; - my @sql = $self->SUPER::get_add_fks_sql(@_); - - foreach my $column (keys %$column_fks) { - my $fk = $column_fks->{$column}; - next if $fk->{UPDATE} && uc($fk->{UPDATE}) ne 'CASCADE'; - my $fk_name = $self->_get_fk_name($table, $column, $fk); - my $to_column = $fk->{COLUMN}; - my $to_table = $fk->{TABLE}; - - my $trigger = <SUPER::get_add_fks_sql(@_); + + foreach my $column (keys %$column_fks) { + my $fk = $column_fks->{$column}; + next if $fk->{UPDATE} && uc($fk->{UPDATE}) ne 'CASCADE'; + my $fk_name = $self->_get_fk_name($table, $column, $fk); + my $to_column = $fk->{COLUMN}; + my $to_table = $fk->{TABLE}; + + my $trigger = <_get_fk_name(@_); - my @sql; - if (!$references->{UPDATE} || $references->{UPDATE} =~ /CASCADE/i) { - push(@sql, "DROP TRIGGER ${fk_name}_uc"); - } - push(@sql, $self->SUPER::get_drop_fk_sql(@_)); - return @sql; + my $self = shift; + my ($table, $column, $references) = @_; + my $fk_name = $self->_get_fk_name(@_); + my @sql; + if (!$references->{UPDATE} || $references->{UPDATE} =~ /CASCADE/i) { + push(@sql, "DROP TRIGGER ${fk_name}_uc"); + } + push(@sql, $self->SUPER::get_drop_fk_sql(@_)); + return @sql; } sub _get_fk_name { - my ($self, $table, $column, $references) = @_; - my $to_table = $references->{TABLE}; - my $to_column = $references->{COLUMN}; - my $fk_name = "${table}_${column}_${to_table}_${to_column}"; - $fk_name = "fk_" . $self->_hash_identifier($fk_name); - - return $fk_name; + my ($self, $table, $column, $references) = @_; + my $to_table = $references->{TABLE}; + my $to_column = $references->{COLUMN}; + my $fk_name = "${table}_${column}_${to_table}_${to_column}"; + $fk_name = "fk_" . $self->_hash_identifier($fk_name); + + return $fk_name; } sub get_add_column_ddl { - my $self = shift; - my ($table, $column, $definition, $init_value) = @_; - my @sql; - - # Create sequences and triggers to emulate SERIAL datatypes. - if ($definition->{TYPE} =~ /SERIAL/i) { - # Clone the definition to not alter the original one. - my %def = %$definition; - # Oracle requires to define the column is several steps. - my $pk = delete $def{PRIMARYKEY}; - my $notnull = delete $def{NOTNULL}; - @sql = $self->SUPER::get_add_column_ddl($table, $column, \%def, $init_value); - push(@sql, $self->_get_create_seq_ddl($table, $column)); - push(@sql, "UPDATE $table SET $column = ${table}_${column}_SEQ.NEXTVAL"); - push(@sql, "ALTER TABLE $table MODIFY $column NOT NULL") if $notnull; - push(@sql, "ALTER TABLE $table ADD PRIMARY KEY ($column)") if $pk; - } - 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)); - } + my $self = shift; + my ($table, $column, $definition, $init_value) = @_; + my @sql; + + # Create sequences and triggers to emulate SERIAL datatypes. + if ($definition->{TYPE} =~ /SERIAL/i) { + + # Clone the definition to not alter the original one. + my %def = %$definition; + + # Oracle requires to define the column is several steps. + my $pk = delete $def{PRIMARYKEY}; + my $notnull = delete $def{NOTNULL}; + @sql = $self->SUPER::get_add_column_ddl($table, $column, \%def, $init_value); + push(@sql, $self->_get_create_seq_ddl($table, $column)); + push(@sql, "UPDATE $table SET $column = ${table}_${column}_SEQ.NEXTVAL"); + push(@sql, "ALTER TABLE $table MODIFY $column NOT NULL") if $notnull; + push(@sql, "ALTER TABLE $table ADD PRIMARY KEY ($column)") if $pk; + } + 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; + return @sql; } sub get_alter_column_ddl { - my ($self, $table, $column, $new_def, $set_nulls_to) = @_; - - my @statements; - my $old_def = $self->get_column_abstract($table, $column); - my $specific = $self->{db_specific}; - - # If the types have changed, we have to deal with that. - if (uc(trim($old_def->{TYPE})) ne uc(trim($new_def->{TYPE}))) { - push(@statements, $self->_get_alter_type_sql($table, $column, - $new_def, $old_def)); - } - - my $default = $new_def->{DEFAULT}; - my $default_old = $old_def->{DEFAULT}; - - if (defined $default) { - $default = $specific->{$default} if exists $specific->{$default}; - } - # This first condition prevents "uninitialized value" errors. - if (!defined $default && !defined $default_old) { - # Do Nothing - } - # If we went from having a default to not having one - elsif (!defined $default && defined $default_old) { - push(@statements, "ALTER TABLE $table MODIFY $column" - . " DEFAULT NULL"); - } - # If we went from no default to a default, or we changed the default. - elsif ( (defined $default && !defined $default_old) || - ($default ne $default_old) ) - { - push(@statements, "ALTER TABLE $table MODIFY $column " - . " DEFAULT $default"); - } - - # If we went from NULL to NOT NULL. - if (!$old_def->{NOTNULL} && $new_def->{NOTNULL}) { - my $setdefault; - # Handle any fields that were NULL before, if we have a default, - $setdefault = $default if defined $default; - # But if we have a set_nulls_to, that overrides the DEFAULT - # (although nobody would usually specify both a default and - # a set_nulls_to.) - $setdefault = $set_nulls_to if defined $set_nulls_to; - if (defined $setdefault) { - push(@statements, "UPDATE $table SET $column = $setdefault" - . " WHERE $column IS NULL"); - } - push(@statements, "ALTER TABLE $table MODIFY $column" - . " NOT NULL"); - push (@statements, _get_notnull_trigger_ddl($table, $column)) - if $old_def->{TYPE} =~ /varchar|text/i - && $new_def->{TYPE} =~ /varchar|text/i; - } - # If we went from NOT NULL to NULL - elsif ($old_def->{NOTNULL} && !$new_def->{NOTNULL}) { - push(@statements, "ALTER TABLE $table MODIFY $column" - . " NULL"); - push(@statements, "DROP TRIGGER ${table}_${column}") - if $new_def->{TYPE} =~ /varchar|text/i - && $old_def->{TYPE} =~ /varchar|text/i; - } - - # If we went from not being a PRIMARY KEY to being a PRIMARY KEY. - if (!$old_def->{PRIMARYKEY} && $new_def->{PRIMARYKEY}) { - push(@statements, "ALTER TABLE $table ADD PRIMARY KEY ($column)"); + my ($self, $table, $column, $new_def, $set_nulls_to) = @_; + + my @statements; + my $old_def = $self->get_column_abstract($table, $column); + my $specific = $self->{db_specific}; + + # If the types have changed, we have to deal with that. + if (uc(trim($old_def->{TYPE})) ne uc(trim($new_def->{TYPE}))) { + push(@statements, + $self->_get_alter_type_sql($table, $column, $new_def, $old_def)); + } + + my $default = $new_def->{DEFAULT}; + my $default_old = $old_def->{DEFAULT}; + + if (defined $default) { + $default = $specific->{$default} if exists $specific->{$default}; + } + + # This first condition prevents "uninitialized value" errors. + if (!defined $default && !defined $default_old) { + + # Do Nothing + } + + # If we went from having a default to not having one + elsif (!defined $default && defined $default_old) { + push(@statements, "ALTER TABLE $table MODIFY $column" . " DEFAULT NULL"); + } + + # If we went from no default to a default, or we changed the default. + elsif ((defined $default && !defined $default_old) + || ($default ne $default_old)) + { + push(@statements, "ALTER TABLE $table MODIFY $column " . " DEFAULT $default"); + } + + # If we went from NULL to NOT NULL. + if (!$old_def->{NOTNULL} && $new_def->{NOTNULL}) { + my $setdefault; + + # Handle any fields that were NULL before, if we have a default, + $setdefault = $default if defined $default; + + # But if we have a set_nulls_to, that overrides the DEFAULT + # (although nobody would usually specify both a default and + # a set_nulls_to.) + $setdefault = $set_nulls_to if defined $set_nulls_to; + if (defined $setdefault) { + push(@statements, + "UPDATE $table SET $column = $setdefault" . " WHERE $column IS NULL"); } - # If we went from being a PK to not being a PK - elsif ( $old_def->{PRIMARYKEY} && !$new_def->{PRIMARYKEY} ) { - push(@statements, "ALTER TABLE $table DROP PRIMARY KEY"); - } - - return @statements; + push(@statements, "ALTER TABLE $table MODIFY $column" . " NOT NULL"); + push(@statements, _get_notnull_trigger_ddl($table, $column)) + if $old_def->{TYPE} =~ /varchar|text/i && $new_def->{TYPE} =~ /varchar|text/i; + } + + # If we went from NOT NULL to NULL + elsif ($old_def->{NOTNULL} && !$new_def->{NOTNULL}) { + push(@statements, "ALTER TABLE $table MODIFY $column" . " NULL"); + push(@statements, "DROP TRIGGER ${table}_${column}") + if $new_def->{TYPE} =~ /varchar|text/i && $old_def->{TYPE} =~ /varchar|text/i; + } + + # If we went from not being a PRIMARY KEY to being a PRIMARY KEY. + if (!$old_def->{PRIMARYKEY} && $new_def->{PRIMARYKEY}) { + push(@statements, "ALTER TABLE $table ADD PRIMARY KEY ($column)"); + } + + # If we went from being a PK to not being a PK + elsif ($old_def->{PRIMARYKEY} && !$new_def->{PRIMARYKEY}) { + push(@statements, "ALTER TABLE $table DROP PRIMARY KEY"); + } + + return @statements; } sub _get_alter_type_sql { - my ($self, $table, $column, $new_def, $old_def) = @_; - my @statements; - - my $type = $new_def->{TYPE}; - $type = $self->{db_specific}->{$type} - if exists $self->{db_specific}->{$type}; - - if ($type =~ /serial/i && $old_def->{TYPE} !~ /serial/i) { - die("You cannot specify a DEFAULT on a SERIAL-type column.") - if $new_def->{DEFAULT}; + my ($self, $table, $column, $new_def, $old_def) = @_; + my @statements; + + my $type = $new_def->{TYPE}; + $type = $self->{db_specific}->{$type} if exists $self->{db_specific}->{$type}; + + if ($type =~ /serial/i && $old_def->{TYPE} !~ /serial/i) { + die("You cannot specify a DEFAULT on a SERIAL-type column.") + if $new_def->{DEFAULT}; + } + + if ( ($old_def->{TYPE} =~ /LONGTEXT/i && $new_def->{TYPE} !~ /LONGTEXT/i) + || ($old_def->{TYPE} !~ /LONGTEXT/i && $new_def->{TYPE} =~ /LONGTEXT/i)) + { + # LONG to VARCHAR or VARCHAR to LONG is not allowed in Oracle, + # just a way to work around. + # Determine whether column_temp is already exist. + my $dbh = Bugzilla->dbh; + my $column_exist = $dbh->selectcol_arrayref( + "SELECT CNAME FROM COL WHERE TNAME = UPPER(?) AND + CNAME = UPPER(?)", undef, $table, $column . "_temp" + ); + if (!@$column_exist) { + push(@statements, "ALTER TABLE $table ADD ${column}_temp $type"); } - - if ( ($old_def->{TYPE} =~ /LONGTEXT/i && $new_def->{TYPE} !~ /LONGTEXT/i) - || ($old_def->{TYPE} !~ /LONGTEXT/i && $new_def->{TYPE} =~ /LONGTEXT/i) - ) { - # LONG to VARCHAR or VARCHAR to LONG is not allowed in Oracle, - # just a way to work around. - # Determine whether column_temp is already exist. - my $dbh=Bugzilla->dbh; - my $column_exist = $dbh->selectcol_arrayref( - "SELECT CNAME FROM COL WHERE TNAME = UPPER(?) AND - CNAME = UPPER(?)", undef,$table,$column . "_temp"); - if(!@$column_exist) { - push(@statements, - "ALTER TABLE $table ADD ${column}_temp $type"); - } - push(@statements, "UPDATE $table SET ${column}_temp = $column"); - push(@statements, "COMMIT"); - push(@statements, "ALTER TABLE $table DROP COLUMN $column"); - push(@statements, - "ALTER TABLE $table RENAME COLUMN ${column}_temp TO $column"); - } else { - push(@statements, "ALTER TABLE $table MODIFY $column $type"); - } - - if ($new_def->{TYPE} =~ /serial/i && $old_def->{TYPE} !~ /serial/i) { - push(@statements, _get_create_seq_ddl($table, $column)); - } - - # If this column is no longer SERIAL, we need to drop the sequence - # that went along with it. - if ($old_def->{TYPE} =~ /serial/i && $new_def->{TYPE} !~ /serial/i) { - push(@statements, "DROP SEQUENCE ${table}_${column}_SEQ"); - push(@statements, "DROP TRIGGER ${table}_${column}_TR"); - } - - # If this column is changed to type TEXT/VARCHAR, we need to deal with - # empty string. - if ( $old_def->{TYPE} !~ /varchar|text/i - && $new_def->{TYPE} =~ /varchar|text/i - && $new_def->{NOTNULL} ) - { - push (@statements, _get_notnull_trigger_ddl($table, $column)); - } - # If this column is no longer TEXT/VARCHAR, we need to drop the trigger - # that went along with it. - if ( $old_def->{TYPE} =~ /varchar|text/i - && $old_def->{NOTNULL} - && $new_def->{TYPE} !~ /varchar|text/i ) - { - push(@statements, "DROP TRIGGER ${table}_${column}"); - } - return @statements; + push(@statements, "UPDATE $table SET ${column}_temp = $column"); + push(@statements, "COMMIT"); + push(@statements, "ALTER TABLE $table DROP COLUMN $column"); + push(@statements, "ALTER TABLE $table RENAME COLUMN ${column}_temp TO $column"); + } + else { + push(@statements, "ALTER TABLE $table MODIFY $column $type"); + } + + if ($new_def->{TYPE} =~ /serial/i && $old_def->{TYPE} !~ /serial/i) { + push(@statements, _get_create_seq_ddl($table, $column)); + } + + # If this column is no longer SERIAL, we need to drop the sequence + # that went along with it. + if ($old_def->{TYPE} =~ /serial/i && $new_def->{TYPE} !~ /serial/i) { + push(@statements, "DROP SEQUENCE ${table}_${column}_SEQ"); + push(@statements, "DROP TRIGGER ${table}_${column}_TR"); + } + + # If this column is changed to type TEXT/VARCHAR, we need to deal with + # empty string. + if ( $old_def->{TYPE} !~ /varchar|text/i + && $new_def->{TYPE} =~ /varchar|text/i + && $new_def->{NOTNULL}) + { + push(@statements, _get_notnull_trigger_ddl($table, $column)); + } + + # If this column is no longer TEXT/VARCHAR, we need to drop the trigger + # that went along with it. + if ( $old_def->{TYPE} =~ /varchar|text/i + && $old_def->{NOTNULL} + && $new_def->{TYPE} !~ /varchar|text/i) + { + push(@statements, "DROP TRIGGER ${table}_${column}"); + } + return @statements; } sub get_rename_column_ddl { - my ($self, $table, $old_name, $new_name) = @_; - if (lc($old_name) eq lc($new_name)) { - # if the only change is a case change, return an empty list. - return (); - } - my @sql = ("ALTER TABLE $table RENAME COLUMN $old_name TO $new_name"); - my $def = $self->get_column_abstract($table, $old_name); - if ($def->{TYPE} =~ /SERIAL/i) { - # We have to rename the series also, and fix the default of the series. - my $old_seq = "${table}_${old_name}_SEQ"; - my $new_seq = "${table}_${new_name}_SEQ"; - push(@sql, "RENAME $old_seq TO $new_seq"); - push(@sql, $self->_get_create_trigger_ddl($table, $new_name, $new_seq)); - push(@sql, "DROP TRIGGER ${table}_${old_name}_TR"); - } - if ($def->{TYPE} =~ /varchar|text/i && $def->{NOTNULL} ) { - push(@sql, _get_notnull_trigger_ddl($table,$new_name)); - push(@sql, "DROP TRIGGER ${table}_${old_name}"); - } - return @sql; + my ($self, $table, $old_name, $new_name) = @_; + if (lc($old_name) eq lc($new_name)) { + + # if the only change is a case change, return an empty list. + return (); + } + my @sql = ("ALTER TABLE $table RENAME COLUMN $old_name TO $new_name"); + my $def = $self->get_column_abstract($table, $old_name); + if ($def->{TYPE} =~ /SERIAL/i) { + + # We have to rename the series also, and fix the default of the series. + my $old_seq = "${table}_${old_name}_SEQ"; + my $new_seq = "${table}_${new_name}_SEQ"; + push(@sql, "RENAME $old_seq TO $new_seq"); + push(@sql, $self->_get_create_trigger_ddl($table, $new_name, $new_seq)); + push(@sql, "DROP TRIGGER ${table}_${old_name}_TR"); + } + if ($def->{TYPE} =~ /varchar|text/i && $def->{NOTNULL}) { + push(@sql, _get_notnull_trigger_ddl($table, $new_name)); + push(@sql, "DROP TRIGGER ${table}_${old_name}"); + } + return @sql; } sub get_drop_column_ddl { - my $self = shift; - my ($table, $column) = @_; - my @sql; - push(@sql, $self->SUPER::get_drop_column_ddl(@_)); - my $dbh=Bugzilla->dbh; - my $trigger_name = uc($table . "_" . $column); - my $exist_trigger = $dbh->selectcol_arrayref( - "SELECT OBJECT_NAME FROM USER_OBJECTS - WHERE OBJECT_NAME = ?", undef, $trigger_name); - if(@$exist_trigger) { - push(@sql, "DROP TRIGGER $trigger_name"); - } - # If this column is of type SERIAL, we need to drop the sequence - # and trigger that went along with it. - my $def = $self->get_column_abstract($table, $column); - if ($def->{TYPE} =~ /SERIAL/i) { - push(@sql, "DROP SEQUENCE ${table}_${column}_SEQ"); - push(@sql, "DROP TRIGGER ${table}_${column}_TR"); - } - return @sql; + my $self = shift; + my ($table, $column) = @_; + my @sql; + push(@sql, $self->SUPER::get_drop_column_ddl(@_)); + my $dbh = Bugzilla->dbh; + my $trigger_name = uc($table . "_" . $column); + my $exist_trigger = $dbh->selectcol_arrayref( + "SELECT OBJECT_NAME FROM USER_OBJECTS + WHERE OBJECT_NAME = ?", undef, $trigger_name + ); + if (@$exist_trigger) { + push(@sql, "DROP TRIGGER $trigger_name"); + } + + # If this column is of type SERIAL, we need to drop the sequence + # and trigger that went along with it. + my $def = $self->get_column_abstract($table, $column); + if ($def->{TYPE} =~ /SERIAL/i) { + push(@sql, "DROP SEQUENCE ${table}_${column}_SEQ"); + push(@sql, "DROP TRIGGER ${table}_${column}_TR"); + } + return @sql; } sub get_rename_table_sql { - my ($self, $old_name, $new_name) = @_; - if (lc($old_name) eq lc($new_name)) { - # if the only change is a case change, return an empty list. - return (); - } + my ($self, $old_name, $new_name) = @_; + if (lc($old_name) eq lc($new_name)) { + + # if the only change is a case change, return an empty list. + return (); + } - my @sql = ("ALTER TABLE $old_name RENAME TO $new_name"); - my @columns = $self->get_table_columns($old_name); - foreach my $column (@columns) { - my $def = $self->get_column_abstract($old_name, $column); - if ($def->{TYPE} =~ /SERIAL/i) { - # If there's a SERIAL column on this table, we also need - # to rename the sequence. - my $old_seq = "${old_name}_${column}_SEQ"; - my $new_seq = "${new_name}_${column}_SEQ"; - push(@sql, "RENAME $old_seq TO $new_seq"); - push(@sql, $self->_get_create_trigger_ddl($new_name, $column, $new_seq)); - push(@sql, "DROP TRIGGER ${old_name}_${column}_TR"); - } - if ($def->{TYPE} =~ /varchar|text/i && $def->{NOTNULL}) { - push(@sql, _get_notnull_trigger_ddl($new_name, $column)); - push(@sql, "DROP TRIGGER ${old_name}_${column}"); - } + my @sql = ("ALTER TABLE $old_name RENAME TO $new_name"); + my @columns = $self->get_table_columns($old_name); + foreach my $column (@columns) { + my $def = $self->get_column_abstract($old_name, $column); + if ($def->{TYPE} =~ /SERIAL/i) { + + # If there's a SERIAL column on this table, we also need + # to rename the sequence. + my $old_seq = "${old_name}_${column}_SEQ"; + my $new_seq = "${new_name}_${column}_SEQ"; + push(@sql, "RENAME $old_seq TO $new_seq"); + push(@sql, $self->_get_create_trigger_ddl($new_name, $column, $new_seq)); + push(@sql, "DROP TRIGGER ${old_name}_${column}_TR"); + } + if ($def->{TYPE} =~ /varchar|text/i && $def->{NOTNULL}) { + push(@sql, _get_notnull_trigger_ddl($new_name, $column)); + push(@sql, "DROP TRIGGER ${old_name}_${column}"); } + } - return @sql; + return @sql; } sub get_drop_table_ddl { - my ($self, $name) = @_; - my @sql; - - my @columns = $self->get_table_columns($name); - foreach my $column (@columns) { - my $def = $self->get_column_abstract($name, $column); - if ($def->{TYPE} =~ /SERIAL/i) { - # If there's a SERIAL column on this table, we also need - # to remove the sequence. - push(@sql, "DROP SEQUENCE ${name}_${column}_SEQ"); - } + my ($self, $name) = @_; + my @sql; + + my @columns = $self->get_table_columns($name); + foreach my $column (@columns) { + my $def = $self->get_column_abstract($name, $column); + if ($def->{TYPE} =~ /SERIAL/i) { + + # If there's a SERIAL column on this table, we also need + # to remove the sequence. + push(@sql, "DROP SEQUENCE ${name}_${column}_SEQ"); } - push(@sql, "DROP TABLE $name CASCADE CONSTRAINTS PURGE"); + } + push(@sql, "DROP TABLE $name CASCADE CONSTRAINTS PURGE"); - return @sql; + return @sql; } sub _get_notnull_trigger_ddl { - my ($table, $column) = @_; - - my $notnull_sql = "CREATE OR REPLACE TRIGGER " - . " ${table}_${column}" - . " BEFORE INSERT OR UPDATE ON ". $table - . " FOR EACH ROW" - . " BEGIN " - . " IF :NEW.". $column ." IS NULL THEN " - . " SELECT '" . Bugzilla::DB::Oracle->EMPTY_STRING - . "' INTO :NEW.". $column ." FROM DUAL; " - . " END IF; " - . " END ".$table.";"; - return $notnull_sql; + my ($table, $column) = @_; + + my $notnull_sql + = "CREATE OR REPLACE TRIGGER " + . " ${table}_${column}" + . " BEFORE INSERT OR UPDATE ON " + . $table + . " FOR EACH ROW" + . " BEGIN " + . " IF :NEW." + . $column + . " IS NULL THEN " + . " SELECT '" + . Bugzilla::DB::Oracle->EMPTY_STRING + . "' INTO :NEW." + . $column + . " FROM DUAL; " + . " END IF; " . " END " + . $table . ";"; + return $notnull_sql; } sub _get_create_seq_ddl { - my ($self, $table, $column, $start_with) = @_; - $start_with ||= 1; - my @ddl; - my $seq_name = "${table}_${column}_SEQ"; - my $seq_sql = "CREATE SEQUENCE $seq_name " - . " INCREMENT BY 1 " - . " START WITH $start_with " - . " NOMAXVALUE " - . " NOCYCLE " - . " NOCACHE"; - push (@ddl, $seq_sql); - push(@ddl, $self->_get_create_trigger_ddl($table, $column, $seq_name)); - - return @ddl; + my ($self, $table, $column, $start_with) = @_; + $start_with ||= 1; + my @ddl; + my $seq_name = "${table}_${column}_SEQ"; + my $seq_sql + = "CREATE SEQUENCE $seq_name " + . " INCREMENT BY 1 " + . " START WITH $start_with " + . " NOMAXVALUE " + . " NOCYCLE " + . " NOCACHE"; + push(@ddl, $seq_sql); + push(@ddl, $self->_get_create_trigger_ddl($table, $column, $seq_name)); + + return @ddl; } sub _get_create_trigger_ddl { - my ($self, $table, $column, $seq_name) = @_; - my $serial_sql = "CREATE OR REPLACE TRIGGER ${table}_${column}_TR " - . " BEFORE INSERT ON $table " - . " FOR EACH ROW " - . " BEGIN " - . " SELECT ${seq_name}.NEXTVAL " - . " INTO :NEW.$column FROM DUAL; " - . " END;"; - return $serial_sql; + my ($self, $table, $column, $seq_name) = @_; + my $serial_sql + = "CREATE OR REPLACE TRIGGER ${table}_${column}_TR " + . " BEFORE INSERT ON $table " + . " FOR EACH ROW " + . " BEGIN " + . " SELECT ${seq_name}.NEXTVAL " + . " INTO :NEW.$column FROM DUAL; " . " END;"; + return $serial_sql; } -sub get_set_serial_sql { - my ($self, $table, $column, $value) = @_; - my @sql; - my $seq_name = "${table}_${column}_SEQ"; - push(@sql, "DROP SEQUENCE ${seq_name}"); - push(@sql, $self->_get_create_seq_ddl($table, $column, $value)); - return @sql; -} +sub get_set_serial_sql { + my ($self, $table, $column, $value) = @_; + my @sql; + my $seq_name = "${table}_${column}_SEQ"; + push(@sql, "DROP SEQUENCE ${seq_name}"); + push(@sql, $self->_get_create_seq_ddl($table, $column, $value)); + return @sql; +} 1; diff --git a/Bugzilla/DB/Schema/Pg.pm b/Bugzilla/DB/Schema/Pg.pm index 55a932272..cf28a02d9 100644 --- a/Bugzilla/DB/Schema/Pg.pm +++ b/Bugzilla/DB/Schema/Pg.pm @@ -23,169 +23,191 @@ use Storable qw(dclone); #------------------------------------------------------------------------------ sub _initialize { - my $self = shift; - - $self = $self->SUPER::_initialize(@_); - - # Remove FULLTEXT index types from the schemas. - foreach my $table (keys %{ $self->{schema} }) { - if ($self->{schema}{$table}{INDEXES}) { - foreach my $index (@{ $self->{schema}{$table}{INDEXES} }) { - if (ref($index) eq 'HASH') { - delete($index->{TYPE}) if (exists $index->{TYPE} - && $index->{TYPE} eq 'FULLTEXT'); - } - } - foreach my $index (@{ $self->{abstract_schema}{$table}{INDEXES} }) { - if (ref($index) eq 'HASH') { - delete($index->{TYPE}) if (exists $index->{TYPE} - && $index->{TYPE} eq 'FULLTEXT'); - } - } + my $self = shift; + + $self = $self->SUPER::_initialize(@_); + + # Remove FULLTEXT index types from the schemas. + foreach my $table (keys %{$self->{schema}}) { + if ($self->{schema}{$table}{INDEXES}) { + foreach my $index (@{$self->{schema}{$table}{INDEXES}}) { + if (ref($index) eq 'HASH') { + delete($index->{TYPE}) + if (exists $index->{TYPE} && $index->{TYPE} eq 'FULLTEXT'); + } + } + foreach my $index (@{$self->{abstract_schema}{$table}{INDEXES}}) { + if (ref($index) eq 'HASH') { + delete($index->{TYPE}) + if (exists $index->{TYPE} && $index->{TYPE} eq 'FULLTEXT'); } + } } + } - $self->{db_specific} = { + $self->{db_specific} = { - BOOLEAN => 'smallint', - FALSE => '0', - TRUE => '1', + BOOLEAN => 'smallint', + FALSE => '0', + TRUE => '1', - INT1 => 'integer', - INT2 => 'integer', - INT3 => 'integer', - INT4 => 'integer', + INT1 => 'integer', + INT2 => 'integer', + INT3 => 'integer', + INT4 => 'integer', - SMALLSERIAL => 'serial unique', - MEDIUMSERIAL => 'serial unique', - INTSERIAL => 'serial unique', + SMALLSERIAL => 'serial unique', + MEDIUMSERIAL => 'serial unique', + INTSERIAL => 'serial unique', - TINYTEXT => 'varchar(255)', - MEDIUMTEXT => 'text', - LONGTEXT => 'text', + TINYTEXT => 'varchar(255)', + MEDIUMTEXT => 'text', + LONGTEXT => 'text', - LONGBLOB => 'bytea', + LONGBLOB => 'bytea', - DATETIME => 'timestamp(0) without time zone', - DATE => 'date', - }; + DATETIME => 'timestamp(0) without time zone', + DATE => 'date', + }; - $self->_adjust_schema; + $self->_adjust_schema; - return $self; + return $self; + +} #eosub--_initialize -} #eosub--_initialize #-------------------------------------------------------------------- sub get_create_database_sql { - my ($self, $name) = @_; - # We only create as utf8 if we have no params (meaning we're doing - # a new installation) or if the utf8 param is on. - my $create_utf8 = Bugzilla->params->{'utf8'} - || !defined Bugzilla->params->{'utf8'}; - my $charset = $create_utf8 ? "ENCODING 'UTF8' TEMPLATE template0" : ''; - return ("CREATE DATABASE $name $charset"); + my ($self, $name) = @_; + + # We only create as utf8 if we have no params (meaning we're doing + # a new installation) or if the utf8 param is on. + my $create_utf8 + = Bugzilla->params->{'utf8'} || !defined Bugzilla->params->{'utf8'}; + my $charset = $create_utf8 ? "ENCODING 'UTF8' TEMPLATE template0" : ''; + return ("CREATE DATABASE $name $charset"); } sub get_rename_column_ddl { - my ($self, $table, $old_name, $new_name) = @_; - if (lc($old_name) eq lc($new_name)) { - # if the only change is a case change, return an empty list, since Pg - # is case-insensitive and will return an error about a duplicate name - return (); - } - my @sql = ("ALTER TABLE $table RENAME COLUMN $old_name TO $new_name"); - my $def = $self->get_column_abstract($table, $old_name); - if ($def->{TYPE} =~ /SERIAL/i) { - # We have to rename the series also. - push(@sql, "ALTER SEQUENCE ${table}_${old_name}_seq - RENAME TO ${table}_${new_name}_seq"); - } - return @sql; + my ($self, $table, $old_name, $new_name) = @_; + if (lc($old_name) eq lc($new_name)) { + + # if the only change is a case change, return an empty list, since Pg + # is case-insensitive and will return an error about a duplicate name + return (); + } + my @sql = ("ALTER TABLE $table RENAME COLUMN $old_name TO $new_name"); + my $def = $self->get_column_abstract($table, $old_name); + if ($def->{TYPE} =~ /SERIAL/i) { + + # We have to rename the series also. + push( + @sql, "ALTER SEQUENCE ${table}_${old_name}_seq + RENAME TO ${table}_${new_name}_seq" + ); + } + return @sql; } sub get_rename_table_sql { - my ($self, $old_name, $new_name) = @_; - if (lc($old_name) eq lc($new_name)) { - # if the only change is a case change, return an empty list, since Pg - # is case-insensitive and will return an error about a duplicate name - return (); + my ($self, $old_name, $new_name) = @_; + if (lc($old_name) eq lc($new_name)) { + + # if the only change is a case change, return an empty list, since Pg + # is case-insensitive and will return an error about a duplicate name + return (); + } + + my @sql = ("ALTER TABLE $old_name RENAME TO $new_name"); + + # If there's a SERIAL column on this table, we also need to rename the + # sequence. + # If there is a PRIMARY KEY, we need to rename it too. + my @columns = $self->get_table_columns($old_name); + foreach my $column (@columns) { + my $def = $self->get_column_abstract($old_name, $column); + if ($def->{TYPE} =~ /SERIAL/i) { + my $old_seq = "${old_name}_${column}_seq"; + my $new_seq = "${new_name}_${column}_seq"; + push(@sql, "ALTER SEQUENCE $old_seq RENAME TO $new_seq"); + push( + @sql, "ALTER TABLE $new_name ALTER COLUMN $column + SET DEFAULT NEXTVAL('$new_seq')" + ); } - - my @sql = ("ALTER TABLE $old_name RENAME TO $new_name"); - - # If there's a SERIAL column on this table, we also need to rename the - # sequence. - # If there is a PRIMARY KEY, we need to rename it too. - my @columns = $self->get_table_columns($old_name); - foreach my $column (@columns) { - my $def = $self->get_column_abstract($old_name, $column); - if ($def->{TYPE} =~ /SERIAL/i) { - my $old_seq = "${old_name}_${column}_seq"; - my $new_seq = "${new_name}_${column}_seq"; - push(@sql, "ALTER SEQUENCE $old_seq RENAME TO $new_seq"); - push(@sql, "ALTER TABLE $new_name ALTER COLUMN $column - SET DEFAULT NEXTVAL('$new_seq')"); - } - if ($def->{PRIMARYKEY}) { - my $old_pk = "${old_name}_pkey"; - my $new_pk = "${new_name}_pkey"; - push(@sql, "ALTER INDEX $old_pk RENAME to $new_pk"); - } + if ($def->{PRIMARYKEY}) { + my $old_pk = "${old_name}_pkey"; + my $new_pk = "${new_name}_pkey"; + push(@sql, "ALTER INDEX $old_pk RENAME to $new_pk"); } + } - return @sql; + return @sql; } sub get_set_serial_sql { - my ($self, $table, $column, $value) = @_; - return ("SELECT setval('${table}_${column}_seq', $value, false) - FROM $table"); + my ($self, $table, $column, $value) = @_; + return ( + "SELECT setval('${table}_${column}_seq', $value, false) + FROM $table" + ); } sub _get_alter_type_sql { - my ($self, $table, $column, $new_def, $old_def) = @_; - my @statements; - - my $type = $new_def->{TYPE}; - $type = $self->{db_specific}->{$type} - if exists $self->{db_specific}->{$type}; - - if ($type =~ /serial/i && $old_def->{TYPE} !~ /serial/i) { - die("You cannot specify a DEFAULT on a SERIAL-type column.") - if $new_def->{DEFAULT}; - } - - $type =~ s/\bserial\b/integer/i; - - # On Pg, you don't need UNIQUE if you're a PK--it creates - # two identical indexes otherwise. - $type =~ s/unique//i if $new_def->{PRIMARYKEY}; - - push(@statements, "ALTER TABLE $table ALTER COLUMN $column - TYPE $type"); - - if ($new_def->{TYPE} =~ /serial/i && $old_def->{TYPE} !~ /serial/i) { - push(@statements, "CREATE SEQUENCE ${table}_${column}_seq - OWNED BY $table.$column"); - push(@statements, "SELECT setval('${table}_${column}_seq', + my ($self, $table, $column, $new_def, $old_def) = @_; + my @statements; + + my $type = $new_def->{TYPE}; + $type = $self->{db_specific}->{$type} if exists $self->{db_specific}->{$type}; + + if ($type =~ /serial/i && $old_def->{TYPE} !~ /serial/i) { + die("You cannot specify a DEFAULT on a SERIAL-type column.") + if $new_def->{DEFAULT}; + } + + $type =~ s/\bserial\b/integer/i; + + # On Pg, you don't need UNIQUE if you're a PK--it creates + # two identical indexes otherwise. + $type =~ s/unique//i if $new_def->{PRIMARYKEY}; + + push( + @statements, "ALTER TABLE $table ALTER COLUMN $column + TYPE $type" + ); + + if ($new_def->{TYPE} =~ /serial/i && $old_def->{TYPE} !~ /serial/i) { + push( + @statements, "CREATE SEQUENCE ${table}_${column}_seq + OWNED BY $table.$column" + ); + push( + @statements, "SELECT setval('${table}_${column}_seq', MAX($table.$column)) - FROM $table"); - push(@statements, "ALTER TABLE $table ALTER COLUMN $column - SET DEFAULT nextval('${table}_${column}_seq')"); - } - - # If this column is no longer SERIAL, we need to drop the sequence - # that went along with it. - if ($old_def->{TYPE} =~ /serial/i && $new_def->{TYPE} !~ /serial/i) { - push(@statements, "ALTER TABLE $table ALTER COLUMN $column - DROP DEFAULT"); - push(@statements, "ALTER SEQUENCE ${table}_${column}_seq - OWNED BY NONE"); - push(@statements, "DROP SEQUENCE ${table}_${column}_seq"); - } - - return @statements; + FROM $table" + ); + push( + @statements, "ALTER TABLE $table ALTER COLUMN $column + SET DEFAULT nextval('${table}_${column}_seq')" + ); + } + + # If this column is no longer SERIAL, we need to drop the sequence + # that went along with it. + if ($old_def->{TYPE} =~ /serial/i && $new_def->{TYPE} !~ /serial/i) { + push( + @statements, "ALTER TABLE $table ALTER COLUMN $column + DROP DEFAULT" + ); + push( + @statements, "ALTER SEQUENCE ${table}_${column}_seq + OWNED BY NONE" + ); + push(@statements, "DROP SEQUENCE ${table}_${column}_seq"); + } + + return @statements; } 1; diff --git a/Bugzilla/DB/Schema/Sqlite.pm b/Bugzilla/DB/Schema/Sqlite.pm index ccdbfd8aa..73ee7cfc5 100644 --- a/Bugzilla/DB/Schema/Sqlite.pm +++ b/Bugzilla/DB/Schema/Sqlite.pm @@ -22,37 +22,37 @@ use constant FK_ON_CREATE => 1; sub _initialize { - my $self = shift; + my $self = shift; - $self = $self->SUPER::_initialize(@_); + $self = $self->SUPER::_initialize(@_); - $self->{db_specific} = { - BOOLEAN => 'integer', - FALSE => '0', - TRUE => '1', + $self->{db_specific} = { + BOOLEAN => 'integer', + FALSE => '0', + TRUE => '1', - INT1 => 'integer', - INT2 => 'integer', - INT3 => 'integer', - INT4 => 'integer', + INT1 => 'integer', + INT2 => 'integer', + INT3 => 'integer', + INT4 => 'integer', - SMALLSERIAL => 'SERIAL', - MEDIUMSERIAL => 'SERIAL', - INTSERIAL => 'SERIAL', + SMALLSERIAL => 'SERIAL', + MEDIUMSERIAL => 'SERIAL', + INTSERIAL => 'SERIAL', - TINYTEXT => 'text', - MEDIUMTEXT => 'text', - LONGTEXT => 'text', + TINYTEXT => 'text', + MEDIUMTEXT => 'text', + LONGTEXT => 'text', - LONGBLOB => 'blob', + LONGBLOB => 'blob', - DATETIME => 'DATETIME', - DATE => 'DATETIME', - }; + DATETIME => 'DATETIME', + DATE => 'DATETIME', + }; - $self->_adjust_schema; + $self->_adjust_schema; - return $self; + return $self; } @@ -61,83 +61,86 @@ sub _initialize { ################################# sub _sqlite_create_table { - my ($self, $table) = @_; - return scalar Bugzilla->dbh->selectrow_array( - "SELECT sql FROM sqlite_master WHERE name = ? AND type = 'table'", - undef, $table); + my ($self, $table) = @_; + return + scalar Bugzilla->dbh->selectrow_array( + "SELECT sql FROM sqlite_master WHERE name = ? AND type = 'table'", + undef, $table); } sub _sqlite_table_lines { - my $self = shift; - my $table_sql = $self->_sqlite_create_table(@_); - $table_sql =~ s/\n*\)$//s; - # The $ makes this work even if people some day add crazy stuff to their - # schema like multi-column foreign keys. - return split(/,\s*$/m, $table_sql); + my $self = shift; + my $table_sql = $self->_sqlite_create_table(@_); + $table_sql =~ s/\n*\)$//s; + + # The $ makes this work even if people some day add crazy stuff to their + # schema like multi-column foreign keys. + return split(/,\s*$/m, $table_sql); } # This does most of the "heavy lifting" of the schema-altering functions. sub _sqlite_alter_schema { - my ($self, $table, $create_table, $options) = @_; - - # $create_table is sometimes an array in the form that _sqlite_table_lines - # returns. - if (ref $create_table) { - $create_table = join(',', @$create_table) . "\n)"; - } - - my $dbh = Bugzilla->dbh; - - my $random = generate_random_password(5); - my $rename_to = "${table}_$random"; - - my @columns = $dbh->bz_table_columns_real($table); - push(@columns, $options->{extra_column}) if $options->{extra_column}; - if (my $exclude = $options->{exclude_column}) { - @columns = grep { $_ ne $exclude } @columns; - } - my @insert_cols = @columns; - my @select_cols = @columns; - if (my $rename = $options->{rename}) { - foreach my $from (keys %$rename) { - my $to = $rename->{$from}; - @insert_cols = map { $_ eq $from ? $to : $_ } @insert_cols; - } + my ($self, $table, $create_table, $options) = @_; + + # $create_table is sometimes an array in the form that _sqlite_table_lines + # returns. + if (ref $create_table) { + $create_table = join(',', @$create_table) . "\n)"; + } + + my $dbh = Bugzilla->dbh; + + my $random = generate_random_password(5); + my $rename_to = "${table}_$random"; + + my @columns = $dbh->bz_table_columns_real($table); + push(@columns, $options->{extra_column}) if $options->{extra_column}; + if (my $exclude = $options->{exclude_column}) { + @columns = grep { $_ ne $exclude } @columns; + } + my @insert_cols = @columns; + my @select_cols = @columns; + if (my $rename = $options->{rename}) { + foreach my $from (keys %$rename) { + my $to = $rename->{$from}; + @insert_cols = map { $_ eq $from ? $to : $_ } @insert_cols; } - - my $insert_str = join(',', @insert_cols); - my $select_str = join(',', @select_cols); - my $copy_sql = "INSERT INTO $table ($insert_str)" - . " SELECT $select_str FROM $rename_to"; - - # We have to turn FKs off before doing this. Otherwise, when we rename - # the table, all of the FKs in the other tables will be automatically - # updated to point to the renamed table. Note that PRAGMA foreign_keys - # can only be set outside of a transaction--otherwise it is a no-op. - if ($dbh->bz_in_transaction) { - die "can't alter the schema inside of a transaction"; - } - my @sql = ( - 'PRAGMA foreign_keys = OFF', - 'BEGIN EXCLUSIVE TRANSACTION', - @{ $options->{pre_sql} || [] }, - "ALTER TABLE $table RENAME TO $rename_to", - $create_table, - $copy_sql, - "DROP TABLE $rename_to", - 'COMMIT TRANSACTION', - 'PRAGMA foreign_keys = ON', - ); + } + + my $insert_str = join(',', @insert_cols); + my $select_str = join(',', @select_cols); + my $copy_sql + = "INSERT INTO $table ($insert_str)" . " SELECT $select_str FROM $rename_to"; + + # We have to turn FKs off before doing this. Otherwise, when we rename + # the table, all of the FKs in the other tables will be automatically + # updated to point to the renamed table. Note that PRAGMA foreign_keys + # can only be set outside of a transaction--otherwise it is a no-op. + if ($dbh->bz_in_transaction) { + die "can't alter the schema inside of a transaction"; + } + my @sql = ( + 'PRAGMA foreign_keys = OFF', + 'BEGIN EXCLUSIVE TRANSACTION', + @{$options->{pre_sql} || []}, + "ALTER TABLE $table RENAME TO $rename_to", + $create_table, + $copy_sql, + "DROP TABLE $rename_to", + 'COMMIT TRANSACTION', + 'PRAGMA foreign_keys = ON', + ); } # For finding a particular column's definition in a CREATE TABLE statement. sub _sqlite_column_regex { - my ($column) = @_; - # 1 = Comma at start - # 2 = Column name + Space - # 3 = Definition - # 4 = Ending comma - return qr/(^|,)(\s\Q$column\E\s+)(.*?)(,|$)/m; + my ($column) = @_; + + # 1 = Comma at start + # 2 = Column name + Space + # 3 = Definition + # 4 = Ending comma + return qr/(^|,)(\s\Q$column\E\s+)(.*?)(,|$)/m; } ############################# @@ -145,133 +148,137 @@ sub _sqlite_column_regex { ############################# sub get_create_database_sql { - # If we get here, it means there was some error creating the - # database file during bz_create_database in Bugzilla::DB, - # and we just want to display that error instead of doing - # anything else. - Bugzilla->dbh; - die "Reached an unreachable point"; + + # If we get here, it means there was some error creating the + # database file during bz_create_database in Bugzilla::DB, + # and we just want to display that error instead of doing + # anything else. + Bugzilla->dbh; + die "Reached an unreachable point"; } sub _get_create_table_ddl { - my $self = shift; - my ($table) = @_; - my $ddl = $self->SUPER::_get_create_table_ddl(@_); - - # TheSchwartz uses its own driver to access its tables, meaning - # that it doesn't understand "COLLATE bugzilla" and in fact - # SQLite throws an error when TheSchwartz tries to access its - # own tables, if COLLATE bugzilla is on them. We don't have - # to fix this elsewhere currently, because we only create - # TheSchwartz's tables, we never modify them. - if ($table =~ /^ts_/) { - $ddl =~ s/ COLLATE bugzilla//g; - } - return $ddl; + my $self = shift; + my ($table) = @_; + my $ddl = $self->SUPER::_get_create_table_ddl(@_); + + # TheSchwartz uses its own driver to access its tables, meaning + # that it doesn't understand "COLLATE bugzilla" and in fact + # SQLite throws an error when TheSchwartz tries to access its + # own tables, if COLLATE bugzilla is on them. We don't have + # to fix this elsewhere currently, because we only create + # TheSchwartz's tables, we never modify them. + if ($table =~ /^ts_/) { + $ddl =~ s/ COLLATE bugzilla//g; + } + return $ddl; } sub get_type_ddl { - my $self = shift; - my $def = dclone($_[0]); - - my $ddl = $self->SUPER::get_type_ddl(@_); - if ($def->{PRIMARYKEY} and $def->{TYPE} =~ /SERIAL/i) { - $ddl =~ s/\bSERIAL\b/integer/; - $ddl =~ s/\bPRIMARY KEY\b/PRIMARY KEY AUTOINCREMENT/; - } - if ($def->{TYPE} =~ /text/i or $def->{TYPE} =~ /char/i) { - $ddl .= " COLLATE bugzilla"; - } - # Don't collate DATETIME fields. - if ($def->{TYPE} eq 'DATETIME') { - $ddl =~ s/\bDATETIME\b/text COLLATE BINARY/; - } - return $ddl; + my $self = shift; + my $def = dclone($_[0]); + + my $ddl = $self->SUPER::get_type_ddl(@_); + if ($def->{PRIMARYKEY} and $def->{TYPE} =~ /SERIAL/i) { + $ddl =~ s/\bSERIAL\b/integer/; + $ddl =~ s/\bPRIMARY KEY\b/PRIMARY KEY AUTOINCREMENT/; + } + if ($def->{TYPE} =~ /text/i or $def->{TYPE} =~ /char/i) { + $ddl .= " COLLATE bugzilla"; + } + + # Don't collate DATETIME fields. + if ($def->{TYPE} eq 'DATETIME') { + $ddl =~ s/\bDATETIME\b/text COLLATE BINARY/; + } + return $ddl; } sub get_alter_column_ddl { - my $self = shift; - my ($table, $column, $new_def, $set_nulls_to) = @_; - my $dbh = Bugzilla->dbh; - - my $table_sql = $self->_sqlite_create_table($table); - my $new_ddl = $self->get_type_ddl($new_def); - # When we do ADD COLUMN, columns can show up all on one line separated - # by commas, so we have to account for that. - my $column_regex = _sqlite_column_regex($column); - $table_sql =~ s/$column_regex/$1$2$new_ddl$4/ - || die "couldn't find $column in $table:\n$table_sql"; - my @pre_sql = $self->_set_nulls_sql(@_); - return $self->_sqlite_alter_schema($table, $table_sql, - { pre_sql => \@pre_sql }); + my $self = shift; + my ($table, $column, $new_def, $set_nulls_to) = @_; + my $dbh = Bugzilla->dbh; + + my $table_sql = $self->_sqlite_create_table($table); + my $new_ddl = $self->get_type_ddl($new_def); + + # When we do ADD COLUMN, columns can show up all on one line separated + # by commas, so we have to account for that. + my $column_regex = _sqlite_column_regex($column); + $table_sql =~ s/$column_regex/$1$2$new_ddl$4/ + || die "couldn't find $column in $table:\n$table_sql"; + my @pre_sql = $self->_set_nulls_sql(@_); + return $self->_sqlite_alter_schema($table, $table_sql, {pre_sql => \@pre_sql}); } sub get_add_column_ddl { - my $self = shift; - my ($table, $column, $definition, $init_value) = @_; - # SQLite can use the normal ADD COLUMN when: - # * The column isn't a PK - if ($definition->{PRIMARYKEY}) { - if ($definition->{NOTNULL} and $definition->{TYPE} !~ /SERIAL/i) { - die "You can only add new SERIAL type PKs with SQLite"; - } - my $table_sql = $self->_sqlite_new_column_sql(@_); - # This works because _sqlite_alter_schema will exclude the new column - # in its INSERT ... SELECT statement, meaning that when the "new" - # table is populated, it will have AUTOINCREMENT values generated - # for it. - return $self->_sqlite_alter_schema($table, $table_sql); - } - # * The column has a default one way or another. Either it - # defaults to NULL (it lacks NOT NULL) or it has a DEFAULT - # clause. Since we also require this when doing bz_add_column (in - # the way of forcing an init_value for NOT NULL columns with no - # default), we first set the init_value as the default and then - # alter the column. - if ($definition->{NOTNULL} and !defined $definition->{DEFAULT}) { - my %with_default = %$definition; - $with_default{DEFAULT} = $init_value; - my @pre_sql = - $self->SUPER::get_add_column_ddl($table, $column, \%with_default); - my $table_sql = $self->_sqlite_new_column_sql(@_); - return $self->_sqlite_alter_schema($table, $table_sql, - { pre_sql => \@pre_sql, extra_column => $column }); + my $self = shift; + my ($table, $column, $definition, $init_value) = @_; + + # SQLite can use the normal ADD COLUMN when: + # * The column isn't a PK + if ($definition->{PRIMARYKEY}) { + if ($definition->{NOTNULL} and $definition->{TYPE} !~ /SERIAL/i) { + die "You can only add new SERIAL type PKs with SQLite"; } - - return $self->SUPER::get_add_column_ddl(@_); + my $table_sql = $self->_sqlite_new_column_sql(@_); + + # This works because _sqlite_alter_schema will exclude the new column + # in its INSERT ... SELECT statement, meaning that when the "new" + # table is populated, it will have AUTOINCREMENT values generated + # for it. + return $self->_sqlite_alter_schema($table, $table_sql); + } + + # * The column has a default one way or another. Either it + # defaults to NULL (it lacks NOT NULL) or it has a DEFAULT + # clause. Since we also require this when doing bz_add_column (in + # the way of forcing an init_value for NOT NULL columns with no + # default), we first set the init_value as the default and then + # alter the column. + if ($definition->{NOTNULL} and !defined $definition->{DEFAULT}) { + my %with_default = %$definition; + $with_default{DEFAULT} = $init_value; + my @pre_sql = $self->SUPER::get_add_column_ddl($table, $column, \%with_default); + my $table_sql = $self->_sqlite_new_column_sql(@_); + return $self->_sqlite_alter_schema($table, $table_sql, + {pre_sql => \@pre_sql, extra_column => $column}); + } + + return $self->SUPER::get_add_column_ddl(@_); } sub _sqlite_new_column_sql { - my ($self, $table, $column, $def) = @_; - my $table_sql = $self->_sqlite_create_table($table); - my $new_ddl = $self->get_type_ddl($def); - my $new_line = "\t$column\t$new_ddl"; - $table_sql =~ s/^(CREATE TABLE \w+ \()/$1\n$new_line,/s - || die "Can't find start of CREATE TABLE:\n$table_sql"; - return $table_sql; + my ($self, $table, $column, $def) = @_; + my $table_sql = $self->_sqlite_create_table($table); + my $new_ddl = $self->get_type_ddl($def); + my $new_line = "\t$column\t$new_ddl"; + $table_sql =~ s/^(CREATE TABLE \w+ \()/$1\n$new_line,/s + || die "Can't find start of CREATE TABLE:\n$table_sql"; + return $table_sql; } sub get_drop_column_ddl { - my ($self, $table, $column) = @_; - my $table_sql = $self->_sqlite_create_table($table); - my $column_regex = _sqlite_column_regex($column); - $table_sql =~ s/$column_regex/$1/ - || die "Can't find column $column: $table_sql"; - # Make sure we don't end up with a comma at the end of the definition. - $table_sql =~ s/,\s+\)$/\n)/s; - return $self->_sqlite_alter_schema($table, $table_sql, - { exclude_column => $column }); + my ($self, $table, $column) = @_; + my $table_sql = $self->_sqlite_create_table($table); + my $column_regex = _sqlite_column_regex($column); + $table_sql =~ s/$column_regex/$1/ + || die "Can't find column $column: $table_sql"; + + # Make sure we don't end up with a comma at the end of the definition. + $table_sql =~ s/,\s+\)$/\n)/s; + return $self->_sqlite_alter_schema($table, $table_sql, + {exclude_column => $column}); } sub get_rename_column_ddl { - my ($self, $table, $old_name, $new_name) = @_; - my $table_sql = $self->_sqlite_create_table($table); - my $column_regex = _sqlite_column_regex($old_name); - $table_sql =~ s/$column_regex/$1\t$new_name\t$3$4/ - || die "Can't find $old_name: $table_sql"; - my %rename = ($old_name => $new_name); - return $self->_sqlite_alter_schema($table, $table_sql, - { rename => \%rename }); + my ($self, $table, $old_name, $new_name) = @_; + my $table_sql = $self->_sqlite_create_table($table); + my $column_regex = _sqlite_column_regex($old_name); + $table_sql =~ s/$column_regex/$1\t$new_name\t$3$4/ + || die "Can't find $old_name: $table_sql"; + my %rename = ($old_name => $new_name); + return $self->_sqlite_alter_schema($table, $table_sql, {rename => \%rename}); } ################ @@ -279,24 +286,23 @@ sub get_rename_column_ddl { ################ sub get_add_fks_sql { - my ($self, $table, $column_fks) = @_; - my @clauses = $self->_sqlite_table_lines($table); - my @add = $self->_column_fks_to_ddl($table, $column_fks); - push(@clauses, @add); - return $self->_sqlite_alter_schema($table, \@clauses); + my ($self, $table, $column_fks) = @_; + my @clauses = $self->_sqlite_table_lines($table); + my @add = $self->_column_fks_to_ddl($table, $column_fks); + push(@clauses, @add); + return $self->_sqlite_alter_schema($table, \@clauses); } sub get_drop_fk_sql { - my ($self, $table, $column, $references) = @_; - my @clauses = $self->_sqlite_table_lines($table); - my $fk_name = $self->_get_fk_name($table, $column, $references); - - my $line_re = qr/^\s+CONSTRAINT $fk_name /s; - grep { $line_re } @clauses - or die "Can't find $fk_name: " . join(',', @clauses); - @clauses = grep { $_ !~ $line_re } @clauses; - - return $self->_sqlite_alter_schema($table, \@clauses); + my ($self, $table, $column, $references) = @_; + my @clauses = $self->_sqlite_table_lines($table); + my $fk_name = $self->_get_fk_name($table, $column, $references); + + my $line_re = qr/^\s+CONSTRAINT $fk_name /s; + grep {$line_re} @clauses or die "Can't find $fk_name: " . join(',', @clauses); + @clauses = grep { $_ !~ $line_re } @clauses; + + return $self->_sqlite_alter_schema($table, \@clauses); } diff --git a/Bugzilla/DB/Sqlite.pm b/Bugzilla/DB/Sqlite.pm index a56ed31ad..68180414c 100644 --- a/Bugzilla/DB/Sqlite.pm +++ b/Bugzilla/DB/Sqlite.pm @@ -46,23 +46,23 @@ sub _sqlite_collate_ci { lc($_[0]) cmp lc($_[1]) } sub _sqlite_mod { $_[0] % $_[1] } sub _sqlite_now { - my $now = DateTime->now(time_zone => Bugzilla->local_timezone); - return $now->ymd . ' ' . $now->hms; + my $now = DateTime->now(time_zone => Bugzilla->local_timezone); + return $now->ymd . ' ' . $now->hms; } # SQL's POSITION starts its values from 1 instead of 0 (so we add 1). sub _sqlite_position { - my ($text, $fragment) = @_; - if (!defined $text or !defined $fragment) { - return undef; - } - my $pos = index $text, $fragment; - return $pos + 1; + my ($text, $fragment) = @_; + if (!defined $text or !defined $fragment) { + return undef; + } + my $pos = index $text, $fragment; + return $pos + 1; } sub _sqlite_position_ci { - my ($text, $fragment) = @_; - return _sqlite_position(lc($text), lc($fragment)); + my ($text, $fragment) = @_; + return _sqlite_position(lc($text), lc($fragment)); } ############### @@ -70,76 +70,84 @@ sub _sqlite_position_ci { ############### sub new { - my ($class, $params) = @_; - my $db_name = $params->{db_name}; - - # Let people specify paths intead of data/ for the DB. - if ($db_name and $db_name !~ m{[\\/]}) { - # When the DB is first created, there's a chance that the - # data directory doesn't exist at all, because the Install::Filesystem - # code happens after DB creation. So we create the directory ourselves - # if it doesn't exist. - my $datadir = bz_locations()->{datadir}; - if (!-d $datadir) { - mkdir $datadir or warn "$datadir: $!"; - } - if (!-d "$datadir/db/") { - mkdir "$datadir/db/" or warn "$datadir/db: $!"; - } - $db_name = bz_locations()->{datadir} . "/db/$db_name"; + my ($class, $params) = @_; + my $db_name = $params->{db_name}; + + # Let people specify paths intead of data/ for the DB. + if ($db_name and $db_name !~ m{[\\/]}) { + + # When the DB is first created, there's a chance that the + # data directory doesn't exist at all, because the Install::Filesystem + # code happens after DB creation. So we create the directory ourselves + # if it doesn't exist. + my $datadir = bz_locations()->{datadir}; + if (!-d $datadir) { + mkdir $datadir or warn "$datadir: $!"; } - - # construct the DSN from the parameters we got - my $dsn = "dbi:SQLite:dbname=$db_name"; - - my $attrs = { - # XXX Should we just enforce this to be always on? - sqlite_unicode => Bugzilla->params->{'utf8'}, - }; - - my $self = $class->db_new({ dsn => $dsn, user => '', - pass => '', attrs => $attrs }); - # Needed by TheSchwartz - $self->{private_bz_dsn} = $dsn; - - my %pragmas = ( - # Make sure that the sqlite file doesn't grow without bound. - auto_vacuum => 1, - encoding => "'UTF-8'", - foreign_keys => 'ON', - # We want the latest file format. - legacy_file_format => 'OFF', - # This guarantees that we get column names like "foo" - # instead of "table.foo" in selectrow_hashref. - short_column_names => 'ON', - # The write-ahead log mode in SQLite 3.7 gets us better concurrency, - # but breaks backwards-compatibility with older versions of - # SQLite. (Which is important because people may also want to use - # command-line clients to access and back up their DB.) If you need - # better concurrency and don't need 3.6 compatibility, then you can - # uncomment this line. - #journal_mode => "'WAL'", - ); - - while (my ($name, $value) = each %pragmas) { - $self->do("PRAGMA $name = $value"); + if (!-d "$datadir/db/") { + mkdir "$datadir/db/" or warn "$datadir/db: $!"; } - - $self->sqlite_create_collation('bugzilla', \&_sqlite_collate_ci); - $self->sqlite_create_function('position', 2, \&_sqlite_position); - $self->sqlite_create_function('iposition', 2, \&_sqlite_position_ci); - # SQLite has a "substr" function, but other DBs call it "SUBSTRING" - # so that's what we use, and I don't know of any way in SQLite to - # alias the SQL "substr" function to be called "SUBSTRING". - $self->sqlite_create_function('substring', 3, \&CORE::substr); - $self->sqlite_create_function('char_length', 1, sub { length($_[0]) }); - $self->sqlite_create_function('mod', 2, \&_sqlite_mod); - $self->sqlite_create_function('now', 0, \&_sqlite_now); - $self->sqlite_create_function('localtimestamp', 1, \&_sqlite_now); - $self->sqlite_create_function('floor', 1, \&POSIX::floor); - - bless ($self, $class); - return $self; + $db_name = bz_locations()->{datadir} . "/db/$db_name"; + } + + # construct the DSN from the parameters we got + my $dsn = "dbi:SQLite:dbname=$db_name"; + + my $attrs = { + + # XXX Should we just enforce this to be always on? + sqlite_unicode => Bugzilla->params->{'utf8'}, + }; + + my $self + = $class->db_new({dsn => $dsn, user => '', pass => '', attrs => $attrs}); + + # Needed by TheSchwartz + $self->{private_bz_dsn} = $dsn; + + my %pragmas = ( + + # Make sure that the sqlite file doesn't grow without bound. + auto_vacuum => 1, + encoding => "'UTF-8'", + foreign_keys => 'ON', + + # We want the latest file format. + legacy_file_format => 'OFF', + + # This guarantees that we get column names like "foo" + # instead of "table.foo" in selectrow_hashref. + short_column_names => 'ON', + + # The write-ahead log mode in SQLite 3.7 gets us better concurrency, + # but breaks backwards-compatibility with older versions of + # SQLite. (Which is important because people may also want to use + # command-line clients to access and back up their DB.) If you need + # better concurrency and don't need 3.6 compatibility, then you can + # uncomment this line. + #journal_mode => "'WAL'", + ); + + while (my ($name, $value) = each %pragmas) { + $self->do("PRAGMA $name = $value"); + } + + $self->sqlite_create_collation('bugzilla', \&_sqlite_collate_ci); + $self->sqlite_create_function('position', 2, \&_sqlite_position); + $self->sqlite_create_function('iposition', 2, \&_sqlite_position_ci); + + # SQLite has a "substr" function, but other DBs call it "SUBSTRING" + # so that's what we use, and I don't know of any way in SQLite to + # alias the SQL "substr" function to be called "SUBSTRING". + $self->sqlite_create_function('substring', 3, \&CORE::substr); + $self->sqlite_create_function('char_length', 1, sub { length($_[0]) }); + $self->sqlite_create_function('mod', 2, \&_sqlite_mod); + $self->sqlite_create_function('now', 0, \&_sqlite_now); + $self->sqlite_create_function('localtimestamp', 1, \&_sqlite_now); + $self->sqlite_create_function('floor', 1, \&POSIX::floor); + + bless($self, $class); + return $self; } ############### @@ -147,86 +155,89 @@ sub new { ############### sub sql_position { - my ($self, $fragment, $text) = @_; - return "POSITION($text, $fragment)"; + my ($self, $fragment, $text) = @_; + return "POSITION($text, $fragment)"; } sub sql_iposition { - my ($self, $fragment, $text) = @_; - return "IPOSITION($text, $fragment)"; + my ($self, $fragment, $text) = @_; + return "IPOSITION($text, $fragment)"; } # SQLite does not have to GROUP BY the optional columns. sub sql_group_by { - my ($self, $needed_columns, $optional_columns) = @_; - my $expression = "GROUP BY $needed_columns"; - return $expression; + my ($self, $needed_columns, $optional_columns) = @_; + my $expression = "GROUP BY $needed_columns"; + return $expression; } # XXX SQLite does not support sorting a GROUP_CONCAT, so $sort is unimplemented. sub sql_group_concat { - my ($self, $column, $separator, $sort) = @_; - $separator = $self->quote(', ') if !defined $separator; - # In SQLite, a GROUP_CONCAT call with a DISTINCT argument can't - # specify its separator, and has to accept the default of ",". - if ($column =~ /^DISTINCT/) { - return "GROUP_CONCAT($column)"; - } - return "GROUP_CONCAT($column, $separator)"; + my ($self, $column, $separator, $sort) = @_; + $separator = $self->quote(', ') if !defined $separator; + + # In SQLite, a GROUP_CONCAT call with a DISTINCT argument can't + # specify its separator, and has to accept the default of ",". + if ($column =~ /^DISTINCT/) { + return "GROUP_CONCAT($column)"; + } + return "GROUP_CONCAT($column, $separator)"; } sub sql_istring { - my ($self, $string) = @_; - return $string; + my ($self, $string) = @_; + return $string; } sub sql_regexp { - my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_; - $real_pattern ||= $pattern; + my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_; + $real_pattern ||= $pattern; - $self->bz_check_regexp($real_pattern) if !$nocheck; + $self->bz_check_regexp($real_pattern) if !$nocheck; - return "$expr REGEXP $pattern"; + return "$expr REGEXP $pattern"; } sub sql_not_regexp { - my $self = shift; - my $re_expression = $self->sql_regexp(@_); - return "NOT($re_expression)"; + my $self = shift; + my $re_expression = $self->sql_regexp(@_); + return "NOT($re_expression)"; } sub sql_limit { - my ($self, $limit, $offset) = @_; - - if (defined($offset)) { - return "LIMIT $limit OFFSET $offset"; - } else { - return "LIMIT $limit"; - } + my ($self, $limit, $offset) = @_; + + if (defined($offset)) { + return "LIMIT $limit OFFSET $offset"; + } + else { + return "LIMIT $limit"; + } } sub sql_from_days { - my ($self, $days) = @_; - return "DATETIME($days)"; + my ($self, $days) = @_; + return "DATETIME($days)"; } sub sql_to_days { - my ($self, $date) = @_; - return "JULIANDAY($date)"; + my ($self, $date) = @_; + return "JULIANDAY($date)"; } 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)"; + 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)"; } sub sql_date_math { - my ($self, $date, $operator, $interval, $units) = @_; - # We do the || thing (concatenation) so that placeholders work properly. - return "DATETIME($date, '$operator' || $interval || ' $units')"; + my ($self, $date, $operator, $interval, $units) = @_; + + # We do the || thing (concatenation) so that placeholders work properly. + return "DATETIME($date, '$operator' || $interval || ' $units')"; } ############### @@ -234,56 +245,57 @@ sub sql_date_math { ############### sub bz_setup_database { - my $self = shift; - $self->SUPER::bz_setup_database(@_); - - # If we created TheSchwartz tables with COLLATE bugzilla (during the - # 4.1.x development series) re-create them without it. - my @tables = $self->bz_table_list(); - my @ts_tables = grep { /^ts_/ } @tables; - my $drop_ok; - foreach my $table (@ts_tables) { - my $create_table = - $self->_bz_real_schema->_sqlite_create_table($table); - if ($create_table =~ /COLLATE bugzilla/) { - if (!$drop_ok) { - _sqlite_jobqueue_drop_message(); - $drop_ok = 1; - } - $self->bz_drop_table($table); - $self->bz_add_table($table); - } + my $self = shift; + $self->SUPER::bz_setup_database(@_); + + # If we created TheSchwartz tables with COLLATE bugzilla (during the + # 4.1.x development series) re-create them without it. + my @tables = $self->bz_table_list(); + my @ts_tables = grep {/^ts_/} @tables; + my $drop_ok; + foreach my $table (@ts_tables) { + my $create_table = $self->_bz_real_schema->_sqlite_create_table($table); + if ($create_table =~ /COLLATE bugzilla/) { + if (!$drop_ok) { + _sqlite_jobqueue_drop_message(); + $drop_ok = 1; + } + $self->bz_drop_table($table); + $self->bz_add_table($table); } + } } sub _sqlite_jobqueue_drop_message { - # This is not translated because this situation will only happen if - # you are updating from a 4.1.x development version of Bugzilla using - # SQLite, and we don't want to maintain this string in strings.txt.pl - # forever for just this one uncommon circumstance. - print <installation_answers->{NO_PAUSE}) { - print install_string('enter_or_ctrl_c'); - getc; - } + unless (Bugzilla->installation_answers->{NO_PAUSE}) { + print install_string('enter_or_ctrl_c'); + getc; + } } # XXX This needs to be implemented. sub bz_explain { } sub bz_table_list_real { - my $self = shift; - my @tables = $self->SUPER::bz_table_list_real(@_); - # SQLite includes a sqlite_sequence table in every database that isn't - # one of our real tables. We exclude any table that starts with sqlite_, - # just to be safe. - @tables = grep { $_ !~ /^sqlite_/ } @tables; - return @tables; + my $self = shift; + my @tables = $self->SUPER::bz_table_list_real(@_); + + # SQLite includes a sqlite_sequence table in every database that isn't + # one of our real tables. We exclude any table that starts with sqlite_, + # just to be safe. + @tables = grep { $_ !~ /^sqlite_/ } @tables; + return @tables; } 1; diff --git a/Bugzilla/Error.pm b/Bugzilla/Error.pm index ef6320d15..ac56b9b02 100644 --- a/Bugzilla/Error.pm +++ b/Bugzilla/Error.pm @@ -26,176 +26,185 @@ use Date::Format; # We cannot use $^S to detect if we are in an eval(), because mod_perl # already eval'uates everything, so $^S = 1 in all cases under mod_perl! sub _in_eval { - my $in_eval = 0; - for (my $stack = 1; my $sub = (caller($stack))[3]; $stack++) { - last if $sub =~ /^ModPerl/; - $in_eval = 1 if $sub =~ /^\(eval\)/; - } - return $in_eval; + my $in_eval = 0; + for (my $stack = 1; my $sub = (caller($stack))[3]; $stack++) { + last if $sub =~ /^ModPerl/; + $in_eval = 1 if $sub =~ /^\(eval\)/; + } + return $in_eval; } sub _throw_error { - my ($name, $error, $vars) = @_; - my $dbh = Bugzilla->dbh; - $vars ||= {}; - - $vars->{error} = $error; - - # Make sure any transaction is rolled back (if supported). - # If we are within an eval(), do not roll back transactions as we are - # eval'uating some test on purpose. - $dbh->bz_rollback_transaction() if ($dbh->bz_in_transaction() && !_in_eval()); - - my $datadir = bz_locations()->{'datadir'}; - # If a writable $datadir/errorlog exists, log error details there. - if (-w "$datadir/errorlog") { - require Bugzilla::Util; - require Data::Dumper; - my $mesg = ""; - for (1..75) { $mesg .= "-"; }; - $mesg .= "\n[$$] " . time2str("%D %H:%M:%S ", time()); - $mesg .= "$name $error "; - $mesg .= Bugzilla::Util::remote_ip(); - $mesg .= Bugzilla->user->login; - $mesg .= (' actually ' . Bugzilla->sudoer->login) if Bugzilla->sudoer; - $mesg .= "\n"; - my %params = Bugzilla->cgi->Vars; - $Data::Dumper::Useqq = 1; - for my $param (sort keys %params) { - my $val = $params{$param}; - # obscure passwords - $val = "*****" if $param =~ /password/i; - # limit line length - $val =~ s/^(.{512}).*$/$1\[CHOP\]/; - $mesg .= "[$$] " . Data::Dumper->Dump([$val],["param($param)"]); - } - for my $var (sort keys %ENV) { - my $val = $ENV{$var}; - $val = "*****" if $val =~ /password|http_pass/i; - $mesg .= "[$$] " . Data::Dumper->Dump([$val],["env($var)"]); - } - open(ERRORLOGFID, ">>", "$datadir/errorlog"); - print ERRORLOGFID "$mesg\n"; - close ERRORLOGFID; - } - - my $template = Bugzilla->template; - my $message; - # There are some tests that throw and catch a lot of errors, - # and calling $template->process over and over for those errors - # is too slow. So instead, we just "die" with a dump of the arguments. - if (Bugzilla->error_mode != ERROR_MODE_TEST) { - $template->process($name, $vars, \$message) - || ThrowTemplateError($template->error()); + my ($name, $error, $vars) = @_; + my $dbh = Bugzilla->dbh; + $vars ||= {}; + + $vars->{error} = $error; + + # Make sure any transaction is rolled back (if supported). + # If we are within an eval(), do not roll back transactions as we are + # eval'uating some test on purpose. + $dbh->bz_rollback_transaction() if ($dbh->bz_in_transaction() && !_in_eval()); + + my $datadir = bz_locations()->{'datadir'}; + + # If a writable $datadir/errorlog exists, log error details there. + if (-w "$datadir/errorlog") { + require Bugzilla::Util; + require Data::Dumper; + my $mesg = ""; + for (1 .. 75) { $mesg .= "-"; } + $mesg .= "\n[$$] " . time2str("%D %H:%M:%S ", time()); + $mesg .= "$name $error "; + $mesg .= Bugzilla::Util::remote_ip(); + $mesg .= Bugzilla->user->login; + $mesg .= (' actually ' . Bugzilla->sudoer->login) if Bugzilla->sudoer; + $mesg .= "\n"; + my %params = Bugzilla->cgi->Vars; + $Data::Dumper::Useqq = 1; + + for my $param (sort keys %params) { + my $val = $params{$param}; + + # obscure passwords + $val = "*****" if $param =~ /password/i; + + # limit line length + $val =~ s/^(.{512}).*$/$1\[CHOP\]/; + $mesg .= "[$$] " . Data::Dumper->Dump([$val], ["param($param)"]); } - - # Let's call the hook first, so that extensions can override - # or extend the default behavior, or add their own error codes. - Bugzilla::Hook::process('error_catch', { error => $error, vars => $vars, - message => \$message }); - - if (Bugzilla->error_mode == ERROR_MODE_WEBPAGE) { - my $cgi = Bugzilla->cgi; - $cgi->close_standby_message('text/html', 'inline', 'error', 'html'); - print $message; - print $cgi->multipart_final() if $cgi->{_multipart_in_progress}; + for my $var (sort keys %ENV) { + my $val = $ENV{$var}; + $val = "*****" if $val =~ /password|http_pass/i; + $mesg .= "[$$] " . Data::Dumper->Dump([$val], ["env($var)"]); } - elsif (Bugzilla->error_mode == ERROR_MODE_TEST) { - die Dumper($vars); + open(ERRORLOGFID, ">>", "$datadir/errorlog"); + print ERRORLOGFID "$mesg\n"; + close ERRORLOGFID; + } + + my $template = Bugzilla->template; + my $message; + + # There are some tests that throw and catch a lot of errors, + # and calling $template->process over and over for those errors + # is too slow. So instead, we just "die" with a dump of the arguments. + if (Bugzilla->error_mode != ERROR_MODE_TEST) { + $template->process($name, $vars, \$message) + || ThrowTemplateError($template->error()); + } + + # Let's call the hook first, so that extensions can override + # or extend the default behavior, or add their own error codes. + Bugzilla::Hook::process('error_catch', + {error => $error, vars => $vars, message => \$message}); + + if (Bugzilla->error_mode == ERROR_MODE_WEBPAGE) { + my $cgi = Bugzilla->cgi; + $cgi->close_standby_message('text/html', 'inline', 'error', 'html'); + print $message; + print $cgi->multipart_final() if $cgi->{_multipart_in_progress}; + } + elsif (Bugzilla->error_mode == ERROR_MODE_TEST) { + die Dumper($vars); + } + elsif (Bugzilla->error_mode == ERROR_MODE_DIE) { + die("$message\n"); + } + elsif (Bugzilla->error_mode == ERROR_MODE_DIE_SOAP_FAULT + || Bugzilla->error_mode == ERROR_MODE_JSON_RPC + || Bugzilla->error_mode == ERROR_MODE_REST) + { + # Clone the hash so we aren't modifying the constant. + my %error_map = %{WS_ERROR_CODE()}; + Bugzilla::Hook::process('webservice_error_codes', {error_map => \%error_map}); + my $code = $error_map{$error}; + if (!$code) { + $code = ERROR_UNKNOWN_FATAL if $name =~ /code/i; + $code = ERROR_UNKNOWN_TRANSIENT if $name =~ /user/i; } - elsif (Bugzilla->error_mode == ERROR_MODE_DIE) { - die("$message\n"); + + if (Bugzilla->error_mode == ERROR_MODE_DIE_SOAP_FAULT) { + die SOAP::Fault->faultcode($code)->faultstring($message); } - elsif (Bugzilla->error_mode == ERROR_MODE_DIE_SOAP_FAULT - || Bugzilla->error_mode == ERROR_MODE_JSON_RPC - || Bugzilla->error_mode == ERROR_MODE_REST) - { - # Clone the hash so we aren't modifying the constant. - my %error_map = %{ WS_ERROR_CODE() }; - Bugzilla::Hook::process('webservice_error_codes', - { error_map => \%error_map }); - my $code = $error_map{$error}; - if (!$code) { - $code = ERROR_UNKNOWN_FATAL if $name =~ /code/i; - $code = ERROR_UNKNOWN_TRANSIENT if $name =~ /user/i; - } - - if (Bugzilla->error_mode == ERROR_MODE_DIE_SOAP_FAULT) { - die SOAP::Fault->faultcode($code)->faultstring($message); - } - else { - my $server = Bugzilla->_json_server; - - my $status_code = 0; - if (Bugzilla->error_mode == ERROR_MODE_REST) { - my %status_code_map = %{ REST_STATUS_CODE_MAP() }; - $status_code = $status_code_map{$code} || $status_code_map{'_default'}; - } - # Technically JSON-RPC isn't allowed to have error numbers - # higher than 999, but we do this to avoid conflicts with - # the internal JSON::RPC error codes. - $server->raise_error(code => 100000 + $code, - status_code => $status_code, - message => $message, - id => $server->{_bz_request_id}, - version => $server->version); - # Most JSON-RPC Throw*Error calls happen within an eval inside - # of JSON::RPC. So, in that circumstance, instead of exiting, - # we die with no message. JSON::RPC checks raise_error before - # it checks $@, so it returns the proper error. - die if _in_eval(); - $server->response($server->error_response_header); - } + else { + my $server = Bugzilla->_json_server; + + my $status_code = 0; + if (Bugzilla->error_mode == ERROR_MODE_REST) { + my %status_code_map = %{REST_STATUS_CODE_MAP()}; + $status_code = $status_code_map{$code} || $status_code_map{'_default'}; + } + + # Technically JSON-RPC isn't allowed to have error numbers + # higher than 999, but we do this to avoid conflicts with + # the internal JSON::RPC error codes. + $server->raise_error( + code => 100000 + $code, + status_code => $status_code, + message => $message, + id => $server->{_bz_request_id}, + version => $server->version + ); + + # Most JSON-RPC Throw*Error calls happen within an eval inside + # of JSON::RPC. So, in that circumstance, instead of exiting, + # we die with no message. JSON::RPC checks raise_error before + # it checks $@, so it returns the proper error. + die if _in_eval(); + $server->response($server->error_response_header); } - exit; + } + exit; } sub ThrowUserError { - _throw_error("global/user-error.html.tmpl", @_); + _throw_error("global/user-error.html.tmpl", @_); } sub ThrowCodeError { - my (undef, $vars) = @_; + my (undef, $vars) = @_; + + # Don't show function arguments, in case they contain + # confidential data. + local $Carp::MaxArgNums = -1; - # Don't show function arguments, in case they contain - # confidential data. - local $Carp::MaxArgNums = -1; - # Don't show the error as coming from Bugzilla::Error, show it - # as coming from the caller. - local $Carp::CarpInternal{'Bugzilla::Error'} = 1; - $vars->{traceback} = Carp::longmess(); + # Don't show the error as coming from Bugzilla::Error, show it + # as coming from the caller. + local $Carp::CarpInternal{'Bugzilla::Error'} = 1; + $vars->{traceback} = Carp::longmess(); - _throw_error("global/code-error.html.tmpl", @_); + _throw_error("global/code-error.html.tmpl", @_); } sub ThrowTemplateError { - my ($template_err) = @_; - my $dbh = Bugzilla->dbh; + my ($template_err) = @_; + my $dbh = Bugzilla->dbh; - # Make sure the transaction is rolled back (if supported). - $dbh->bz_rollback_transaction() if $dbh->bz_in_transaction(); + # Make sure the transaction is rolled back (if supported). + $dbh->bz_rollback_transaction() if $dbh->bz_in_transaction(); - my $vars = {}; - if (Bugzilla->error_mode == ERROR_MODE_DIE) { - die("error: template error: $template_err"); - } + my $vars = {}; + if (Bugzilla->error_mode == ERROR_MODE_DIE) { + die("error: template error: $template_err"); + } - $vars->{'template_error_msg'} = $template_err; - $vars->{'error'} = "template_error"; + $vars->{'template_error_msg'} = $template_err; + $vars->{'error'} = "template_error"; - my $template = Bugzilla->template; + my $template = Bugzilla->template; - # Try a template first; but if this one fails too, fall back - # on plain old print statements. - if (!$template->process("global/code-error.html.tmpl", $vars)) { - require Bugzilla::Util; - import Bugzilla::Util qw(html_quote); - my $maintainer = Bugzilla->params->{'maintainer'}; - my $error = html_quote($vars->{'template_error_msg'}); - my $error2 = html_quote($template->error()); - my $url = html_quote(Bugzilla->cgi->self_url); + # Try a template first; but if this one fails too, fall back + # on plain old print statements. + if (!$template->process("global/code-error.html.tmpl", $vars)) { + require Bugzilla::Util; + import Bugzilla::Util qw(html_quote); + my $maintainer = Bugzilla->params->{'maintainer'}; + my $error = html_quote($vars->{'template_error_msg'}); + my $error2 = html_quote($template->error()); + my $url = html_quote(Bugzilla->cgi->self_url); - print < Bugzilla has suffered an internal error. Please save this page and send it to $maintainer with details of what you were doing at the @@ -206,8 +215,8 @@ sub ThrowTemplateError { First error: $error
Second error: $error2

END - } - exit; + } + exit; } 1; diff --git a/Bugzilla/Extension.pm b/Bugzilla/Extension.pm index e24ceb9eb..746fc4bfd 100644 --- a/Bugzilla/Extension.pm +++ b/Bugzilla/Extension.pm @@ -14,8 +14,8 @@ use warnings; use Bugzilla::Constants; use Bugzilla::Error; use Bugzilla::Install::Util qw( - extension_code_files extension_template_directory - extension_package_directory extension_web_directory); + extension_code_files extension_template_directory + extension_package_directory extension_web_directory); use File::Basename; use File::Spec; @@ -25,10 +25,10 @@ use File::Spec; #################### sub new { - my ($class, $params) = @_; - $params ||= {}; - bless $params, $class; - return $params; + my ($class, $params) = @_; + $params ||= {}; + bless $params, $class; + return $params; } ####################################### @@ -36,148 +36,151 @@ sub new { ####################################### sub load { - my ($class, $extension_file, $config_file) = @_; - my $package; - - # This is needed during checksetup.pl, because Extension packages can - # only be loaded once (they return "1" the second time they're loaded, - # instead of their name). During checksetup.pl, extensions are loaded - # once by Bugzilla::Install::Requirements, and then later again via - # Bugzilla->extensions (because of hooks). - my $map = Bugzilla->request_cache->{extension_requirement_package_map}; - - if ($config_file) { - if ($map and defined $map->{$config_file}) { - $package = $map->{$config_file}; - } - else { - my $name = require $config_file; - if ($name =~ /^\d+$/) { - ThrowCodeError('extension_must_return_name', - { extension => $config_file, - returned => $name }); - } - $package = "${class}::$name"; - } - - __do_call($package, 'modify_inc', $config_file); - } - - if ($map and defined $map->{$extension_file}) { - $package = $map->{$extension_file}; - $package->modify_inc($extension_file) if !$config_file; + my ($class, $extension_file, $config_file) = @_; + my $package; + + # This is needed during checksetup.pl, because Extension packages can + # only be loaded once (they return "1" the second time they're loaded, + # instead of their name). During checksetup.pl, extensions are loaded + # once by Bugzilla::Install::Requirements, and then later again via + # Bugzilla->extensions (because of hooks). + my $map = Bugzilla->request_cache->{extension_requirement_package_map}; + + if ($config_file) { + if ($map and defined $map->{$config_file}) { + $package = $map->{$config_file}; } else { - my $name = require $extension_file; - if ($name =~ /^\d+$/) { - ThrowCodeError('extension_must_return_name', - { extension => $extension_file, returned => $name }); - } - $package = "${class}::$name"; - $package->modify_inc($extension_file) if !$config_file; + my $name = require $config_file; + if ($name =~ /^\d+$/) { + ThrowCodeError('extension_must_return_name', + {extension => $config_file, returned => $name}); + } + $package = "${class}::$name"; } - $class->_validate_package($package, $extension_file); - return $package; -} - -sub _validate_package { - my ($class, $package, $extension_file) = @_; - - # For extensions from data/extensions/additional, we don't have a file - # name, so we fake it. - if (!$extension_file) { - $extension_file = $package; - $extension_file =~ s/::/\//g; - $extension_file .= '.pm'; + __do_call($package, 'modify_inc', $config_file); + } + + if ($map and defined $map->{$extension_file}) { + $package = $map->{$extension_file}; + $package->modify_inc($extension_file) if !$config_file; + } + else { + my $name = require $extension_file; + if ($name =~ /^\d+$/) { + ThrowCodeError('extension_must_return_name', + {extension => $extension_file, returned => $name}); } + $package = "${class}::$name"; + $package->modify_inc($extension_file) if !$config_file; + } - if (!eval { $package->NAME }) { - ThrowCodeError('extension_no_name', - { filename => $extension_file, package => $package }); - } + $class->_validate_package($package, $extension_file); + return $package; +} - if (!$package->isa($class)) { - ThrowCodeError('extension_must_be_subclass', - { filename => $extension_file, - package => $package, - class => $class }); - } +sub _validate_package { + my ($class, $package, $extension_file) = @_; + + # For extensions from data/extensions/additional, we don't have a file + # name, so we fake it. + if (!$extension_file) { + $extension_file = $package; + $extension_file =~ s/::/\//g; + $extension_file .= '.pm'; + } + + if (!eval { $package->NAME }) { + ThrowCodeError('extension_no_name', + {filename => $extension_file, package => $package}); + } + + if (!$package->isa($class)) { + ThrowCodeError('extension_must_be_subclass', + {filename => $extension_file, package => $package, class => $class}); + } } sub load_all { - my $class = shift; - my ($file_sets, $extra_packages) = extension_code_files(); - my @packages; - foreach my $file_set (@$file_sets) { - my $package = $class->load(@$file_set); - push(@packages, $package); - } - - # Extensions from data/extensions/additional - foreach my $package (@$extra_packages) { - # Don't load an "additional" extension if we already have an extension - # loaded with that name. - next if grep($_ eq $package, @packages); - # Untaint the package name - $package =~ /([\w:]+)/; - $package = $1; - eval("require $package") || die $@; - $package->_validate_package($package); - push(@packages, $package); - } - - return \@packages; + my $class = shift; + my ($file_sets, $extra_packages) = extension_code_files(); + my @packages; + foreach my $file_set (@$file_sets) { + my $package = $class->load(@$file_set); + push(@packages, $package); + } + + # Extensions from data/extensions/additional + foreach my $package (@$extra_packages) { + + # Don't load an "additional" extension if we already have an extension + # loaded with that name. + next if grep($_ eq $package, @packages); + + # Untaint the package name + $package =~ /([\w:]+)/; + $package = $1; + eval("require $package") || die $@; + $package->_validate_package($package); + push(@packages, $package); + } + + return \@packages; } # Modifies @INC so that extensions can use modules like # "use Bugzilla::Extension::Foo::Bar", when Bar.pm is in the lib/ # directory of the extension. sub modify_inc { - my ($class, $file) = @_; - - # Note that this package_dir call is necessary to set things up - # for my_inc, even if we didn't take its return value. - my $package_dir = __do_call($class, 'package_dir', $file); - # Don't modify @INC for extensions that are just files in the extensions/ - # directory. We don't want Bugzilla's base lib/CGI.pm being loaded as - # Bugzilla::Extension::Foo::CGI or any other confusing thing like that. - return if $package_dir eq bz_locations->{'extensionsdir'}; - unshift(@INC, sub { __do_call($class, 'my_inc', @_) }); + my ($class, $file) = @_; + + # Note that this package_dir call is necessary to set things up + # for my_inc, even if we didn't take its return value. + my $package_dir = __do_call($class, 'package_dir', $file); + + # Don't modify @INC for extensions that are just files in the extensions/ + # directory. We don't want Bugzilla's base lib/CGI.pm being loaded as + # Bugzilla::Extension::Foo::CGI or any other confusing thing like that. + return if $package_dir eq bz_locations->{'extensionsdir'}; + unshift(@INC, sub { __do_call($class, 'my_inc', @_) }); } # This is what gets put into @INC by modify_inc. sub my_inc { - my ($class, undef, $file) = @_; - - # This avoids infinite recursion in case anything inside of this function - # does a "require". (I know for sure that File::Spec->case_tolerant does - # a "require" on Windows, for example.) - return if $file !~ /^Bugzilla/; - - my $lib_dir = __do_call($class, 'lib_dir'); - my @class_parts = split('::', $class); - my ($vol, $dir, $file_name) = File::Spec->splitpath($file); - my @dir_parts = File::Spec->splitdir($dir); - # File::Spec::Win32 (any maybe other OSes) add an empty directory at the - # end of @dir_parts. - @dir_parts = grep { $_ ne '' } @dir_parts; - # Validate that this is a sub-package of Bugzilla::Extension::Foo ($class). - for (my $i = 0; $i < scalar(@class_parts); $i++) { - return if !@dir_parts; - if (File::Spec->case_tolerant) { - return if lc($class_parts[$i]) ne lc($dir_parts[0]); - } - else { - return if $class_parts[$i] ne $dir_parts[0]; - } - shift(@dir_parts); + my ($class, undef, $file) = @_; + + # This avoids infinite recursion in case anything inside of this function + # does a "require". (I know for sure that File::Spec->case_tolerant does + # a "require" on Windows, for example.) + return if $file !~ /^Bugzilla/; + + my $lib_dir = __do_call($class, 'lib_dir'); + my @class_parts = split('::', $class); + my ($vol, $dir, $file_name) = File::Spec->splitpath($file); + my @dir_parts = File::Spec->splitdir($dir); + + # File::Spec::Win32 (any maybe other OSes) add an empty directory at the + # end of @dir_parts. + @dir_parts = grep { $_ ne '' } @dir_parts; + + # Validate that this is a sub-package of Bugzilla::Extension::Foo ($class). + for (my $i = 0; $i < scalar(@class_parts); $i++) { + return if !@dir_parts; + if (File::Spec->case_tolerant) { + return if lc($class_parts[$i]) ne lc($dir_parts[0]); } - # For Bugzilla::Extension::Foo::Bar, this would look something like - # extensions/Example/lib/Bar.pm - my $resolved_path = File::Spec->catfile($lib_dir, @dir_parts, $file_name); - open(my $fh, '<', $resolved_path); - return $fh; + else { + return if $class_parts[$i] ne $dir_parts[0]; + } + shift(@dir_parts); + } + + # For Bugzilla::Extension::Foo::Bar, this would look something like + # extensions/Example/lib/Bar.pm + my $resolved_path = File::Spec->catfile($lib_dir, @dir_parts, $file_name); + open(my $fh, '<', $resolved_path); + return $fh; } #################### @@ -187,23 +190,24 @@ sub my_inc { use constant enabled => 1; sub lib_dir { - my $invocant = shift; - my $package_dir = __do_call($invocant, 'package_dir'); - # For extensions that are just files in the extensions/ directory, - # use the base lib/ dir as our "lib_dir". Note that Bugzilla never - # uses lib_dir in this case, though, because modify_inc is prevented - # from modifying @INC when we're just a file in the extensions/ directory. - # So this particular code block exists just to make lib_dir return - # something right in case an extension needs it for some odd reason. - if ($package_dir eq bz_locations()->{'extensionsdir'}) { - return bz_locations->{'ext_libpath'}; - } - return File::Spec->catdir($package_dir, 'lib'); + my $invocant = shift; + my $package_dir = __do_call($invocant, 'package_dir'); + + # For extensions that are just files in the extensions/ directory, + # use the base lib/ dir as our "lib_dir". Note that Bugzilla never + # uses lib_dir in this case, though, because modify_inc is prevented + # from modifying @INC when we're just a file in the extensions/ directory. + # So this particular code block exists just to make lib_dir return + # something right in case an extension needs it for some odd reason. + if ($package_dir eq bz_locations()->{'extensionsdir'}) { + return bz_locations->{'ext_libpath'}; + } + return File::Spec->catdir($package_dir, 'lib'); } sub template_dir { return extension_template_directory(@_); } -sub package_dir { return extension_package_directory(@_); } -sub web_dir { return extension_web_directory(@_); } +sub package_dir { return extension_package_directory(@_); } +sub web_dir { return extension_web_directory(@_); } ###################### # Helper Subroutines # @@ -217,13 +221,13 @@ sub web_dir { return extension_web_directory(@_); } # the method. This is necessary because Config.pm is not a subclass of # Bugzilla::Extension. sub __do_call { - my ($class, $method, @args) = @_; - if ($class->can($method)) { - return $class->$method(@args); - } - my $function_ref; - { no strict 'refs'; $function_ref = \&{$method}; } - return $function_ref->($class, @args); + my ($class, $method, @args) = @_; + if ($class->can($method)) { + return $class->$method(@args); + } + my $function_ref; + { no strict 'refs'; $function_ref = \&{$method}; } + return $function_ref->($class, @args); } 1; diff --git a/Bugzilla/Field.pm b/Bugzilla/Field.pm index 761f7b94e..4a364eb3a 100644 --- a/Bugzilla/Field.pm +++ b/Bugzilla/Field.pm @@ -81,82 +81,80 @@ use constant DB_TABLE => 'fielddefs'; use constant LIST_ORDER => 'sortkey, name'; use constant DB_COLUMNS => qw( - id - name - description - long_desc - type - custom - mailhead - sortkey - obsolete - enter_bug - buglist - visibility_field_id - value_field_id - reverse_desc - is_mandatory - is_numeric + id + name + description + long_desc + type + custom + mailhead + sortkey + obsolete + enter_bug + buglist + visibility_field_id + value_field_id + reverse_desc + is_mandatory + is_numeric ); use constant VALIDATORS => { - custom => \&_check_custom, - description => \&_check_description, - long_desc => \&_check_long_desc, - enter_bug => \&_check_enter_bug, - buglist => \&Bugzilla::Object::check_boolean, - mailhead => \&_check_mailhead, - name => \&_check_name, - obsolete => \&_check_obsolete, - reverse_desc => \&_check_reverse_desc, - sortkey => \&_check_sortkey, - type => \&_check_type, - value_field_id => \&_check_value_field_id, - visibility_field_id => \&_check_visibility_field_id, - visibility_values => \&_check_visibility_values, - is_mandatory => \&Bugzilla::Object::check_boolean, - is_numeric => \&_check_is_numeric, + custom => \&_check_custom, + description => \&_check_description, + long_desc => \&_check_long_desc, + enter_bug => \&_check_enter_bug, + buglist => \&Bugzilla::Object::check_boolean, + mailhead => \&_check_mailhead, + name => \&_check_name, + obsolete => \&_check_obsolete, + reverse_desc => \&_check_reverse_desc, + sortkey => \&_check_sortkey, + type => \&_check_type, + value_field_id => \&_check_value_field_id, + visibility_field_id => \&_check_visibility_field_id, + visibility_values => \&_check_visibility_values, + is_mandatory => \&Bugzilla::Object::check_boolean, + is_numeric => \&_check_is_numeric, }; use constant VALIDATOR_DEPENDENCIES => { - is_numeric => ['type'], - name => ['custom'], - type => ['custom'], - reverse_desc => ['type'], - value_field_id => ['type'], - visibility_values => ['visibility_field_id'], + is_numeric => ['type'], + name => ['custom'], + type => ['custom'], + reverse_desc => ['type'], + value_field_id => ['type'], + visibility_values => ['visibility_field_id'], }; use constant UPDATE_COLUMNS => qw( - description - long_desc - mailhead - sortkey - obsolete - enter_bug - buglist - visibility_field_id - value_field_id - reverse_desc - is_mandatory - is_numeric - type + description + long_desc + mailhead + sortkey + obsolete + enter_bug + buglist + visibility_field_id + value_field_id + reverse_desc + is_mandatory + is_numeric + type ); # How various field types translate into SQL data definitions. use constant SQL_DEFINITIONS => { - # Using commas because these are constants and they shouldn't - # be auto-quoted by the "=>" operator. - FIELD_TYPE_FREETEXT, { TYPE => 'varchar(255)', - NOTNULL => 1, DEFAULT => "''"}, - FIELD_TYPE_SINGLE_SELECT, { TYPE => 'varchar(64)', NOTNULL => 1, - DEFAULT => "'---'" }, - FIELD_TYPE_TEXTAREA, { TYPE => 'MEDIUMTEXT', - NOTNULL => 1, DEFAULT => "''"}, - FIELD_TYPE_DATETIME, { TYPE => 'DATETIME' }, - FIELD_TYPE_DATE, { TYPE => 'DATE' }, - FIELD_TYPE_BUG_ID, { TYPE => 'INT3' }, - FIELD_TYPE_INTEGER, { TYPE => 'INT4', NOTNULL => 1, DEFAULT => 0 }, + + # Using commas because these are constants and they shouldn't + # be auto-quoted by the "=>" operator. + FIELD_TYPE_FREETEXT, {TYPE => 'varchar(255)', NOTNULL => 1, DEFAULT => "''"}, + FIELD_TYPE_SINGLE_SELECT, + {TYPE => 'varchar(64)', NOTNULL => 1, DEFAULT => "'---'"}, FIELD_TYPE_TEXTAREA, + {TYPE => 'MEDIUMTEXT', NOTNULL => 1, DEFAULT => "''"}, FIELD_TYPE_DATETIME, + {TYPE => 'DATETIME'}, FIELD_TYPE_DATE, {TYPE => 'DATE'}, FIELD_TYPE_BUG_ID, + {TYPE => 'INT3'}, FIELD_TYPE_INTEGER, + {TYPE => 'INT4', NOTNULL => 1, DEFAULT => 0}, }; # Field definitions for the fields that ship with Bugzilla. @@ -164,110 +162,232 @@ use constant SQL_DEFINITIONS => { # the fielddefs table. # 'days_elapsed' is set in populate_field_definitions() itself. use constant DEFAULT_FIELDS => ( - {name => 'bug_id', desc => 'Bug #', in_new_bugmail => 1, - buglist => 1, is_numeric => 1}, - {name => 'short_desc', desc => 'Summary', in_new_bugmail => 1, - is_mandatory => 1, buglist => 1}, - {name => 'classification', desc => 'Classification', in_new_bugmail => 1, - type => FIELD_TYPE_SINGLE_SELECT, buglist => 1}, - {name => 'product', desc => 'Product', in_new_bugmail => 1, - is_mandatory => 1, - type => FIELD_TYPE_SINGLE_SELECT, buglist => 1}, - {name => 'version', desc => 'Version', in_new_bugmail => 1, - is_mandatory => 1, buglist => 1}, - {name => 'rep_platform', desc => 'Platform', in_new_bugmail => 1, - type => FIELD_TYPE_SINGLE_SELECT, buglist => 1}, - {name => 'bug_file_loc', desc => 'URL', in_new_bugmail => 1, - buglist => 1}, - {name => 'op_sys', desc => 'OS/Version', in_new_bugmail => 1, - type => FIELD_TYPE_SINGLE_SELECT, buglist => 1}, - {name => 'bug_status', desc => 'Status', in_new_bugmail => 1, - type => FIELD_TYPE_SINGLE_SELECT, buglist => 1}, - {name => 'status_whiteboard', desc => 'Status Whiteboard', - in_new_bugmail => 1, buglist => 1}, - {name => 'keywords', desc => 'Keywords', in_new_bugmail => 1, - type => FIELD_TYPE_KEYWORDS, buglist => 1}, - {name => 'resolution', desc => 'Resolution', - type => FIELD_TYPE_SINGLE_SELECT, buglist => 1}, - {name => 'bug_severity', desc => 'Severity', in_new_bugmail => 1, - type => FIELD_TYPE_SINGLE_SELECT, buglist => 1}, - {name => 'priority', desc => 'Priority', in_new_bugmail => 1, - type => FIELD_TYPE_SINGLE_SELECT, buglist => 1}, - {name => 'component', desc => 'Component', in_new_bugmail => 1, - is_mandatory => 1, - type => FIELD_TYPE_SINGLE_SELECT, buglist => 1}, - {name => 'assigned_to', desc => 'AssignedTo', in_new_bugmail => 1, - buglist => 1}, - {name => 'reporter', desc => 'ReportedBy', in_new_bugmail => 1, - 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, buglist => 1}, - {name => 'blocked', desc => 'Blocks', in_new_bugmail => 1, - is_numeric => 1, buglist => 1}, - - {name => 'attachments.description', desc => 'Attachment description'}, - {name => 'attachments.filename', desc => 'Attachment filename'}, - {name => 'attachments.mimetype', desc => 'Attachment mime type'}, - {name => 'attachments.ispatch', desc => 'Attachment is patch', - is_numeric => 1}, - {name => 'attachments.isobsolete', desc => 'Attachment is obsolete', - is_numeric => 1}, - {name => 'attachments.isprivate', desc => 'Attachment is private', - is_numeric => 1}, - {name => 'attachments.submitter', desc => 'Attachment creator'}, - - {name => 'target_milestone', desc => 'Target Milestone', - in_new_bugmail => 1, buglist => 1}, - {name => 'creation_ts', desc => 'Creation date', - buglist => 1}, - {name => 'delta_ts', desc => 'Last changed date', - buglist => 1}, - {name => 'longdesc', desc => 'Comment'}, - {name => 'longdescs.isprivate', desc => 'Comment is private', - is_numeric => 1}, - {name => 'longdescs.count', desc => 'Number of Comments', - buglist => 1, is_numeric => 1}, - {name => 'alias', desc => 'Alias', buglist => 1}, - {name => 'everconfirmed', desc => 'Ever Confirmed', - is_numeric => 1}, - {name => 'reporter_accessible', desc => 'Reporter Accessible', - is_numeric => 1}, - {name => 'cclist_accessible', desc => 'CC Accessible', - is_numeric => 1}, - {name => 'bug_group', desc => 'Group', in_new_bugmail => 1}, - {name => 'estimated_time', desc => 'Estimated Hours', - in_new_bugmail => 1, buglist => 1, is_numeric => 1}, - {name => 'remaining_time', desc => 'Remaining Hours', buglist => 1, - is_numeric => 1}, - {name => 'deadline', desc => 'Deadline', - type => FIELD_TYPE_DATETIME, in_new_bugmail => 1, buglist => 1}, - {name => 'commenter', desc => 'Commenter'}, - {name => 'flagtypes.name', desc => 'Flags', buglist => 1}, - {name => 'requestees.login_name', desc => 'Flag Requestee'}, - {name => 'setters.login_name', desc => 'Flag Setter'}, - {name => 'work_time', desc => 'Hours Worked', buglist => 1, - is_numeric => 1}, - {name => 'percentage_complete', desc => 'Percentage Complete', - buglist => 1, is_numeric => 1}, - {name => 'content', desc => 'Content'}, - {name => 'attach_data.thedata', desc => 'Attachment data'}, - {name => "owner_idle_time", desc => "Time Since Assignee Touched"}, - {name => 'see_also', desc => "See Also", - type => FIELD_TYPE_BUG_URLS}, - {name => 'tag', desc => 'Personal Tags', buglist => 1, - type => FIELD_TYPE_KEYWORDS}, - {name => 'last_visit_ts', desc => 'Last Visit', buglist => 1, - type => FIELD_TYPE_DATETIME}, - {name => 'comment_tag', desc => 'Comment Tag'}, + { + name => 'bug_id', + desc => 'Bug #', + in_new_bugmail => 1, + buglist => 1, + is_numeric => 1 + }, + { + name => 'short_desc', + desc => 'Summary', + in_new_bugmail => 1, + is_mandatory => 1, + buglist => 1 + }, + { + name => 'classification', + desc => 'Classification', + in_new_bugmail => 1, + type => FIELD_TYPE_SINGLE_SELECT, + buglist => 1 + }, + { + name => 'product', + desc => 'Product', + in_new_bugmail => 1, + is_mandatory => 1, + type => FIELD_TYPE_SINGLE_SELECT, + buglist => 1 + }, + { + name => 'version', + desc => 'Version', + in_new_bugmail => 1, + is_mandatory => 1, + buglist => 1 + }, + { + name => 'rep_platform', + desc => 'Platform', + in_new_bugmail => 1, + type => FIELD_TYPE_SINGLE_SELECT, + buglist => 1 + }, + {name => 'bug_file_loc', desc => 'URL', in_new_bugmail => 1, buglist => 1}, + { + name => 'op_sys', + desc => 'OS/Version', + in_new_bugmail => 1, + type => FIELD_TYPE_SINGLE_SELECT, + buglist => 1 + }, + { + name => 'bug_status', + desc => 'Status', + in_new_bugmail => 1, + type => FIELD_TYPE_SINGLE_SELECT, + buglist => 1 + }, + { + name => 'status_whiteboard', + desc => 'Status Whiteboard', + in_new_bugmail => 1, + buglist => 1 + }, + { + name => 'keywords', + desc => 'Keywords', + in_new_bugmail => 1, + type => FIELD_TYPE_KEYWORDS, + buglist => 1 + }, + { + name => 'resolution', + desc => 'Resolution', + type => FIELD_TYPE_SINGLE_SELECT, + buglist => 1 + }, + { + name => 'bug_severity', + desc => 'Severity', + in_new_bugmail => 1, + type => FIELD_TYPE_SINGLE_SELECT, + buglist => 1 + }, + { + name => 'priority', + desc => 'Priority', + in_new_bugmail => 1, + type => FIELD_TYPE_SINGLE_SELECT, + buglist => 1 + }, + { + name => 'component', + desc => 'Component', + in_new_bugmail => 1, + is_mandatory => 1, + type => FIELD_TYPE_SINGLE_SELECT, + buglist => 1 + }, + { + name => 'assigned_to', + desc => 'AssignedTo', + in_new_bugmail => 1, + buglist => 1 + }, + {name => 'reporter', desc => 'ReportedBy', in_new_bugmail => 1, 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, + buglist => 1 + }, + { + name => 'blocked', + desc => 'Blocks', + in_new_bugmail => 1, + is_numeric => 1, + buglist => 1 + }, + + {name => 'attachments.description', desc => 'Attachment description'}, + {name => 'attachments.filename', desc => 'Attachment filename'}, + {name => 'attachments.mimetype', desc => 'Attachment mime type'}, + {name => 'attachments.ispatch', desc => 'Attachment is patch', is_numeric => 1}, + { + name => 'attachments.isobsolete', + desc => 'Attachment is obsolete', + is_numeric => 1 + }, + { + name => 'attachments.isprivate', + desc => 'Attachment is private', + is_numeric => 1 + }, + {name => 'attachments.submitter', desc => 'Attachment creator'}, + + { + name => 'target_milestone', + desc => 'Target Milestone', + in_new_bugmail => 1, + buglist => 1 + }, + {name => 'creation_ts', desc => 'Creation date', buglist => 1}, + {name => 'delta_ts', desc => 'Last changed date', buglist => 1}, + {name => 'longdesc', desc => 'Comment'}, + {name => 'longdescs.isprivate', desc => 'Comment is private', is_numeric => 1}, + { + name => 'longdescs.count', + desc => 'Number of Comments', + buglist => 1, + is_numeric => 1 + }, + {name => 'alias', desc => 'Alias', buglist => 1}, + {name => 'everconfirmed', desc => 'Ever Confirmed', is_numeric => 1}, + {name => 'reporter_accessible', desc => 'Reporter Accessible', is_numeric => 1}, + {name => 'cclist_accessible', desc => 'CC Accessible', is_numeric => 1}, + {name => 'bug_group', desc => 'Group', in_new_bugmail => 1}, + { + name => 'estimated_time', + desc => 'Estimated Hours', + in_new_bugmail => 1, + buglist => 1, + is_numeric => 1 + }, + { + name => 'remaining_time', + desc => 'Remaining Hours', + buglist => 1, + is_numeric => 1 + }, + { + name => 'deadline', + desc => 'Deadline', + type => FIELD_TYPE_DATETIME, + in_new_bugmail => 1, + buglist => 1 + }, + {name => 'commenter', desc => 'Commenter'}, + {name => 'flagtypes.name', desc => 'Flags', buglist => 1}, + {name => 'requestees.login_name', desc => 'Flag Requestee'}, + {name => 'setters.login_name', desc => 'Flag Setter'}, + {name => 'work_time', desc => 'Hours Worked', buglist => 1, is_numeric => 1}, + { + name => 'percentage_complete', + desc => 'Percentage Complete', + buglist => 1, + is_numeric => 1 + }, + {name => 'content', desc => 'Content'}, + {name => 'attach_data.thedata', desc => 'Attachment data'}, + {name => "owner_idle_time", desc => "Time Since Assignee Touched"}, + {name => 'see_also', desc => "See Also", type => FIELD_TYPE_BUG_URLS}, + { + name => 'tag', + desc => 'Personal Tags', + buglist => 1, + type => FIELD_TYPE_KEYWORDS + }, + { + name => 'last_visit_ts', + desc => 'Last Visit', + buglist => 1, + type => FIELD_TYPE_DATETIME + }, + {name => 'comment_tag', desc => 'Comment Tag'}, ); ################ @@ -276,12 +396,12 @@ use constant DEFAULT_FIELDS => ( # Override match to add is_select. sub match { - my $self = shift; - my ($params) = @_; - if (delete $params->{is_select}) { - $params->{type} = [FIELD_TYPE_SINGLE_SELECT, FIELD_TYPE_MULTI_SELECT]; - } - return $self->SUPER::match(@_); + my $self = shift; + my ($params) = @_; + if (delete $params->{is_select}) { + $params->{type} = [FIELD_TYPE_SINGLE_SELECT, FIELD_TYPE_MULTI_SELECT]; + } + return $self->SUPER::match(@_); } ############## @@ -291,151 +411,153 @@ sub match { sub _check_custom { return $_[1] ? 1 : 0; } sub _check_description { - my ($invocant, $desc) = @_; - $desc = clean_text($desc); - $desc || ThrowUserError('field_missing_description'); - return $desc; + my ($invocant, $desc) = @_; + $desc = clean_text($desc); + $desc || ThrowUserError('field_missing_description'); + return $desc; } sub _check_long_desc { - my ($invocant, $long_desc) = @_; - $long_desc = clean_text($long_desc || ''); - if (length($long_desc) > MAX_FIELD_LONG_DESC_LENGTH) { - ThrowUserError('field_long_desc_too_long'); - } - return $long_desc; + my ($invocant, $long_desc) = @_; + $long_desc = clean_text($long_desc || ''); + if (length($long_desc) > MAX_FIELD_LONG_DESC_LENGTH) { + ThrowUserError('field_long_desc_too_long'); + } + return $long_desc; } sub _check_enter_bug { return $_[1] ? 1 : 0; } sub _check_is_numeric { - my ($invocant, $value, undef, $params) = @_; - my $type = blessed($invocant) ? $invocant->type : $params->{type}; - return 1 if $type == FIELD_TYPE_BUG_ID; - return $value ? 1 : 0; + my ($invocant, $value, undef, $params) = @_; + my $type = blessed($invocant) ? $invocant->type : $params->{type}; + return 1 if $type == FIELD_TYPE_BUG_ID; + return $value ? 1 : 0; } sub _check_mailhead { return $_[1] ? 1 : 0; } sub _check_name { - my ($class, $name, undef, $params) = @_; - $name = lc(clean_text($name)); - $name || ThrowUserError('field_missing_name'); - - # Don't want to allow a name that might mess up SQL. - my $name_regex = qr/^[\w\.]+$/; - # Custom fields have more restrictive name requirements than - # standard fields. - $name_regex = qr/^[a-zA-Z0-9_]+$/ if $params->{custom}; - # Custom fields can't be named just "cf_", and there is no normal - # field named just "cf_". - ($name =~ $name_regex && $name ne "cf_") - || ThrowUserError('field_invalid_name', { name => $name }); - - # If it's custom, prepend cf_ to the custom field name to distinguish - # it from standard fields. - if ($name !~ /^cf_/ && $params->{custom}) { - $name = 'cf_' . $name; - } + my ($class, $name, undef, $params) = @_; + $name = lc(clean_text($name)); + $name || ThrowUserError('field_missing_name'); + + # Don't want to allow a name that might mess up SQL. + my $name_regex = qr/^[\w\.]+$/; + + # Custom fields have more restrictive name requirements than + # standard fields. + $name_regex = qr/^[a-zA-Z0-9_]+$/ if $params->{custom}; + + # Custom fields can't be named just "cf_", and there is no normal + # field named just "cf_". + ($name =~ $name_regex && $name ne "cf_") + || ThrowUserError('field_invalid_name', {name => $name}); + + # If it's custom, prepend cf_ to the custom field name to distinguish + # it from standard fields. + if ($name !~ /^cf_/ && $params->{custom}) { + $name = 'cf_' . $name; + } - # Assure the name is unique. Names can't be changed, so we don't have - # to worry about what to do on updates. - my $field = new Bugzilla::Field({ name => $name }); - ThrowUserError('field_already_exists', {'field' => $field }) if $field; + # Assure the name is unique. Names can't be changed, so we don't have + # to worry about what to do on updates. + my $field = new Bugzilla::Field({name => $name}); + ThrowUserError('field_already_exists', {'field' => $field}) if $field; - return $name; + return $name; } sub _check_obsolete { return $_[1] ? 1 : 0; } sub _check_sortkey { - my ($invocant, $sortkey) = @_; - my $skey = $sortkey; - if (!defined $skey || $skey eq '') { - ($sortkey) = Bugzilla->dbh->selectrow_array( - 'SELECT MAX(sortkey) + 100 FROM fielddefs') || 100; - } - detaint_natural($sortkey) - || ThrowUserError('field_invalid_sortkey', { sortkey => $skey }); - return $sortkey; + my ($invocant, $sortkey) = @_; + my $skey = $sortkey; + if (!defined $skey || $skey eq '') { + ($sortkey) + = Bugzilla->dbh->selectrow_array('SELECT MAX(sortkey) + 100 FROM fielddefs') + || 100; + } + detaint_natural($sortkey) + || ThrowUserError('field_invalid_sortkey', {sortkey => $skey}); + return $sortkey; } sub _check_type { - my ($invocant, $type, undef, $params) = @_; - my $saved_type = $type; - (detaint_natural($type) && $type < FIELD_TYPE_HIGHEST_PLUS_ONE) - || ThrowCodeError('invalid_customfield_type', { type => $saved_type }); - - my $custom = blessed($invocant) ? $invocant->custom : $params->{custom}; - if ($custom && !$type) { - ThrowCodeError('field_type_not_specified'); - } + my ($invocant, $type, undef, $params) = @_; + my $saved_type = $type; + (detaint_natural($type) && $type < FIELD_TYPE_HIGHEST_PLUS_ONE) + || ThrowCodeError('invalid_customfield_type', {type => $saved_type}); + + my $custom = blessed($invocant) ? $invocant->custom : $params->{custom}; + if ($custom && !$type) { + ThrowCodeError('field_type_not_specified'); + } - return $type; + return $type; } sub _check_value_field_id { - my ($invocant, $field_id, undef, $params) = @_; - my $is_select = $invocant->is_select($params); - if ($field_id && !$is_select) { - ThrowUserError('field_value_control_select_only'); - } - return $invocant->_check_visibility_field_id($field_id); + my ($invocant, $field_id, undef, $params) = @_; + my $is_select = $invocant->is_select($params); + if ($field_id && !$is_select) { + ThrowUserError('field_value_control_select_only'); + } + return $invocant->_check_visibility_field_id($field_id); } sub _check_visibility_field_id { - my ($invocant, $field_id) = @_; - $field_id = trim($field_id); - return undef if !$field_id; - my $field = Bugzilla::Field->check({ id => $field_id }); - if (blessed($invocant) && $field->id == $invocant->id) { - ThrowUserError('field_cant_control_self', { field => $field }); - } - if (!$field->is_select) { - ThrowUserError('field_control_must_be_select', - { field => $field }); - } - return $field->id; + my ($invocant, $field_id) = @_; + $field_id = trim($field_id); + return undef if !$field_id; + my $field = Bugzilla::Field->check({id => $field_id}); + if (blessed($invocant) && $field->id == $invocant->id) { + ThrowUserError('field_cant_control_self', {field => $field}); + } + if (!$field->is_select) { + ThrowUserError('field_control_must_be_select', {field => $field}); + } + return $field->id; } sub _check_visibility_values { - my ($invocant, $values, undef, $params) = @_; - my $field; - if (blessed $invocant) { - $field = $invocant->visibility_field; - } - elsif ($params->{visibility_field_id}) { - $field = $invocant->new($params->{visibility_field_id}); - } - # When no field is set, no values are set. - return [] if !$field; + my ($invocant, $values, undef, $params) = @_; + my $field; + if (blessed $invocant) { + $field = $invocant->visibility_field; + } + elsif ($params->{visibility_field_id}) { + $field = $invocant->new($params->{visibility_field_id}); + } - if (!scalar @$values) { - ThrowUserError('field_visibility_values_must_be_selected', - { field => $field }); - } + # When no field is set, no values are set. + return [] if !$field; + + if (!scalar @$values) { + ThrowUserError('field_visibility_values_must_be_selected', {field => $field}); + } - my @visibility_values; - my $choice = Bugzilla::Field::Choice->type($field); - foreach my $value (@$values) { - if (!blessed $value) { - $value = $choice->check({ id => $value }); - } - push(@visibility_values, $value); + my @visibility_values; + my $choice = Bugzilla::Field::Choice->type($field); + foreach my $value (@$values) { + if (!blessed $value) { + $value = $choice->check({id => $value}); } + push(@visibility_values, $value); + } - return \@visibility_values; + return \@visibility_values; } sub _check_reverse_desc { - my ($invocant, $reverse_desc, undef, $params) = @_; - my $type = blessed($invocant) ? $invocant->type : $params->{type}; - if ($type != FIELD_TYPE_BUG_ID) { - return undef; # store NULL for non-reversible field types - } - - $reverse_desc = clean_text($reverse_desc); - return $reverse_desc; + my ($invocant, $reverse_desc, undef, $params) = @_; + my $type = blessed($invocant) ? $invocant->type : $params->{type}; + if ($type != FIELD_TYPE_BUG_ID) { + return undef; # store NULL for non-reversible field types + } + + $reverse_desc = clean_text($reverse_desc); + return $reverse_desc; } sub _check_is_mandatory { return $_[1] ? 1 : 0; } @@ -583,11 +705,13 @@ objects. =cut sub is_select { - my ($invocant, $params) = @_; - # This allows this method to be called by create() validators. - my $type = blessed($invocant) ? $invocant->type : $params->{type}; - return ($type == FIELD_TYPE_SINGLE_SELECT - || $type == FIELD_TYPE_MULTI_SELECT) ? 1 : 0 + my ($invocant, $params) = @_; + + # This allows this method to be called by create() validators. + my $type = blessed($invocant) ? $invocant->type : $params->{type}; + return ($type == FIELD_TYPE_SINGLE_SELECT || $type == FIELD_TYPE_MULTI_SELECT) + ? 1 + : 0; } =over @@ -608,19 +732,19 @@ This method returns C<1> if the field is "abnormal", C<0> otherwise. =cut sub is_abnormal { - my $self = shift; - return ABNORMAL_SELECTS->{$self->name} ? 1 : 0; + my $self = shift; + return ABNORMAL_SELECTS->{$self->name} ? 1 : 0; } sub legal_values { - my $self = shift; + my $self = shift; - if (!defined $self->{'legal_values'}) { - require Bugzilla::Field::Choice; - my @values = Bugzilla::Field::Choice->type($self)->get_all(); - $self->{'legal_values'} = \@values; - } - return $self->{'legal_values'}; + if (!defined $self->{'legal_values'}) { + require Bugzilla::Field::Choice; + my @values = Bugzilla::Field::Choice->type($self)->get_all(); + $self->{'legal_values'} = \@values; + } + return $self->{'legal_values'}; } =pod @@ -637,8 +761,8 @@ in the C. =cut sub is_timetracking { - my ($self) = @_; - return grep($_ eq $self->name, TIMETRACKING_FIELDS) ? 1 : 0; + my ($self) = @_; + return grep($_ eq $self->name, TIMETRACKING_FIELDS) ? 1 : 0; } =pod @@ -657,12 +781,11 @@ Returns undef if there is no field that controls this field's visibility. =cut sub visibility_field { - my $self = shift; - if ($self->{visibility_field_id}) { - $self->{visibility_field} ||= - $self->new($self->{visibility_field_id}); - } - return $self->{visibility_field}; + my $self = shift; + if ($self->{visibility_field_id}) { + $self->{visibility_field} ||= $self->new($self->{visibility_field_id}); + } + return $self->{visibility_field}; } =pod @@ -680,22 +803,23 @@ or undef if there is no C set. =cut sub visibility_values { - my $self = shift; - my $dbh = Bugzilla->dbh; - - return [] if !$self->{visibility_field_id}; - - if (!defined $self->{visibility_values}) { - my $visibility_value_ids = - $dbh->selectcol_arrayref("SELECT value_id FROM field_visibility - WHERE field_id = ?", undef, $self->id); - - $self->{visibility_values} = - Bugzilla::Field::Choice->type($self->visibility_field) - ->new_from_list($visibility_value_ids); - } + my $self = shift; + my $dbh = Bugzilla->dbh; + + return [] if !$self->{visibility_field_id}; + + if (!defined $self->{visibility_values}) { + my $visibility_value_ids = $dbh->selectcol_arrayref( + "SELECT value_id FROM field_visibility + WHERE field_id = ?", undef, $self->id + ); - return $self->{visibility_values}; + $self->{visibility_values} + = Bugzilla::Field::Choice->type($self->visibility_field) + ->new_from_list($visibility_value_ids); + } + + return $self->{visibility_values}; } =pod @@ -712,10 +836,10 @@ field controls the visibility of. =cut sub controls_visibility_of { - my $self = shift; - $self->{controls_visibility_of} ||= - Bugzilla::Field->match({ visibility_field_id => $self->id }); - return $self->{controls_visibility_of}; + my $self = shift; + $self->{controls_visibility_of} + ||= Bugzilla::Field->match({visibility_field_id => $self->id}); + return $self->{controls_visibility_of}; } =pod @@ -733,11 +857,11 @@ Returns undef if there is no field that controls this field's visibility. =cut sub value_field { - my $self = shift; - if ($self->{value_field_id}) { - $self->{value_field} ||= $self->new($self->{value_field_id}); - } - return $self->{value_field}; + my $self = shift; + if ($self->{value_field_id}) { + $self->{value_field} ||= $self->new($self->{value_field_id}); + } + return $self->{value_field}; } =pod @@ -754,10 +878,10 @@ field controls the values of. =cut sub controls_values_of { - my $self = shift; - $self->{controls_values_of} ||= - Bugzilla::Field->match({ value_field_id => $self->id }); - return $self->{controls_values_of}; + my $self = shift; + $self->{controls_values_of} + ||= Bugzilla::Field->match({value_field_id => $self->id}); + return $self->{controls_values_of}; } =over @@ -771,15 +895,15 @@ See L. =cut sub is_visible_on_bug { - my ($self, $bug) = @_; + my ($self, $bug) = @_; - # Always return visible, if this field is not - # visibility controlled. - return 1 if !$self->{visibility_field_id}; + # Always return visible, if this field is not + # visibility controlled. + return 1 if !$self->{visibility_field_id}; - my $visibility_values = $self->visibility_values; + my $visibility_values = $self->visibility_values; - return (any { $_->is_set_on_bug($bug) } @$visibility_values) ? 1 : 0; + return (any { $_->is_set_on_bug($bug) } @$visibility_values) ? 1 : 0; } =over @@ -795,13 +919,13 @@ dependency tree display, and similar functionality. =cut -sub is_relationship { - my $self = shift; - my $desc = $self->reverse_desc; - if (defined $desc && $desc ne "") { - return 1; - } - return 0; +sub is_relationship { + my $self = shift; + my $desc = $self->reverse_desc; + if (defined $desc && $desc ne "") { + return 1; + } + return 0; } =over @@ -888,29 +1012,32 @@ They will throw an error if you try to set the values to something invalid. =cut -sub set_description { $_[0]->set('description', $_[1]); } -sub set_long_desc { $_[0]->set('long_desc', $_[1]); } -sub set_enter_bug { $_[0]->set('enter_bug', $_[1]); } -sub set_is_numeric { $_[0]->set('is_numeric', $_[1]); } -sub set_obsolete { $_[0]->set('obsolete', $_[1]); } -sub set_sortkey { $_[0]->set('sortkey', $_[1]); } -sub set_in_new_bugmail { $_[0]->set('mailhead', $_[1]); } -sub set_buglist { $_[0]->set('buglist', $_[1]); } -sub set_reverse_desc { $_[0]->set('reverse_desc', $_[1]); } +sub set_description { $_[0]->set('description', $_[1]); } +sub set_long_desc { $_[0]->set('long_desc', $_[1]); } +sub set_enter_bug { $_[0]->set('enter_bug', $_[1]); } +sub set_is_numeric { $_[0]->set('is_numeric', $_[1]); } +sub set_obsolete { $_[0]->set('obsolete', $_[1]); } +sub set_sortkey { $_[0]->set('sortkey', $_[1]); } +sub set_in_new_bugmail { $_[0]->set('mailhead', $_[1]); } +sub set_buglist { $_[0]->set('buglist', $_[1]); } +sub set_reverse_desc { $_[0]->set('reverse_desc', $_[1]); } + sub set_visibility_field { - my ($self, $value) = @_; - $self->set('visibility_field_id', $value); - delete $self->{visibility_field}; - delete $self->{visibility_values}; + my ($self, $value) = @_; + $self->set('visibility_field_id', $value); + delete $self->{visibility_field}; + delete $self->{visibility_values}; } + sub set_visibility_values { - my ($self, $value_ids) = @_; - $self->set('visibility_values', $value_ids); + my ($self, $value_ids) = @_; + $self->set('visibility_values', $value_ids); } + sub set_value_field { - my ($self, $value) = @_; - $self->set('value_field_id', $value); - delete $self->{value_field}; + my ($self, $value) = @_; + $self->set('value_field_id', $value); + delete $self->{value_field}; } sub set_is_mandatory { $_[0]->set('is_mandatory', $_[1]); } @@ -934,69 +1061,73 @@ there are no values specified (or EVER specified) for the field. =cut sub remove_from_db { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - my $name = $self->name; + my $name = $self->name; - if (!$self->custom) { - ThrowCodeError('field_not_custom', {'name' => $name }); - } + if (!$self->custom) { + ThrowCodeError('field_not_custom', {'name' => $name}); + } - if (!$self->obsolete) { - ThrowUserError('customfield_not_obsolete', {'name' => $self->name }); - } + if (!$self->obsolete) { + ThrowUserError('customfield_not_obsolete', {'name' => $self->name}); + } - $dbh->bz_start_transaction(); + $dbh->bz_start_transaction(); - # Check to see if bug activity table has records (should be fast with index) - my $has_activity = $dbh->selectrow_array("SELECT COUNT(*) FROM bugs_activity - WHERE fieldid = ?", undef, $self->id); - if ($has_activity) { - ThrowUserError('customfield_has_activity', {'name' => $name }); - } + # Check to see if bug activity table has records (should be fast with index) + my $has_activity = $dbh->selectrow_array( + "SELECT COUNT(*) FROM bugs_activity + WHERE fieldid = ?", undef, $self->id + ); + if ($has_activity) { + ThrowUserError('customfield_has_activity', {'name' => $name}); + } - # Check to see if bugs table has records (slow) - my $bugs_query = ""; + # Check to see if bugs table has records (slow) + my $bugs_query = ""; - if ($self->type == FIELD_TYPE_MULTI_SELECT) { - $bugs_query = "SELECT COUNT(*) FROM bug_$name"; - } - else { - $bugs_query = "SELECT COUNT(*) FROM bugs WHERE $name IS NOT NULL"; - if ($self->type != FIELD_TYPE_BUG_ID - && $self->type != FIELD_TYPE_DATE - && $self->type != FIELD_TYPE_DATETIME) - { - $bugs_query .= " AND $name != ''"; - } - # Ignore the default single select value - if ($self->type == FIELD_TYPE_SINGLE_SELECT) { - $bugs_query .= " AND $name != '---'"; - } + if ($self->type == FIELD_TYPE_MULTI_SELECT) { + $bugs_query = "SELECT COUNT(*) FROM bug_$name"; + } + else { + $bugs_query = "SELECT COUNT(*) FROM bugs WHERE $name IS NOT NULL"; + if ( $self->type != FIELD_TYPE_BUG_ID + && $self->type != FIELD_TYPE_DATE + && $self->type != FIELD_TYPE_DATETIME) + { + $bugs_query .= " AND $name != ''"; } - my $has_bugs = $dbh->selectrow_array($bugs_query); - if ($has_bugs) { - ThrowUserError('customfield_has_contents', {'name' => $name }); + # Ignore the default single select value + if ($self->type == FIELD_TYPE_SINGLE_SELECT) { + $bugs_query .= " AND $name != '---'"; } + } - # Once we reach here, we should be OK to delete. - $self->SUPER::remove_from_db(); + my $has_bugs = $dbh->selectrow_array($bugs_query); + if ($has_bugs) { + ThrowUserError('customfield_has_contents', {'name' => $name}); + } - my $type = $self->type; + # Once we reach here, we should be OK to delete. + $self->SUPER::remove_from_db(); - # the values for multi-select are stored in a seperate table - if ($type != FIELD_TYPE_MULTI_SELECT) { - $dbh->bz_drop_column('bugs', $name); - } + my $type = $self->type; - if ($self->is_select) { - # Delete the table that holds the legal values for this field. - $dbh->bz_drop_field_tables($self); - } + # the values for multi-select are stored in a seperate table + if ($type != FIELD_TYPE_MULTI_SELECT) { + $dbh->bz_drop_column('bugs', $name); + } + + if ($self->is_select) { - $dbh->bz_commit_transaction() + # Delete the table that holds the legal values for this field. + $dbh->bz_drop_field_tables($self); + } + + $dbh->bz_commit_transaction(); } =pod @@ -1042,90 +1173,95 @@ C - boolean - Whether this field is mandatory. Defaults to 0. =cut sub create { - my $class = shift; - my ($params) = @_; - my $dbh = Bugzilla->dbh; - - # This makes sure the "sortkey" validator runs, even if - # the parameter isn't sent to create(). - $params->{sortkey} = undef if !exists $params->{sortkey}; - $params->{type} ||= 0; - # We mark the custom field as obsolete till it has been fully created, - # to avoid race conditions when viewing bugs at the same time. - my $is_obsolete = $params->{obsolete}; - $params->{obsolete} = 1 if $params->{custom}; - - $dbh->bz_start_transaction(); - $class->check_required_create_fields(@_); - my $field_values = $class->run_create_validators($params); - my $visibility_values = delete $field_values->{visibility_values}; - my $field = $class->insert_create_data($field_values); - - $field->set_visibility_values($visibility_values); - $field->_update_visibility_values(); - - $dbh->bz_commit_transaction(); - Bugzilla->memcached->clear_config(); + my $class = shift; + my ($params) = @_; + my $dbh = Bugzilla->dbh; + + # This makes sure the "sortkey" validator runs, even if + # the parameter isn't sent to create(). + $params->{sortkey} = undef if !exists $params->{sortkey}; + $params->{type} ||= 0; + + # We mark the custom field as obsolete till it has been fully created, + # to avoid race conditions when viewing bugs at the same time. + my $is_obsolete = $params->{obsolete}; + $params->{obsolete} = 1 if $params->{custom}; + + $dbh->bz_start_transaction(); + $class->check_required_create_fields(@_); + my $field_values = $class->run_create_validators($params); + my $visibility_values = delete $field_values->{visibility_values}; + my $field = $class->insert_create_data($field_values); + + $field->set_visibility_values($visibility_values); + $field->_update_visibility_values(); + + $dbh->bz_commit_transaction(); + Bugzilla->memcached->clear_config(); + + if ($field->custom) { + my $name = $field->name; + my $type = $field->type; + if (SQL_DEFINITIONS->{$type}) { + + # Create the database column that stores the data for this field. + $dbh->bz_add_column('bugs', $name, SQL_DEFINITIONS->{$type}); + } + + if ($field->is_select) { - if ($field->custom) { - my $name = $field->name; - my $type = $field->type; - if (SQL_DEFINITIONS->{$type}) { - # Create the database column that stores the data for this field. - $dbh->bz_add_column('bugs', $name, SQL_DEFINITIONS->{$type}); - } - - if ($field->is_select) { - # Create the table that holds the legal values for this field. - $dbh->bz_add_field_tables($field); - } - - if ($type == FIELD_TYPE_SINGLE_SELECT) { - # Insert a default value of "---" into the legal values table. - $dbh->do("INSERT INTO $name (value) VALUES ('---')"); - } - - # Restore the original obsolete state of the custom field. - $dbh->do('UPDATE fielddefs SET obsolete = 0 WHERE id = ?', undef, $field->id) - unless $is_obsolete; - - Bugzilla->memcached->clear({ table => 'fielddefs', id => $field->id }); - Bugzilla->memcached->clear_config(); + # Create the table that holds the legal values for this field. + $dbh->bz_add_field_tables($field); } - return $field; -} + if ($type == FIELD_TYPE_SINGLE_SELECT) { -sub update { - my $self = shift; - my $changes = $self->SUPER::update(@_); - my $dbh = Bugzilla->dbh; - if ($changes->{value_field_id} && $self->is_select) { - $dbh->do("UPDATE " . $self->name . " SET visibility_value_id = NULL"); + # Insert a default value of "---" into the legal values table. + $dbh->do("INSERT INTO $name (value) VALUES ('---')"); } - $self->_update_visibility_values(); + + # Restore the original obsolete state of the custom field. + $dbh->do('UPDATE fielddefs SET obsolete = 0 WHERE id = ?', undef, $field->id) + unless $is_obsolete; + + Bugzilla->memcached->clear({table => 'fielddefs', id => $field->id}); Bugzilla->memcached->clear_config(); - return $changes; + } + + return $field; +} + +sub update { + my $self = shift; + my $changes = $self->SUPER::update(@_); + my $dbh = Bugzilla->dbh; + if ($changes->{value_field_id} && $self->is_select) { + $dbh->do("UPDATE " . $self->name . " SET visibility_value_id = NULL"); + } + $self->_update_visibility_values(); + Bugzilla->memcached->clear_config(); + return $changes; } sub _update_visibility_values { - my $self = shift; - my $dbh = Bugzilla->dbh; - - my @visibility_value_ids = map($_->id, @{$self->visibility_values}); - $self->_delete_visibility_values(); - for my $value_id (@visibility_value_ids) { - $dbh->do("INSERT INTO field_visibility (field_id, value_id) - VALUES (?, ?)", undef, $self->id, $value_id); - } + my $self = shift; + my $dbh = Bugzilla->dbh; + + my @visibility_value_ids = map($_->id, @{$self->visibility_values}); + $self->_delete_visibility_values(); + for my $value_id (@visibility_value_ids) { + $dbh->do( + "INSERT INTO field_visibility (field_id, value_id) + VALUES (?, ?)", undef, $self->id, $value_id + ); + } } sub _delete_visibility_values { - my ($self) = @_; - my $dbh = Bugzilla->dbh; - $dbh->do("DELETE FROM field_visibility WHERE field_id = ?", - undef, $self->id); - delete $self->{visibility_values}; + my ($self) = @_; + my $dbh = Bugzilla->dbh; + $dbh->do("DELETE FROM field_visibility WHERE field_id = ?", undef, $self->id); + delete $self->{visibility_values}; } =pod @@ -1148,13 +1284,14 @@ Returns: a reference to a list of valid values. =cut sub get_legal_field_values { - my ($field) = @_; - my $dbh = Bugzilla->dbh; - my $result_ref = $dbh->selectcol_arrayref( - "SELECT value FROM $field + my ($field) = @_; + my $dbh = Bugzilla->dbh; + my $result_ref = $dbh->selectcol_arrayref( + "SELECT value FROM $field WHERE isactive = ? - ORDER BY sortkey, value", undef, (1)); - return $result_ref; + ORDER BY sortkey, value", undef, (1) + ); + return $result_ref; } =over @@ -1173,107 +1310,115 @@ Returns: nothing =cut sub populate_field_definitions { - my $dbh = Bugzilla->dbh; - - # ADD and UPDATE field definitions - foreach my $def (DEFAULT_FIELDS) { - my $field = new Bugzilla::Field({ name => $def->{name} }); - if ($field) { - $field->set_description($def->{desc}); - $field->set_in_new_bugmail($def->{in_new_bugmail}); - $field->set_buglist($def->{buglist}); - $field->_set_type($def->{type}) if $def->{type}; - $field->set_is_mandatory($def->{is_mandatory}); - $field->set_is_numeric($def->{is_numeric}); - $field->update(); - } - else { - if (exists $def->{in_new_bugmail}) { - $def->{mailhead} = $def->{in_new_bugmail}; - delete $def->{in_new_bugmail}; - } - $def->{description} = delete $def->{desc}; - Bugzilla::Field->create($def); - } + my $dbh = Bugzilla->dbh; + + # ADD and UPDATE field definitions + foreach my $def (DEFAULT_FIELDS) { + my $field = new Bugzilla::Field({name => $def->{name}}); + if ($field) { + $field->set_description($def->{desc}); + $field->set_in_new_bugmail($def->{in_new_bugmail}); + $field->set_buglist($def->{buglist}); + $field->_set_type($def->{type}) if $def->{type}; + $field->set_is_mandatory($def->{is_mandatory}); + $field->set_is_numeric($def->{is_numeric}); + $field->update(); } + else { + if (exists $def->{in_new_bugmail}) { + $def->{mailhead} = $def->{in_new_bugmail}; + delete $def->{in_new_bugmail}; + } + $def->{description} = delete $def->{desc}; + Bugzilla::Field->create($def); + } + } - # DELETE fields which were added only accidentally, or which - # were never tracked in bugs_activity. Note that you can never - # delete fields which are used by bugs_activity. - - # Oops. Bug 163299 - $dbh->do("DELETE FROM fielddefs WHERE name='cc_accessible'"); - # Oops. Bug 215319 - $dbh->do("DELETE FROM fielddefs WHERE name='requesters.login_name'"); - # This field was never tracked in bugs_activity, so it's safe to delete. - $dbh->do("DELETE FROM fielddefs WHERE name='attachments.thedata'"); - - # MODIFY old field definitions - - # 2005-11-13 LpSolit@gmail.com - Bug 302599 - # One of the field names was a fragment of SQL code, which is DB dependent. - # We have to rename it to a real name, which is DB independent. - my $new_field_name = 'days_elapsed'; - my $field_description = 'Days since bug changed'; - - my ($old_field_id, $old_field_name) = - $dbh->selectrow_array('SELECT id, name FROM fielddefs - WHERE description = ?', - undef, $field_description); - - if ($old_field_id && ($old_field_name ne $new_field_name)) { - say "SQL fragment found in the 'fielddefs' table..."; - say "Old field name: $old_field_name"; - # We have to fix saved searches first. Queries have been escaped - # before being saved. We have to do the same here to find them. - $old_field_name = url_quote($old_field_name); - my $broken_named_queries = - $dbh->selectall_arrayref('SELECT userid, name, query - FROM namedqueries WHERE ' . - $dbh->sql_istrcmp('query', '?', 'LIKE'), - undef, "%=$old_field_name%"); - - my $sth_UpdateQueries = $dbh->prepare('UPDATE namedqueries SET query = ? - WHERE userid = ? AND name = ?'); - - print "Fixing saved searches...\n" if scalar(@$broken_named_queries); - foreach my $named_query (@$broken_named_queries) { - my ($userid, $name, $query) = @$named_query; - $query =~ s/=\Q$old_field_name\E(&|$)/=$new_field_name$1/gi; - $sth_UpdateQueries->execute($query, $userid, $name); - } - - # We now do the same with saved chart series. - my $broken_series = - $dbh->selectall_arrayref('SELECT series_id, query - FROM series WHERE ' . - $dbh->sql_istrcmp('query', '?', 'LIKE'), - undef, "%=$old_field_name%"); - - my $sth_UpdateSeries = $dbh->prepare('UPDATE series SET query = ? - WHERE series_id = ?'); - - print "Fixing saved chart series...\n" if scalar(@$broken_series); - foreach my $series (@$broken_series) { - my ($series_id, $query) = @$series; - $query =~ s/=\Q$old_field_name\E(&|$)/=$new_field_name$1/gi; - $sth_UpdateSeries->execute($query, $series_id); - } - # Now that saved searches have been fixed, we can fix the field name. - say "Fixing the 'fielddefs' table..."; - say "New field name: $new_field_name"; - $dbh->do('UPDATE fielddefs SET name = ? WHERE id = ?', - undef, ($new_field_name, $old_field_id)); + # DELETE fields which were added only accidentally, or which + # were never tracked in bugs_activity. Note that you can never + # delete fields which are used by bugs_activity. + + # Oops. Bug 163299 + $dbh->do("DELETE FROM fielddefs WHERE name='cc_accessible'"); + + # Oops. Bug 215319 + $dbh->do("DELETE FROM fielddefs WHERE name='requesters.login_name'"); + + # This field was never tracked in bugs_activity, so it's safe to delete. + $dbh->do("DELETE FROM fielddefs WHERE name='attachments.thedata'"); + + # MODIFY old field definitions + + # 2005-11-13 LpSolit@gmail.com - Bug 302599 + # One of the field names was a fragment of SQL code, which is DB dependent. + # We have to rename it to a real name, which is DB independent. + my $new_field_name = 'days_elapsed'; + my $field_description = 'Days since bug changed'; + + my ($old_field_id, $old_field_name) = $dbh->selectrow_array( + 'SELECT id, name FROM fielddefs + WHERE description = ?', undef, $field_description + ); + + if ($old_field_id && ($old_field_name ne $new_field_name)) { + say "SQL fragment found in the 'fielddefs' table..."; + say "Old field name: $old_field_name"; + + # We have to fix saved searches first. Queries have been escaped + # before being saved. We have to do the same here to find them. + $old_field_name = url_quote($old_field_name); + my $broken_named_queries = $dbh->selectall_arrayref( + 'SELECT userid, name, query + FROM namedqueries WHERE ' + . $dbh->sql_istrcmp('query', '?', 'LIKE'), undef, "%=$old_field_name%" + ); + + my $sth_UpdateQueries = $dbh->prepare( + 'UPDATE namedqueries SET query = ? + WHERE userid = ? AND name = ?' + ); + + print "Fixing saved searches...\n" if scalar(@$broken_named_queries); + foreach my $named_query (@$broken_named_queries) { + my ($userid, $name, $query) = @$named_query; + $query =~ s/=\Q$old_field_name\E(&|$)/=$new_field_name$1/gi; + $sth_UpdateQueries->execute($query, $userid, $name); } - # This field has to be created separately, or the above upgrade code - # might not run properly. - Bugzilla::Field->create({ name => $new_field_name, - description => $field_description }) - unless new Bugzilla::Field({ name => $new_field_name }); + # We now do the same with saved chart series. + my $broken_series = $dbh->selectall_arrayref( + 'SELECT series_id, query + FROM series WHERE ' + . $dbh->sql_istrcmp('query', '?', 'LIKE'), undef, "%=$old_field_name%" + ); + + my $sth_UpdateSeries = $dbh->prepare( + 'UPDATE series SET query = ? + WHERE series_id = ?' + ); + + print "Fixing saved chart series...\n" if scalar(@$broken_series); + foreach my $series (@$broken_series) { + my ($series_id, $query) = @$series; + $query =~ s/=\Q$old_field_name\E(&|$)/=$new_field_name$1/gi; + $sth_UpdateSeries->execute($query, $series_id); + } -} + # Now that saved searches have been fixed, we can fix the field name. + say "Fixing the 'fielddefs' table..."; + say "New field name: $new_field_name"; + $dbh->do('UPDATE fielddefs SET name = ? WHERE id = ?', + undef, ($new_field_name, $old_field_id)); + } + # This field has to be created separately, or the above upgrade code + # might not run properly. + Bugzilla::Field->create({ + name => $new_field_name, description => $field_description + }) + unless new Bugzilla::Field({name => $new_field_name}); + +} =head2 Data Validation @@ -1305,32 +1450,32 @@ Returns: 1 on success; 0 on failure if $no_warn is true (else an =cut sub check_field { - my ($name, $value, $legalsRef, $no_warn) = @_; - my $dbh = Bugzilla->dbh; - - # If $legalsRef is undefined, we use the default valid values. - # Valid values for this check are all possible values. - # Using get_legal_values would only return active values, but since - # some bugs may have inactive values set, we want to check them too. - unless (defined $legalsRef) { - $legalsRef = Bugzilla::Field->new({name => $name})->legal_values; - my @values = map($_->name, @$legalsRef); - $legalsRef = \@values; + my ($name, $value, $legalsRef, $no_warn) = @_; + my $dbh = Bugzilla->dbh; + + # If $legalsRef is undefined, we use the default valid values. + # Valid values for this check are all possible values. + # Using get_legal_values would only return active values, but since + # some bugs may have inactive values set, we want to check them too. + unless (defined $legalsRef) { + $legalsRef = Bugzilla::Field->new({name => $name})->legal_values; + my @values = map($_->name, @$legalsRef); + $legalsRef = \@values; - } + } - if (!defined($value) - or trim($value) eq "" - or !grep { $_ eq $value } @$legalsRef) - { - return 0 if $no_warn; # We don't want an error to be thrown; return. - trick_taint($name); + if ( !defined($value) + or trim($value) eq "" + or !grep { $_ eq $value } @$legalsRef) + { + return 0 if $no_warn; # We don't want an error to be thrown; return. + trick_taint($name); - my $field = new Bugzilla::Field({ name => $name }); - my $field_desc = $field ? $field->description : $name; - ThrowCodeError('illegal_field', { field => $field_desc }); - } - return 1; + my $field = new Bugzilla::Field({name => $name}); + my $field_desc = $field ? $field->description : $name; + ThrowCodeError('illegal_field', {field => $field_desc}); + } + return 1; } =pod @@ -1352,10 +1497,10 @@ Returns: the corresponding field ID or an error if the field name =cut sub get_field_id { - my $field = Bugzilla->fields({ by_name => 1 })->{$_[0]} - or ThrowCodeError('invalid_field_name', {field => $_[0]}); + my $field = Bugzilla->fields({by_name => 1})->{$_[0]} + or ThrowCodeError('invalid_field_name', {field => $_[0]}); - return $field->id; + return $field->id; } 1; diff --git a/Bugzilla/Field/Choice.pm b/Bugzilla/Field/Choice.pm index a66f69cee..360f851aa 100644 --- a/Bugzilla/Field/Choice.pm +++ b/Bugzilla/Field/Choice.pm @@ -28,42 +28,42 @@ use Scalar::Util qw(blessed); use constant IS_CONFIG => 1; use constant DB_COLUMNS => qw( - id - value - sortkey - isactive - visibility_value_id + id + value + sortkey + isactive + visibility_value_id ); use constant UPDATE_COLUMNS => qw( - value - sortkey - isactive - visibility_value_id + value + sortkey + isactive + visibility_value_id ); use constant NAME_FIELD => 'value'; use constant LIST_ORDER => 'sortkey, value'; use constant VALIDATORS => { - value => \&_check_value, - sortkey => \&_check_sortkey, - visibility_value_id => \&_check_visibility_value_id, - isactive => \&_check_isactive, + value => \&_check_value, + sortkey => \&_check_sortkey, + visibility_value_id => \&_check_visibility_value_id, + isactive => \&_check_isactive, }; use constant CLASS_MAP => { - bug_status => 'Bugzilla::Status', - classification => 'Bugzilla::Classification', - component => 'Bugzilla::Component', - product => 'Bugzilla::Product', + bug_status => 'Bugzilla::Status', + classification => 'Bugzilla::Classification', + component => 'Bugzilla::Component', + product => 'Bugzilla::Product', }; use constant DEFAULT_MAP => { - op_sys => 'defaultopsys', - rep_platform => 'defaultplatform', - priority => 'defaultpriority', - bug_severity => 'defaultseverity', + op_sys => 'defaultopsys', + rep_platform => 'defaultplatform', + priority => 'defaultpriority', + bug_severity => 'defaultseverity', }; ################# @@ -76,49 +76,50 @@ use constant DEFAULT_MAP => { # are Bugzilla::Status objects. sub type { - my ($class, $field) = @_; - my $field_obj = blessed $field ? $field : Bugzilla::Field->check($field); - my $field_name = $field_obj->name; - - if (my $package = $class->CLASS_MAP->{$field_name}) { - # Callers expect the module to be already loaded. - eval "require $package"; - return $package; - } + my ($class, $field) = @_; + my $field_obj = blessed $field ? $field : Bugzilla::Field->check($field); + my $field_name = $field_obj->name; + + if (my $package = $class->CLASS_MAP->{$field_name}) { - # For generic classes, we use a lowercase class name, so as - # not to interfere with any real subclasses we might make some day. - my $package = "Bugzilla::Field::Choice::$field_name"; - Bugzilla->request_cache->{"field_$package"} = $field_obj; - - # This package only needs to be created once. We check if the DB_TABLE - # glob for this package already exists, which tells us whether or not - # we need to create the package (this works even under mod_perl, where - # this package definition will persist across requests)). - if (!defined *{"${package}::DB_TABLE"}) { - eval <request_cache->{"field_$package"} = $field_obj; + + # This package only needs to be created once. We check if the DB_TABLE + # glob for this package already exists, which tells us whether or not + # we need to create the package (this works even under mod_perl, where + # this package definition will persist across requests)). + if (!defined *{"${package}::DB_TABLE"}) { + eval < '$field_name'; EOC - } + } - return $package; + return $package; } ################ # Constructors # ################ -# We just make new() enforce this, which should give developers +# We just make new() enforce this, which should give developers # the understanding that you can't use Bugzilla::Field::Choice # without calling type(). sub new { - my $class = shift; - if ($class eq 'Bugzilla::Field::Choice') { - ThrowCodeError('field_choice_must_use_type'); - } - $class->SUPER::new(@_); + my $class = shift; + if ($class eq 'Bugzilla::Field::Choice') { + ThrowCodeError('field_choice_must_use_type'); + } + $class->SUPER::new(@_); } ######################### @@ -130,64 +131,66 @@ sub new { # columns. (Normally Bugzilla::Object dies if you pass arguments # that aren't valid columns.) sub create { - my $class = shift; - my ($params) = @_; - foreach my $key (keys %$params) { - if (!grep {$_ eq $key} $class->_get_db_columns) { - delete $params->{$key}; - } + my $class = shift; + my ($params) = @_; + foreach my $key (keys %$params) { + if (!grep { $_ eq $key } $class->_get_db_columns) { + delete $params->{$key}; } - return $class->SUPER::create(@_); + } + return $class->SUPER::create(@_); } sub update { - my $self = shift; - my $dbh = Bugzilla->dbh; - my $fname = $self->field->name; - - $dbh->bz_start_transaction(); - - my ($changes, $old_self) = $self->SUPER::update(@_); - if (exists $changes->{value}) { - my ($old, $new) = @{ $changes->{value} }; - if ($self->field->type == FIELD_TYPE_MULTI_SELECT) { - $dbh->do("UPDATE bug_$fname SET value = ? WHERE value = ?", - undef, $new, $old); - } - else { - $dbh->do("UPDATE bugs SET $fname = ? WHERE $fname = ?", - undef, $new, $old); - } + my $self = shift; + my $dbh = Bugzilla->dbh; + my $fname = $self->field->name; - if ($old_self->is_default) { - my $param = $self->DEFAULT_MAP->{$self->field->name}; - SetParam($param, $self->name); - write_params(); - } + $dbh->bz_start_transaction(); + + my ($changes, $old_self) = $self->SUPER::update(@_); + if (exists $changes->{value}) { + my ($old, $new) = @{$changes->{value}}; + if ($self->field->type == FIELD_TYPE_MULTI_SELECT) { + $dbh->do("UPDATE bug_$fname SET value = ? WHERE value = ?", undef, $new, $old); } + else { + $dbh->do("UPDATE bugs SET $fname = ? WHERE $fname = ?", undef, $new, $old); + } + + if ($old_self->is_default) { + my $param = $self->DEFAULT_MAP->{$self->field->name}; + SetParam($param, $self->name); + write_params(); + } + } - $dbh->bz_commit_transaction(); - return wantarray ? ($changes, $old_self) : $changes; + $dbh->bz_commit_transaction(); + return wantarray ? ($changes, $old_self) : $changes; } sub remove_from_db { - my $self = shift; - if ($self->is_default) { - ThrowUserError('fieldvalue_is_default', - { field => $self->field, value => $self, - param_name => $self->DEFAULT_MAP->{$self->field->name}, - }); - } - if ($self->is_static) { - ThrowUserError('fieldvalue_not_deletable', - { field => $self->field, value => $self }); - } - if ($self->bug_count) { - ThrowUserError("fieldvalue_still_has_bugs", - { field => $self->field, value => $self }); - } - $self->_check_if_controller(); # From ChoiceInterface. - $self->SUPER::remove_from_db(); + my $self = shift; + if ($self->is_default) { + ThrowUserError( + 'fieldvalue_is_default', + { + field => $self->field, + value => $self, + param_name => $self->DEFAULT_MAP->{$self->field->name}, + } + ); + } + if ($self->is_static) { + ThrowUserError('fieldvalue_not_deletable', + {field => $self->field, value => $self}); + } + if ($self->bug_count) { + ThrowUserError("fieldvalue_still_has_bugs", + {field => $self->field, value => $self}); + } + $self->_check_if_controller(); # From ChoiceInterface. + $self->SUPER::remove_from_db(); } ############ @@ -195,12 +198,13 @@ sub remove_from_db { ############ sub set_is_active { $_[0]->set('isactive', $_[1]); } -sub set_name { $_[0]->set('value', $_[1]); } -sub set_sortkey { $_[0]->set('sortkey', $_[1]); } +sub set_name { $_[0]->set('value', $_[1]); } +sub set_sortkey { $_[0]->set('sortkey', $_[1]); } + sub set_visibility_value { - my ($self, $value) = @_; - $self->set('visibility_value_id', $value); - delete $self->{visibility_value}; + my ($self, $value) = @_; + $self->set('visibility_value_id', $value); + delete $self->{visibility_value}; } ############## @@ -208,73 +212,74 @@ sub set_visibility_value { ############## sub _check_isactive { - my ($invocant, $value) = @_; - $value = Bugzilla::Object::check_boolean($invocant, $value); - if (!$value and ref $invocant) { - if ($invocant->is_default) { - my $field = $invocant->field; - ThrowUserError('fieldvalue_is_default', - { value => $invocant, field => $field, - param_name => $invocant->DEFAULT_MAP->{$field->name} - }); - } - if ($invocant->is_static) { - ThrowUserError('fieldvalue_not_deletable', - { value => $invocant, field => $invocant->field }); + my ($invocant, $value) = @_; + $value = Bugzilla::Object::check_boolean($invocant, $value); + if (!$value and ref $invocant) { + if ($invocant->is_default) { + my $field = $invocant->field; + ThrowUserError( + 'fieldvalue_is_default', + { + value => $invocant, + field => $field, + param_name => $invocant->DEFAULT_MAP->{$field->name} } + ); + } + if ($invocant->is_static) { + ThrowUserError('fieldvalue_not_deletable', + {value => $invocant, field => $invocant->field}); } - return $value; + } + return $value; } sub _check_value { - my ($invocant, $value) = @_; + my ($invocant, $value) = @_; - my $field = $invocant->field; + my $field = $invocant->field; - $value = trim($value); + $value = trim($value); - # Make sure people don't rename static values - if (blessed($invocant) && $value ne $invocant->name - && $invocant->is_static) - { - ThrowUserError('fieldvalue_not_editable', - { field => $field, old_value => $invocant }); - } + # Make sure people don't rename static values + if (blessed($invocant) && $value ne $invocant->name && $invocant->is_static) { + ThrowUserError('fieldvalue_not_editable', + {field => $field, old_value => $invocant}); + } - ThrowUserError('fieldvalue_undefined') if !defined $value || $value eq ""; - ThrowUserError('fieldvalue_name_too_long', { value => $value }) - if length($value) > MAX_FIELD_VALUE_SIZE; + ThrowUserError('fieldvalue_undefined') if !defined $value || $value eq ""; + ThrowUserError('fieldvalue_name_too_long', {value => $value}) + if length($value) > MAX_FIELD_VALUE_SIZE; - my $exists = $invocant->type($field)->new({ name => $value }); - if ($exists && (!blessed($invocant) || $invocant->id != $exists->id)) { - ThrowUserError('fieldvalue_already_exists', - { field => $field, value => $exists }); - } + my $exists = $invocant->type($field)->new({name => $value}); + if ($exists && (!blessed($invocant) || $invocant->id != $exists->id)) { + ThrowUserError('fieldvalue_already_exists', + {field => $field, value => $exists}); + } - return $value; + return $value; } sub _check_sortkey { - my ($invocant, $value) = @_; - $value = trim($value); - return 0 if !$value; - # Store for the error message in case detaint_natural clears it. - my $orig_value = $value; - (detaint_natural($value) && $value <= MAX_SMALLINT) - || ThrowUserError('fieldvalue_sortkey_invalid', - { sortkey => $orig_value, - field => $invocant->field }); - return $value; + my ($invocant, $value) = @_; + $value = trim($value); + return 0 if !$value; + + # Store for the error message in case detaint_natural clears it. + my $orig_value = $value; + (detaint_natural($value) && $value <= MAX_SMALLINT) + || ThrowUserError('fieldvalue_sortkey_invalid', + {sortkey => $orig_value, field => $invocant->field}); + return $value; } sub _check_visibility_value_id { - my ($invocant, $value_id) = @_; - $value_id = trim($value_id); - my $field = $invocant->field->value_field; - return undef if !$field || !$value_id; - my $value_obj = Bugzilla::Field::Choice->type($field) - ->check({ id => $value_id }); - return $value_obj->id; + my ($invocant, $value_id) = @_; + $value_id = trim($value_id); + my $field = $invocant->field->value_field; + return undef if !$field || !$value_id; + my $value_obj = Bugzilla::Field::Choice->type($field)->check({id => $value_id}); + return $value_obj->id; } 1; diff --git a/Bugzilla/Field/ChoiceInterface.pm b/Bugzilla/Field/ChoiceInterface.pm index 634d36ad1..eeedfca83 100644 --- a/Bugzilla/Field/ChoiceInterface.pm +++ b/Bugzilla/Field/ChoiceInterface.pm @@ -26,14 +26,19 @@ sub FIELD_NAME { return $_[0]->DB_TABLE; } #################### sub _check_if_controller { - my $self = shift; - my $vis_fields = $self->controls_visibility_of_fields; - my $values = $self->controlled_values_array; - if (@$vis_fields || @$values) { - ThrowUserError('fieldvalue_is_controller', - { value => $self, fields => [map($_->name, @$vis_fields)], - vals => $self->controlled_values }); - } + my $self = shift; + my $vis_fields = $self->controls_visibility_of_fields; + my $values = $self->controlled_values_array; + if (@$vis_fields || @$values) { + ThrowUserError( + 'fieldvalue_is_controller', + { + value => $self, + fields => [map($_->name, @$vis_fields)], + vals => $self->controlled_values + } + ); + } } @@ -42,145 +47,149 @@ sub _check_if_controller { ############# sub is_active { return $_[0]->{'isactive'}; } -sub sortkey { return $_[0]->{'sortkey'}; } +sub sortkey { return $_[0]->{'sortkey'}; } sub bug_count { - my $self = shift; - return $self->{bug_count} if defined $self->{bug_count}; - my $dbh = Bugzilla->dbh; - my $fname = $self->field->name; - my $count; - if ($self->field->type == FIELD_TYPE_MULTI_SELECT) { - $count = $dbh->selectrow_array("SELECT COUNT(*) FROM bug_$fname - WHERE value = ?", undef, $self->name); - } - else { - $count = $dbh->selectrow_array("SELECT COUNT(*) FROM bugs - WHERE $fname = ?", - undef, $self->name); - } - $self->{bug_count} = $count; - return $count; + my $self = shift; + return $self->{bug_count} if defined $self->{bug_count}; + my $dbh = Bugzilla->dbh; + my $fname = $self->field->name; + my $count; + if ($self->field->type == FIELD_TYPE_MULTI_SELECT) { + $count = $dbh->selectrow_array( + "SELECT COUNT(*) FROM bug_$fname + WHERE value = ?", undef, $self->name + ); + } + else { + $count = $dbh->selectrow_array( + "SELECT COUNT(*) FROM bugs + WHERE $fname = ?", undef, $self->name + ); + } + $self->{bug_count} = $count; + return $count; } sub field { - my $invocant = shift; - my $class = ref $invocant || $invocant; - my $cache = Bugzilla->request_cache; - # This is just to make life easier for subclasses. Our auto-generated - # subclasses from Bugzilla::Field::Choice->type() already have this set. - $cache->{"field_$class"} ||= - new Bugzilla::Field({ name => $class->FIELD_NAME }); - return $cache->{"field_$class"}; + my $invocant = shift; + my $class = ref $invocant || $invocant; + my $cache = Bugzilla->request_cache; + + # This is just to make life easier for subclasses. Our auto-generated + # subclasses from Bugzilla::Field::Choice->type() already have this set. + $cache->{"field_$class"} ||= new Bugzilla::Field({name => $class->FIELD_NAME}); + return $cache->{"field_$class"}; } sub is_default { - my $self = shift; - my $name = $self->DEFAULT_MAP->{$self->field->name}; - # If it doesn't exist in DEFAULT_MAP, then there is no parameter - # related to this field. - return 0 unless $name; - return ($self->name eq Bugzilla->params->{$name}) ? 1 : 0; + my $self = shift; + my $name = $self->DEFAULT_MAP->{$self->field->name}; + + # If it doesn't exist in DEFAULT_MAP, then there is no parameter + # related to this field. + return 0 unless $name; + return ($self->name eq Bugzilla->params->{$name}) ? 1 : 0; } sub is_static { - my $self = shift; - # If we need to special-case Resolution for *anything* else, it should - # get its own subclass. - if ($self->field->name eq 'resolution') { - return grep($_ eq $self->name, ('', 'FIXED', 'DUPLICATE')) - ? 1 : 0; - } - elsif ($self->field->custom) { - return $self->name eq '---' ? 1 : 0; - } - return 0; + my $self = shift; + + # If we need to special-case Resolution for *anything* else, it should + # get its own subclass. + if ($self->field->name eq 'resolution') { + return grep($_ eq $self->name, ('', 'FIXED', 'DUPLICATE')) ? 1 : 0; + } + elsif ($self->field->custom) { + return $self->name eq '---' ? 1 : 0; + } + return 0; } sub controls_visibility_of_fields { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - if (!$self->{controls_visibility_of_fields}) { - my $ids = $dbh->selectcol_arrayref( - "SELECT id FROM fielddefs + if (!$self->{controls_visibility_of_fields}) { + my $ids = $dbh->selectcol_arrayref( + "SELECT id FROM fielddefs INNER JOIN field_visibility ON fielddefs.id = field_visibility.field_id - WHERE value_id = ? AND visibility_field_id = ?", undef, - $self->id, $self->field->id); + WHERE value_id = ? AND visibility_field_id = ?", undef, $self->id, + $self->field->id + ); - $self->{controls_visibility_of_fields} = - Bugzilla::Field->new_from_list($ids); - } + $self->{controls_visibility_of_fields} = Bugzilla::Field->new_from_list($ids); + } - return $self->{controls_visibility_of_fields}; + return $self->{controls_visibility_of_fields}; } sub visibility_value { - my $self = shift; - if ($self->{visibility_value_id}) { - require Bugzilla::Field::Choice; - $self->{visibility_value} ||= - Bugzilla::Field::Choice->type($self->field->value_field)->new( - $self->{visibility_value_id}); - } - return $self->{visibility_value}; + my $self = shift; + if ($self->{visibility_value_id}) { + require Bugzilla::Field::Choice; + $self->{visibility_value} + ||= Bugzilla::Field::Choice->type($self->field->value_field) + ->new($self->{visibility_value_id}); + } + return $self->{visibility_value}; } sub controlled_values { - my $self = shift; - return $self->{controlled_values} if defined $self->{controlled_values}; - my $fields = $self->field->controls_values_of; - my %controlled_values; - require Bugzilla::Field::Choice; - foreach my $field (@$fields) { - $controlled_values{$field->name} = - Bugzilla::Field::Choice->type($field) - ->match({ visibility_value_id => $self->id }); - } - $self->{controlled_values} = \%controlled_values; - return $self->{controlled_values}; + my $self = shift; + return $self->{controlled_values} if defined $self->{controlled_values}; + my $fields = $self->field->controls_values_of; + my %controlled_values; + require Bugzilla::Field::Choice; + foreach my $field (@$fields) { + $controlled_values{$field->name} = Bugzilla::Field::Choice->type($field) + ->match({visibility_value_id => $self->id}); + } + $self->{controlled_values} = \%controlled_values; + return $self->{controlled_values}; } sub controlled_values_array { - my ($self) = @_; - my $values = $self->controlled_values; - return [map { @{ $values->{$_} } } keys %$values]; + my ($self) = @_; + my $values = $self->controlled_values; + return [map { @{$values->{$_}} } keys %$values]; } sub is_visible_on_bug { - my ($self, $bug) = @_; + my ($self, $bug) = @_; - # Values currently set on the bug are always shown. - return 1 if $self->is_set_on_bug($bug); + # Values currently set on the bug are always shown. + return 1 if $self->is_set_on_bug($bug); - # Inactive values are, otherwise, never shown. - return 0 if !$self->is_active; + # Inactive values are, otherwise, never shown. + return 0 if !$self->is_active; - # Values without a visibility value are, otherwise, always shown. - my $visibility_value = $self->visibility_value; - return 1 if !$visibility_value; + # Values without a visibility value are, otherwise, always shown. + my $visibility_value = $self->visibility_value; + return 1 if !$visibility_value; - # Values with a visibility value are only shown if the visibility - # value is set on the bug. - return $visibility_value->is_set_on_bug($bug); + # Values with a visibility value are only shown if the visibility + # value is set on the bug. + return $visibility_value->is_set_on_bug($bug); } sub is_set_on_bug { - my ($self, $bug) = @_; - my $field_name = $self->FIELD_NAME; - # This allows bug/create/create.html.tmpl to pass in a hashref that - # looks like a bug object. - my $value = blessed($bug) ? $bug->$field_name : $bug->{$field_name}; - $value = $value->name if blessed($value); - return 0 if !defined $value; - - if ($self->field->type == FIELD_TYPE_BUG_URLS - or $self->field->type == FIELD_TYPE_MULTI_SELECT) - { - return grep($_ eq $self->name, @$value) ? 1 : 0; - } - return $value eq $self->name ? 1 : 0; + my ($self, $bug) = @_; + my $field_name = $self->FIELD_NAME; + + # This allows bug/create/create.html.tmpl to pass in a hashref that + # looks like a bug object. + my $value = blessed($bug) ? $bug->$field_name : $bug->{$field_name}; + $value = $value->name if blessed($value); + return 0 if !defined $value; + + if ( $self->field->type == FIELD_TYPE_BUG_URLS + or $self->field->type == FIELD_TYPE_MULTI_SELECT) + { + return grep($_ eq $self->name, @$value) ? 1 : 0; + } + return $value eq $self->name ? 1 : 0; } 1; diff --git a/Bugzilla/Flag.pm b/Bugzilla/Flag.pm index 50474b885..f0f5856cf 100644 --- a/Bugzilla/Flag.pm +++ b/Bugzilla/Flag.pm @@ -58,8 +58,9 @@ use parent qw(Bugzilla::Object Exporter); #### Initialization #### ############################### -use constant DB_TABLE => 'flags'; +use constant DB_TABLE => 'flags'; use constant LIST_ORDER => 'id'; + # Flags are tracked in bugs_activity. use constant AUDIT_CREATES => 0; use constant AUDIT_UPDATES => 0; @@ -68,35 +69,32 @@ use constant AUDIT_REMOVES => 0; use constant SKIP_REQUESTEE_ON_ERROR => 1; sub DB_COLUMNS { - my $dbh = Bugzilla->dbh; - return qw( - id - type_id - bug_id - attach_id - requestee_id - setter_id - status), - $dbh->sql_date_format('creation_date', '%Y.%m.%d %H:%i:%s') . - ' AS creation_date', - $dbh->sql_date_format('modification_date', '%Y.%m.%d %H:%i:%s') . - ' AS modification_date'; + my $dbh = Bugzilla->dbh; + return qw( + id + type_id + bug_id + attach_id + requestee_id + setter_id + status), + $dbh->sql_date_format('creation_date', '%Y.%m.%d %H:%i:%s') + . ' AS creation_date', + $dbh->sql_date_format('modification_date', '%Y.%m.%d %H:%i:%s') + . ' AS modification_date'; } use constant UPDATE_COLUMNS => qw( - requestee_id - setter_id - status - type_id + requestee_id + setter_id + status + type_id ); -use constant VALIDATORS => { -}; +use constant VALIDATORS => {}; -use constant UPDATE_VALIDATORS => { - setter => \&_check_setter, - status => \&_check_status, -}; +use constant UPDATE_VALIDATORS => + {setter => \&_check_setter, status => \&_check_status,}; ############################### #### Accessors ###### @@ -138,15 +136,15 @@ Returns the timestamp when the flag was last modified. =cut -sub id { return $_[0]->{'id'}; } -sub name { return $_[0]->type->name; } -sub type_id { return $_[0]->{'type_id'}; } -sub bug_id { return $_[0]->{'bug_id'}; } -sub attach_id { return $_[0]->{'attach_id'}; } -sub status { return $_[0]->{'status'}; } -sub setter_id { return $_[0]->{'setter_id'}; } -sub requestee_id { return $_[0]->{'requestee_id'}; } -sub creation_date { return $_[0]->{'creation_date'}; } +sub id { return $_[0]->{'id'}; } +sub name { return $_[0]->type->name; } +sub type_id { return $_[0]->{'type_id'}; } +sub bug_id { return $_[0]->{'bug_id'}; } +sub attach_id { return $_[0]->{'attach_id'}; } +sub status { return $_[0]->{'status'}; } +sub setter_id { return $_[0]->{'setter_id'}; } +sub requestee_id { return $_[0]->{'requestee_id'}; } +sub creation_date { return $_[0]->{'creation_date'}; } sub modification_date { return $_[0]->{'modification_date'}; } ############################### @@ -180,40 +178,42 @@ is an attachment flag, else undefined. =cut sub type { - my $self = shift; + my $self = shift; - return $self->{'type'} ||= new Bugzilla::FlagType($self->{'type_id'}); + return $self->{'type'} ||= new Bugzilla::FlagType($self->{'type_id'}); } sub setter { - my $self = shift; + my $self = shift; - return $self->{'setter'} ||= new Bugzilla::User({ id => $self->{'setter_id'}, cache => 1 }); + return $self->{'setter'} + ||= new Bugzilla::User({id => $self->{'setter_id'}, cache => 1}); } sub requestee { - my $self = shift; + my $self = shift; - if (!defined $self->{'requestee'} && $self->{'requestee_id'}) { - $self->{'requestee'} = new Bugzilla::User({ id => $self->{'requestee_id'}, cache => 1 }); - } - return $self->{'requestee'}; + if (!defined $self->{'requestee'} && $self->{'requestee_id'}) { + $self->{'requestee'} + = new Bugzilla::User({id => $self->{'requestee_id'}, cache => 1}); + } + return $self->{'requestee'}; } sub attachment { - my $self = shift; - return undef unless $self->attach_id; + my $self = shift; + return undef unless $self->attach_id; - require Bugzilla::Attachment; - return $self->{'attachment'} - ||= new Bugzilla::Attachment({ id => $self->attach_id, cache => 1 }); + require Bugzilla::Attachment; + return $self->{'attachment'} + ||= new Bugzilla::Attachment({id => $self->attach_id, cache => 1}); } sub bug { - my $self = shift; + my $self = shift; - require Bugzilla::Bug; - return $self->{'bug'} ||= new Bugzilla::Bug({ id => $self->bug_id, cache => 1 }); + require Bugzilla::Bug; + return $self->{'bug'} ||= new Bugzilla::Bug({id => $self->bug_id, cache => 1}); } ################################ @@ -235,26 +235,27 @@ and returns an array of matching records. =cut sub match { - my $class = shift; - my ($criteria) = @_; - - # If the caller specified only bug or attachment flags, - # limit the query to those kinds of flags. - if (my $type = delete $criteria->{'target_type'}) { - if ($type eq 'bug') { - $criteria->{'attach_id'} = IS_NULL; - } - elsif (!defined $criteria->{'attach_id'}) { - $criteria->{'attach_id'} = NOT_NULL; - } + my $class = shift; + my ($criteria) = @_; + + # If the caller specified only bug or attachment flags, + # limit the query to those kinds of flags. + if (my $type = delete $criteria->{'target_type'}) { + if ($type eq 'bug') { + $criteria->{'attach_id'} = IS_NULL; } - # Flag->snapshot() calls Flag->match() with bug_id and attach_id - # as hash keys, even if attach_id is undefined. - if (exists $criteria->{'attach_id'} && !defined $criteria->{'attach_id'}) { - $criteria->{'attach_id'} = IS_NULL; + elsif (!defined $criteria->{'attach_id'}) { + $criteria->{'attach_id'} = NOT_NULL; } + } + + # Flag->snapshot() calls Flag->match() with bug_id and attach_id + # as hash keys, even if attach_id is undefined. + if (exists $criteria->{'attach_id'} && !defined $criteria->{'attach_id'}) { + $criteria->{'attach_id'} = IS_NULL; + } - return $class->SUPER::match(@_); + return $class->SUPER::match(@_); } =pod @@ -272,8 +273,8 @@ and returns an array of matching records. =cut sub count { - my $class = shift; - return scalar @{$class->match(@_)}; + my $class = shift; + return scalar @{$class->match(@_)}; } ###################################################################### @@ -281,144 +282,156 @@ sub count { ###################################################################### sub set_flag { - my ($class, $obj, $params) = @_; - - my ($bug, $attachment, $obj_flag, $requestee_changed); - if (blessed($obj) && $obj->isa('Bugzilla::Attachment')) { - $attachment = $obj; - $bug = $attachment->bug; + my ($class, $obj, $params) = @_; + + my ($bug, $attachment, $obj_flag, $requestee_changed); + if (blessed($obj) && $obj->isa('Bugzilla::Attachment')) { + $attachment = $obj; + $bug = $attachment->bug; + } + elsif (blessed($obj) && $obj->isa('Bugzilla::Bug')) { + $bug = $obj; + } + else { + ThrowCodeError('flag_unexpected_object', {'caller' => ref $obj}); + } + + # Make sure the user can change flags + my $privs; + $bug->check_can_change_field('flagtypes.name', 0, 1, \$privs) + || ThrowUserError('illegal_change', + {field => 'flagtypes.name', privs => $privs}); + + # Update (or delete) an existing flag. + if ($params->{id}) { + my $flag = $class->check({id => $params->{id}}); + + # Security check: make sure the flag belongs to the bug/attachment. + # We don't check that the user editing the flag can see + # the bug/attachment. That's the job of the caller. + ($attachment && $flag->attach_id && $attachment->id == $flag->attach_id) + || (!$attachment && !$flag->attach_id && $bug->id == $flag->bug_id) + || ThrowCodeError('invalid_flag_association', + {bug_id => $bug->id, attach_id => $attachment ? $attachment->id : undef}); + + # Extract the current flag object from the object. + my ($obj_flagtype) = grep { $_->id == $flag->type_id } @{$obj->flag_types}; + + # If no flagtype can be found for this flag, this means the bug is being + # moved into a product/component where the flag is no longer valid. + # So either we can attach the flag to another flagtype having the same + # name, or we remove the flag. + if (!$obj_flagtype) { + my $success = $flag->retarget($obj); + return unless $success; + + ($obj_flagtype) = grep { $_->id == $flag->type_id } @{$obj->flag_types}; + push(@{$obj_flagtype->{flags}}, $flag); } - elsif (blessed($obj) && $obj->isa('Bugzilla::Bug')) { - $bug = $obj; - } - else { - ThrowCodeError('flag_unexpected_object', { 'caller' => ref $obj }); + ($obj_flag) = grep { $_->id == $flag->id } @{$obj_flagtype->{flags}}; + + # If the flag has the correct type but cannot be found above, this means + # the flag is going to be removed (e.g. because this is a pending request + # and the attachment is being marked as obsolete). + return unless $obj_flag; + + ($obj_flag, $requestee_changed) + = $class->_validate($obj_flag, $obj_flagtype, $params, $bug, $attachment); + } + + # Create a new flag. + elsif ($params->{type_id}) { + + # Don't bother validating types the user didn't touch. + return if $params->{status} eq 'X'; + + my $flagtype = Bugzilla::FlagType->check({id => $params->{type_id}}); + + # Security check: make sure the flag type belongs to the bug/attachment. + ( $attachment + && $flagtype->target_type eq 'attachment' + && scalar(grep { $_->id == $flagtype->id } @{$attachment->flag_types})) + || (!$attachment + && $flagtype->target_type eq 'bug' + && scalar(grep { $_->id == $flagtype->id } @{$bug->flag_types})) + || ThrowCodeError('invalid_flag_association', + {bug_id => $bug->id, attach_id => $attachment ? $attachment->id : undef}); + + # Make sure the flag type is active. + $flagtype->is_active + || ThrowCodeError('flag_type_inactive', {type => $flagtype->name}); + + # Extract the current flagtype object from the object. + my ($obj_flagtype) = grep { $_->id == $flagtype->id } @{$obj->flag_types}; + + # We cannot create a new flag if there is already one and this + # flag type is not multiplicable. + if (!$flagtype->is_multiplicable) { + if (scalar @{$obj_flagtype->{flags}}) { + ThrowUserError('flag_type_not_multiplicable', {type => $flagtype}); + } } - # Make sure the user can change flags - my $privs; - $bug->check_can_change_field('flagtypes.name', 0, 1, \$privs) - || ThrowUserError('illegal_change', - { field => 'flagtypes.name', privs => $privs }); - - # Update (or delete) an existing flag. - if ($params->{id}) { - my $flag = $class->check({ id => $params->{id} }); - - # Security check: make sure the flag belongs to the bug/attachment. - # We don't check that the user editing the flag can see - # the bug/attachment. That's the job of the caller. - ($attachment && $flag->attach_id && $attachment->id == $flag->attach_id) - || (!$attachment && !$flag->attach_id && $bug->id == $flag->bug_id) - || ThrowCodeError('invalid_flag_association', - { bug_id => $bug->id, - attach_id => $attachment ? $attachment->id : undef }); - - # Extract the current flag object from the object. - my ($obj_flagtype) = grep { $_->id == $flag->type_id } @{$obj->flag_types}; - # If no flagtype can be found for this flag, this means the bug is being - # moved into a product/component where the flag is no longer valid. - # So either we can attach the flag to another flagtype having the same - # name, or we remove the flag. - if (!$obj_flagtype) { - my $success = $flag->retarget($obj); - return unless $success; - - ($obj_flagtype) = grep { $_->id == $flag->type_id } @{$obj->flag_types}; - push(@{$obj_flagtype->{flags}}, $flag); - } - ($obj_flag) = grep { $_->id == $flag->id } @{$obj_flagtype->{flags}}; - # If the flag has the correct type but cannot be found above, this means - # the flag is going to be removed (e.g. because this is a pending request - # and the attachment is being marked as obsolete). - return unless $obj_flag; - - ($obj_flag, $requestee_changed) = - $class->_validate($obj_flag, $obj_flagtype, $params, $bug, $attachment); - } - # Create a new flag. - elsif ($params->{type_id}) { - # Don't bother validating types the user didn't touch. - return if $params->{status} eq 'X'; - - my $flagtype = Bugzilla::FlagType->check({ id => $params->{type_id} }); - # Security check: make sure the flag type belongs to the bug/attachment. - ($attachment && $flagtype->target_type eq 'attachment' - && scalar(grep { $_->id == $flagtype->id } @{$attachment->flag_types})) - || (!$attachment && $flagtype->target_type eq 'bug' - && scalar(grep { $_->id == $flagtype->id } @{$bug->flag_types})) - || ThrowCodeError('invalid_flag_association', - { bug_id => $bug->id, - attach_id => $attachment ? $attachment->id : undef }); - - # Make sure the flag type is active. - $flagtype->is_active - || ThrowCodeError('flag_type_inactive', { type => $flagtype->name }); - - # Extract the current flagtype object from the object. - my ($obj_flagtype) = grep { $_->id == $flagtype->id } @{$obj->flag_types}; - - # We cannot create a new flag if there is already one and this - # flag type is not multiplicable. - if (!$flagtype->is_multiplicable) { - if (scalar @{$obj_flagtype->{flags}}) { - ThrowUserError('flag_type_not_multiplicable', { type => $flagtype }); - } - } - - ($obj_flag, $requestee_changed) = - $class->_validate(undef, $obj_flagtype, $params, $bug, $attachment); - } - else { - ThrowCodeError('param_required', { function => $class . '->set_flag', - param => 'id/type_id' }); - } - - if ($obj_flag - && $requestee_changed - && $obj_flag->requestee_id - && $obj_flag->requestee->setting('requestee_cc') eq 'on') - { - $bug->add_cc($obj_flag->requestee); - } + ($obj_flag, $requestee_changed) + = $class->_validate(undef, $obj_flagtype, $params, $bug, $attachment); + } + else { + ThrowCodeError('param_required', + {function => $class . '->set_flag', param => 'id/type_id'}); + } + + if ( $obj_flag + && $requestee_changed + && $obj_flag->requestee_id + && $obj_flag->requestee->setting('requestee_cc') eq 'on') + { + $bug->add_cc($obj_flag->requestee); + } } sub _validate { - my ($class, $flag, $flag_type, $params, $bug, $attachment) = @_; - - # If it's a new flag, let's create it now. - my $obj_flag = $flag || bless({ type_id => $flag_type->id, - status => '', - bug_id => $bug->id, - attach_id => $attachment ? - $attachment->id : undef}, - $class); - - my $old_status = $obj_flag->status; - my $old_requestee_id = $obj_flag->requestee_id; + my ($class, $flag, $flag_type, $params, $bug, $attachment) = @_; - $obj_flag->_set_status($params->{status}); - $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); - - # The setter field MUST NOT be updated if neither the status - # nor the requestee fields changed. - if (($obj_flag->status ne $old_status) || $requestee_changed) { - $obj_flag->_set_setter($params->{setter}); - } - - # If the flag is deleted, remove it from the list. - if ($obj_flag->status eq 'X') { - @{$flag_type->{flags}} = grep { $_->id != $obj_flag->id } @{$flag_type->{flags}}; - return; - } - # Add the newly created flag to the list. - elsif (!$obj_flag->id) { - push(@{$flag_type->{flags}}, $obj_flag); - } - return wantarray ? ($obj_flag, $requestee_changed) : $obj_flag; + # If it's a new flag, let's create it now. + my $obj_flag = $flag || bless( + { + type_id => $flag_type->id, + status => '', + bug_id => $bug->id, + attach_id => $attachment ? $attachment->id : undef + }, + $class + ); + + my $old_status = $obj_flag->status; + my $old_requestee_id = $obj_flag->requestee_id; + + $obj_flag->_set_status($params->{status}); + $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); + + # The setter field MUST NOT be updated if neither the status + # nor the requestee fields changed. + if (($obj_flag->status ne $old_status) || $requestee_changed) { + $obj_flag->_set_setter($params->{setter}); + } + + # If the flag is deleted, remove it from the list. + if ($obj_flag->status eq 'X') { + @{$flag_type->{flags}} + = grep { $_->id != $obj_flag->id } @{$flag_type->{flags}}; + return; + } + + # Add the newly created flag to the list. + elsif (!$obj_flag->id) { + push(@{$flag_type->{flags}}, $obj_flag); + } + return wantarray ? ($obj_flag, $requestee_changed) : $obj_flag; } =pod @@ -434,143 +447,151 @@ Creates a flag record in the database. =cut sub create { - my ($class, $flag, $timestamp) = @_; - $timestamp ||= Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); + my ($class, $flag, $timestamp) = @_; + $timestamp ||= Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); - my $params = {}; - my @columns = grep { $_ ne 'id' } $class->_get_db_columns; + my $params = {}; + my @columns = grep { $_ ne 'id' } $class->_get_db_columns; - # Some columns use date formatting so use alias instead - @columns = map { /\s+AS\s+(.*)$/ ? $1 : $_ } @columns; + # Some columns use date formatting so use alias instead + @columns = map { /\s+AS\s+(.*)$/ ? $1 : $_ } @columns; - $params->{$_} = $flag->{$_} foreach @columns; + $params->{$_} = $flag->{$_} foreach @columns; - $params->{creation_date} = $params->{modification_date} = $timestamp; + $params->{creation_date} = $params->{modification_date} = $timestamp; - $flag = $class->SUPER::create($params); - return $flag; + $flag = $class->SUPER::create($params); + return $flag; } sub update { - my $self = shift; - my $dbh = Bugzilla->dbh; - 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', Bugzilla->local_timezone); - Bugzilla->memcached->clear({ table => 'flags', id => $self->id }); - } - return $changes; + my $self = shift; + my $dbh = Bugzilla->dbh; + 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', Bugzilla->local_timezone); + Bugzilla->memcached->clear({table => 'flags', id => $self->id}); + } + return $changes; } sub snapshot { - my ($class, $flags) = @_; - - my @summaries; - foreach my $flag (@$flags) { - my $summary = $flag->setter->nick . ':' . $flag->type->name . $flag->status; - $summary .= "(" . $flag->requestee->login . ")" if $flag->requestee; - push(@summaries, $summary); - } - return @summaries; + my ($class, $flags) = @_; + + my @summaries; + foreach my $flag (@$flags) { + my $summary = $flag->setter->nick . ':' . $flag->type->name . $flag->status; + $summary .= "(" . $flag->requestee->login . ")" if $flag->requestee; + push(@summaries, $summary); + } + return @summaries; } sub update_activity { - my ($class, $old_summaries, $new_summaries) = @_; + my ($class, $old_summaries, $new_summaries) = @_; - my ($removed, $added) = diff_arrays($old_summaries, $new_summaries); - if (scalar @$removed || scalar @$added) { - # Remove flag requester/setter information - foreach (@$removed, @$added) { s/^[^:]+:// } + my ($removed, $added) = diff_arrays($old_summaries, $new_summaries); + if (scalar @$removed || scalar @$added) { - $removed = join(", ", @$removed); - $added = join(", ", @$added); - return ($removed, $added); - } - return (); + # Remove flag requester/setter information + foreach (@$removed, @$added) {s/^[^:]+://} + + $removed = join(", ", @$removed); + $added = join(", ", @$added); + return ($removed, $added); + } + return (); } sub update_flags { - my ($class, $self, $old_self, $timestamp) = @_; + my ($class, $self, $old_self, $timestamp) = @_; - my @old_summaries = $class->snapshot($old_self->flags); - my %old_flags = map { $_->id => $_ } @{$old_self->flags}; + my @old_summaries = $class->snapshot($old_self->flags); + my %old_flags = map { $_->id => $_ } @{$old_self->flags}; - foreach my $new_flag (@{$self->flags}) { - if (!$new_flag->id) { - # This is a new flag. - my $flag = $class->create($new_flag, $timestamp); - $new_flag->{id} = $flag->id; - $class->notify($new_flag, undef, $self, $timestamp); - } - else { - my $changes = $new_flag->update($timestamp); - if (scalar(keys %$changes)) { - $class->notify($new_flag, $old_flags{$new_flag->id}, $self, $timestamp); - } - delete $old_flags{$new_flag->id}; - } + foreach my $new_flag (@{$self->flags}) { + if (!$new_flag->id) { + + # This is a new flag. + my $flag = $class->create($new_flag, $timestamp); + $new_flag->{id} = $flag->id; + $class->notify($new_flag, undef, $self, $timestamp); } - # These flags have been deleted. - foreach my $old_flag (values %old_flags) { - $class->notify(undef, $old_flag, $self, $timestamp); - $old_flag->remove_from_db(); + else { + my $changes = $new_flag->update($timestamp); + if (scalar(keys %$changes)) { + $class->notify($new_flag, $old_flags{$new_flag->id}, $self, $timestamp); + } + delete $old_flags{$new_flag->id}; } - - # If the bug has been moved into another product or component, - # we must also take care of attachment flags which are no longer valid, - # as well as all bug flags which haven't been forgotten above. - if ($self->isa('Bugzilla::Bug') - && ($self->{_old_product_name} || $self->{_old_component_name})) + } + + # These flags have been deleted. + foreach my $old_flag (values %old_flags) { + $class->notify(undef, $old_flag, $self, $timestamp); + $old_flag->remove_from_db(); + } + + # If the bug has been moved into another product or component, + # we must also take care of attachment flags which are no longer valid, + # as well as all bug flags which haven't been forgotten above. + if ($self->isa('Bugzilla::Bug') + && ($self->{_old_product_name} || $self->{_old_component_name})) + { + my @removed = $class->force_cleanup($self); + push(@old_summaries, @removed); + } + + my @new_summaries = $class->snapshot($self->flags); + my @changes = $class->update_activity(\@old_summaries, \@new_summaries); + + Bugzilla::Hook::process( + 'flag_end_of_update', { - my @removed = $class->force_cleanup($self); - push(@old_summaries, @removed); + object => $self, + timestamp => $timestamp, + old_flags => \@old_summaries, + new_flags => \@new_summaries, } - - my @new_summaries = $class->snapshot($self->flags); - my @changes = $class->update_activity(\@old_summaries, \@new_summaries); - - Bugzilla::Hook::process('flag_end_of_update', { object => $self, - timestamp => $timestamp, - old_flags => \@old_summaries, - new_flags => \@new_summaries, - }); - return @changes; + ); + return @changes; } sub retarget { - my ($self, $obj) = @_; - - my @flagtypes = grep { $_->name eq $self->type->name } @{$obj->flag_types}; - - my $success = 0; - foreach my $flagtype (@flagtypes) { - next if !$flagtype->is_active; - next if (!$flagtype->is_multiplicable && scalar @{$flagtype->{flags}}); - next unless (($self->status eq '?' && $self->setter->can_request_flag($flagtype)) - || $self->setter->can_set_flag($flagtype)); - - $self->{type_id} = $flagtype->id; - delete $self->{type}; - $success = 1; - last; - } - return $success; + my ($self, $obj) = @_; + + my @flagtypes = grep { $_->name eq $self->type->name } @{$obj->flag_types}; + + my $success = 0; + foreach my $flagtype (@flagtypes) { + next if !$flagtype->is_active; + next if (!$flagtype->is_multiplicable && scalar @{$flagtype->{flags}}); + next + unless (($self->status eq '?' && $self->setter->can_request_flag($flagtype)) + || $self->setter->can_set_flag($flagtype)); + + $self->{type_id} = $flagtype->id; + delete $self->{type}; + $success = 1; + last; + } + return $success; } # In case the bug's product/component has changed, clear flags that are # no longer valid. sub force_cleanup { - my ($class, $bug) = @_; - my $dbh = Bugzilla->dbh; + my ($class, $bug) = @_; + my $dbh = Bugzilla->dbh; - my $flag_ids = $dbh->selectcol_arrayref( - 'SELECT DISTINCT flags.id + my $flag_ids = $dbh->selectcol_arrayref( + 'SELECT DISTINCT flags.id FROM flags INNER JOIN bugs ON flags.bug_id = bugs.bug_id @@ -578,48 +599,50 @@ sub force_cleanup { ON flags.type_id = i.type_id AND (bugs.product_id = i.product_id OR i.product_id IS NULL) AND (bugs.component_id = i.component_id OR i.component_id IS NULL) - WHERE bugs.bug_id = ? AND i.type_id IS NULL', - undef, $bug->id); + WHERE bugs.bug_id = ? AND i.type_id IS NULL', undef, $bug->id + ); - my @removed = $class->force_retarget($flag_ids, $bug); + my @removed = $class->force_retarget($flag_ids, $bug); - $flag_ids = $dbh->selectcol_arrayref( - 'SELECT DISTINCT flags.id + $flag_ids = $dbh->selectcol_arrayref( + 'SELECT DISTINCT flags.id FROM flags, bugs, flagexclusions e WHERE bugs.bug_id = ? AND flags.bug_id = bugs.bug_id AND flags.type_id = e.type_id AND (bugs.product_id = e.product_id OR e.product_id IS NULL) AND (bugs.component_id = e.component_id OR e.component_id IS NULL)', - undef, $bug->id); + undef, $bug->id + ); - push(@removed , $class->force_retarget($flag_ids, $bug)); - return @removed; + push(@removed, $class->force_retarget($flag_ids, $bug)); + return @removed; } sub force_retarget { - my ($class, $flag_ids, $bug) = @_; - my $dbh = Bugzilla->dbh; - - my $flags = $class->new_from_list($flag_ids); - my @removed; - foreach my $flag (@$flags) { - # $bug is undefined when e.g. editing inclusion and exclusion lists. - my $obj = $flag->attachment || $bug || $flag->bug; - my $is_retargetted = $flag->retarget($obj); - if ($is_retargetted) { - $dbh->do('UPDATE flags SET type_id = ? WHERE id = ?', - undef, ($flag->type_id, $flag->id)); - Bugzilla->memcached->clear({ table => 'flags', id => $flag->id }); - } - else { - # Track deleted attachment flags. - push(@removed, $class->snapshot([$flag])) if $flag->attach_id; - $class->notify(undef, $flag, $bug || $flag->bug); - $flag->remove_from_db(); - } + my ($class, $flag_ids, $bug) = @_; + my $dbh = Bugzilla->dbh; + + my $flags = $class->new_from_list($flag_ids); + my @removed; + foreach my $flag (@$flags) { + + # $bug is undefined when e.g. editing inclusion and exclusion lists. + my $obj = $flag->attachment || $bug || $flag->bug; + my $is_retargetted = $flag->retarget($obj); + if ($is_retargetted) { + $dbh->do('UPDATE flags SET type_id = ? WHERE id = ?', + undef, ($flag->type_id, $flag->id)); + Bugzilla->memcached->clear({table => 'flags', id => $flag->id}); + } + else { + # Track deleted attachment flags. + push(@removed, $class->snapshot([$flag])) if $flag->attach_id; + $class->notify(undef, $flag, $bug || $flag->bug); + $flag->remove_from_db(); } - return @removed; + } + return @removed; } ############################### @@ -627,164 +650,178 @@ sub force_retarget { ############################### sub _set_requestee { - my ($self, $requestee, $bug, $attachment, $skip_requestee_on_error) = @_; + my ($self, $requestee, $bug, $attachment, $skip_requestee_on_error) = @_; - $self->{requestee} = - $self->_check_requestee($requestee, $bug, $attachment, $skip_requestee_on_error); + $self->{requestee} = $self->_check_requestee($requestee, $bug, $attachment, + $skip_requestee_on_error); - $self->{requestee_id} = - $self->{requestee} ? $self->{requestee}->id : undef; + $self->{requestee_id} = $self->{requestee} ? $self->{requestee}->id : undef; } sub _set_setter { - my ($self, $setter) = @_; + my ($self, $setter) = @_; - $self->set('setter', $setter); - $self->{setter_id} = $self->setter->id; + $self->set('setter', $setter); + $self->{setter_id} = $self->setter->id; } sub _set_status { - my ($self, $status) = @_; + my ($self, $status) = @_; - # Store the old flag status. It's needed by _check_setter(). - $self->{_old_status} = $self->status; - $self->set('status', $status); + # Store the old flag status. It's needed by _check_setter(). + $self->{_old_status} = $self->status; + $self->set('status', $status); } sub _check_requestee { - 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 '?'); - - # Store this value before updating the flag object. - my $old_requestee = $self->requestee ? $self->requestee->login : ''; - - if ($self->status eq '?' && $requestee) { - $requestee = Bugzilla::User->check($requestee); + 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 '?'); + + # Store this value before updating the flag object. + my $old_requestee = $self->requestee ? $self->requestee->login : ''; + + if ($self->status eq '?' && $requestee) { + $requestee = Bugzilla::User->check($requestee); + } + else { + undef $requestee; + } + + if ($requestee && $requestee->login ne $old_requestee) { + + # Make sure the user didn't specify a requestee unless the flag + # is specifically requestable. For existing flags, if the requestee + # was set before the flag became specifically unrequestable, the + # user can either remove them or leave them alone. + ThrowUserError('flag_type_requestee_disabled', {type => $self->type}) + if !$self->type->is_requesteeble; + + # You can't ask a disabled account, as they don't have the ability to + # set the flag. + ThrowUserError('flag_requestee_disabled', {requestee => $requestee}) + if !$requestee->is_enabled; + + # Make sure the requestee can see the bug. + # 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 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; + } + else { + ThrowUserError( + 'flag_requestee_unauthorized', + { + flag_type => $self->type, + requestee => $requestee, + bug_id => $self->bug_id, + attach_id => $self->attach_id + } + ); + } } - else { + + # Make sure the requestee can see the private attachment. + elsif ($self->attach_id && $attachment->isprivate && !$requestee->is_insider) { + if ($skip_requestee_on_error) { undef $requestee; + } + else { + ThrowUserError( + 'flag_requestee_unauthorized_attachment', + { + flag_type => $self->type, + requestee => $requestee, + bug_id => $self->bug_id, + attach_id => $self->attach_id + } + ); + } } - if ($requestee && $requestee->login ne $old_requestee) { - # Make sure the user didn't specify a requestee unless the flag - # is specifically requestable. For existing flags, if the requestee - # was set before the flag became specifically unrequestable, the - # user can either remove them or leave them alone. - ThrowUserError('flag_type_requestee_disabled', { type => $self->type }) - if !$self->type->is_requesteeble; - - # You can't ask a disabled account, as they don't have the ability to - # set the flag. - ThrowUserError('flag_requestee_disabled', { requestee => $requestee }) - if !$requestee->is_enabled; - - # Make sure the requestee can see the bug. - # 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 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; - } - else { - ThrowUserError('flag_requestee_unauthorized', - { flag_type => $self->type, - requestee => $requestee, - bug_id => $self->bug_id, - attach_id => $self->attach_id }); - } - } - # Make sure the requestee can see the private attachment. - elsif ($self->attach_id && $attachment->isprivate && !$requestee->is_insider) { - if ($skip_requestee_on_error) { - undef $requestee; - } - else { - ThrowUserError('flag_requestee_unauthorized_attachment', - { flag_type => $self->type, - requestee => $requestee, - bug_id => $self->bug_id, - attach_id => $self->attach_id }); - } - } - # Make sure the user is allowed to set the flag. - elsif (!$requestee->can_set_flag($self->type)) { - if ($skip_requestee_on_error) { - undef $requestee; - } - else { - ThrowUserError('flag_requestee_needs_privs', - {'requestee' => $requestee, - 'flagtype' => $self->type}); - } - } + # Make sure the user is allowed to set the flag. + elsif (!$requestee->can_set_flag($self->type)) { + if ($skip_requestee_on_error) { + undef $requestee; + } + else { + ThrowUserError('flag_requestee_needs_privs', + {'requestee' => $requestee, 'flagtype' => $self->type}); + } } - return $requestee; + } + return $requestee; } sub _check_setter { - my ($self, $setter) = @_; - - # By default, the currently logged in user is the setter. - $setter ||= Bugzilla->user; - (blessed($setter) && $setter->isa('Bugzilla::User') && $setter->id) - || ThrowUserError('invalid_user'); - - # set_status() has already been called. So this refers - # to the new flag status. - my $status = $self->status; - - # Make sure the user is authorized to modify flags, see bug 180879: - # - The flag exists and is unchanged. - # - The flag setter can unset flag. - # - Users in the request_group can clear pending requests and set flags - # and can rerequest set flags. - # - Users in the grant_group can set/clear flags, including "+" and "-". - unless (($status eq $self->{_old_status}) - || ($status eq 'X' && $setter->id == Bugzilla->user->id) - || (($status eq 'X' || $status eq '?') - && $setter->can_request_flag($self->type)) - || $setter->can_set_flag($self->type)) - { - ThrowUserError('flag_update_denied', - { name => $self->type->name, - status => $status, - old_status => $self->{_old_status} }); - } - - # If the request is being retargetted, we don't update - # the setter, so that the setter gets the notification. - if ($status eq '?' && $self->{_old_status} eq '?') { - return $self->setter; - } - return $setter; + my ($self, $setter) = @_; + + # By default, the currently logged in user is the setter. + $setter ||= Bugzilla->user; + (blessed($setter) && $setter->isa('Bugzilla::User') && $setter->id) + || ThrowUserError('invalid_user'); + + # set_status() has already been called. So this refers + # to the new flag status. + my $status = $self->status; + + # Make sure the user is authorized to modify flags, see bug 180879: + # - The flag exists and is unchanged. + # - The flag setter can unset flag. + # - Users in the request_group can clear pending requests and set flags + # and can rerequest set flags. + # - Users in the grant_group can set/clear flags, including "+" and "-". + unless (($status eq $self->{_old_status}) + || ($status eq 'X' && $setter->id == Bugzilla->user->id) + || (($status eq 'X' || $status eq '?') + && $setter->can_request_flag($self->type)) + || $setter->can_set_flag($self->type)) + { + ThrowUserError( + 'flag_update_denied', + { + name => $self->type->name, + status => $status, + old_status => $self->{_old_status} + } + ); + } + + # If the request is being retargetted, we don't update + # the setter, so that the setter gets the notification. + if ($status eq '?' && $self->{_old_status} eq '?') { + return $self->setter; + } + return $setter; } sub _check_status { - my ($self, $status) = @_; - - # - Make sure the status is valid. - # - Make sure the user didn't request the flag unless it's requestable. - # If the flag existed and was requested before it became unrequestable, - # leave it as is. - if (!grep($status eq $_ , qw(X + - ?)) - || ($status eq '?' && $self->status ne '?' && !$self->type->is_requestable)) - { - ThrowUserError('flag_status_invalid', { id => $self->id, - status => $status }); - } - return $status; + my ($self, $status) = @_; + + # - Make sure the status is valid. + # - Make sure the user didn't request the flag unless it's requestable. + # If the flag existed and was requested before it became unrequestable, + # leave it as is. + if (!grep($status eq $_, qw(X + - ?)) + || ($status eq '?' && $self->status ne '?' && !$self->type->is_requestable)) + { + ThrowUserError('flag_status_invalid', {id => $self->id, status => $status}); + } + return $status; } ###################################################################### @@ -805,128 +842,146 @@ array of hashes. This array is then passed to Flag::create(). =cut sub extract_flags_from_cgi { - my ($class, $bug, $attachment, $vars, $skip) = @_; - my $cgi = Bugzilla->cgi; - - my $match_status = Bugzilla::User::match_field({ - '^requestee(_type)?-(\d+)$' => { 'type' => 'multi' }, - }, undef, $skip); - - $vars->{'match_field'} = 'requestee'; - if ($match_status == USER_MATCH_FAILED) { - $vars->{'message'} = 'user_match_failed'; - } - elsif ($match_status == USER_MATCH_MULTIPLE) { - $vars->{'message'} = 'user_match_multiple'; + my ($class, $bug, $attachment, $vars, $skip) = @_; + my $cgi = Bugzilla->cgi; + + my $match_status + = Bugzilla::User::match_field( + {'^requestee(_type)?-(\d+)$' => {'type' => 'multi'},}, + undef, $skip); + + $vars->{'match_field'} = 'requestee'; + if ($match_status == USER_MATCH_FAILED) { + $vars->{'message'} = 'user_match_failed'; + } + elsif ($match_status == USER_MATCH_MULTIPLE) { + $vars->{'message'} = 'user_match_multiple'; + } + + # Extract a list of flag type IDs from field names. + my @flagtype_ids = map(/^flag_type-(\d+)$/ ? $1 : (), $cgi->param()); + @flagtype_ids = grep($cgi->param("flag_type-$_") ne 'X', @flagtype_ids); + + # Extract a list of existing flag IDs. + my @flag_ids = map(/^flag-(\d+)$/ ? $1 : (), $cgi->param()); + + return ([], []) unless (scalar(@flagtype_ids) || scalar(@flag_ids)); + + my (@new_flags, @flags); + foreach my $flag_id (@flag_ids) { + my $flag = $class->new($flag_id); + + # If the flag no longer exists, ignore it. + next unless $flag; + + my $status = $cgi->param("flag-$flag_id"); + + # If the user entered more than one name into the requestee field + # (i.e. they want more than one person to set the flag) we can reuse + # the existing flag for the first person (who may well be the existing + # requestee), but we have to create new flags for each additional requestee. + my @requestees = $cgi->param("requestee-$flag_id"); + my $requestee_email; + if ($status eq "?" && scalar(@requestees) > 1 && $flag->type->is_multiplicable) + { + # The first person, for which we'll reuse the existing flag. + $requestee_email = shift(@requestees); + + # Create new flags like the existing one for each additional person. + foreach my $login (@requestees) { + push( + @new_flags, + { + type_id => $flag->type_id, + status => "?", + requestee => $login, + skip_roe => $skip + } + ); + } } + elsif ($status eq "?" && scalar(@requestees)) { - # Extract a list of flag type IDs from field names. - my @flagtype_ids = map(/^flag_type-(\d+)$/ ? $1 : (), $cgi->param()); - @flagtype_ids = grep($cgi->param("flag_type-$_") ne 'X', @flagtype_ids); - - # Extract a list of existing flag IDs. - my @flag_ids = map(/^flag-(\d+)$/ ? $1 : (), $cgi->param()); - - return ([], []) unless (scalar(@flagtype_ids) || scalar(@flag_ids)); - - my (@new_flags, @flags); - foreach my $flag_id (@flag_ids) { - my $flag = $class->new($flag_id); - # If the flag no longer exists, ignore it. - next unless $flag; - - my $status = $cgi->param("flag-$flag_id"); - - # If the user entered more than one name into the requestee field - # (i.e. they want more than one person to set the flag) we can reuse - # the existing flag for the first person (who may well be the existing - # requestee), but we have to create new flags for each additional requestee. - my @requestees = $cgi->param("requestee-$flag_id"); - my $requestee_email; - if ($status eq "?" - && scalar(@requestees) > 1 - && $flag->type->is_multiplicable) - { - # The first person, for which we'll reuse the existing flag. - $requestee_email = shift(@requestees); - - # Create new flags like the existing one for each additional person. - foreach my $login (@requestees) { - push(@new_flags, { type_id => $flag->type_id, - status => "?", - requestee => $login, - skip_roe => $skip }); - } - } - elsif ($status eq "?" && scalar(@requestees)) { - # If there are several requestees and the flag type is not multiplicable, - # this will fail. But that's the job of the validator to complain. All - # we do here is to extract and convert data from the CGI. - $requestee_email = trim($cgi->param("requestee-$flag_id") || ''); - } - - push(@flags, { id => $flag_id, - status => $status, - requestee => $requestee_email, - skip_roe => $skip }); + # If there are several requestees and the flag type is not multiplicable, + # this will fail. But that's the job of the validator to complain. All + # we do here is to extract and convert data from the CGI. + $requestee_email = trim($cgi->param("requestee-$flag_id") || ''); } - # Get a list of active flag types available for this product/component. - my $flag_types = Bugzilla::FlagType::match( - { 'product_id' => $bug->{'product_id'}, - 'component_id' => $bug->{'component_id'}, - 'is_active' => 1 }); - - foreach my $flagtype_id (@flagtype_ids) { - # Checks if there are unexpected flags for the product/component. - if (!scalar(grep { $_->id == $flagtype_id } @$flag_types)) { - $vars->{'message'} = 'unexpected_flag_types'; - last; - } + push( + @flags, + { + id => $flag_id, + status => $status, + requestee => $requestee_email, + skip_roe => $skip + } + ); + } + + # Get a list of active flag types available for this product/component. + my $flag_types = Bugzilla::FlagType::match({ + 'product_id' => $bug->{'product_id'}, + 'component_id' => $bug->{'component_id'}, + 'is_active' => 1 + }); + + foreach my $flagtype_id (@flagtype_ids) { + + # Checks if there are unexpected flags for the product/component. + if (!scalar(grep { $_->id == $flagtype_id } @$flag_types)) { + $vars->{'message'} = 'unexpected_flag_types'; + last; } - - foreach my $flag_type (@$flag_types) { - my $type_id = $flag_type->id; - - # Bug flags are only valid for bugs, and attachment flags are - # only valid for attachments. So don't mix both. - next unless ($flag_type->target_type eq 'bug' xor $attachment); - - # We are only interested in flags the user tries to create. - next unless scalar(grep { $_ == $type_id } @flagtype_ids); - - # Get the number of flags of this type already set for this target. - my $has_flags = $class->count( - { 'type_id' => $type_id, - 'target_type' => $attachment ? 'attachment' : 'bug', - 'bug_id' => $bug->bug_id, - 'attach_id' => $attachment ? $attachment->id : undef }); - - # Do not create a new flag of this type if this flag type is - # not multiplicable and already has a flag set. - next if (!$flag_type->is_multiplicable && $has_flags); - - my $status = $cgi->param("flag_type-$type_id"); - trick_taint($status); - - my @logins = $cgi->param("requestee_type-$type_id"); - if ($status eq "?" && scalar(@logins)) { - foreach my $login (@logins) { - push (@new_flags, { type_id => $type_id, - status => $status, - requestee => $login, - skip_roe => $skip }); - last unless $flag_type->is_multiplicable; - } - } - else { - push (@new_flags, { type_id => $type_id, - status => $status }); - } + } + + foreach my $flag_type (@$flag_types) { + my $type_id = $flag_type->id; + + # Bug flags are only valid for bugs, and attachment flags are + # only valid for attachments. So don't mix both. + next unless ($flag_type->target_type eq 'bug' xor $attachment); + + # We are only interested in flags the user tries to create. + next unless scalar(grep { $_ == $type_id } @flagtype_ids); + + # Get the number of flags of this type already set for this target. + my $has_flags = $class->count({ + 'type_id' => $type_id, + 'target_type' => $attachment ? 'attachment' : 'bug', + 'bug_id' => $bug->bug_id, + 'attach_id' => $attachment ? $attachment->id : undef + }); + + # Do not create a new flag of this type if this flag type is + # not multiplicable and already has a flag set. + next if (!$flag_type->is_multiplicable && $has_flags); + + my $status = $cgi->param("flag_type-$type_id"); + trick_taint($status); + + my @logins = $cgi->param("requestee_type-$type_id"); + if ($status eq "?" && scalar(@logins)) { + foreach my $login (@logins) { + push( + @new_flags, + { + type_id => $type_id, + status => $status, + requestee => $login, + skip_roe => $skip + } + ); + last unless $flag_type->is_multiplicable; + } + } + else { + push(@new_flags, {type_id => $type_id, status => $status}); } + } - # Return the list of flags to update and/or to create. - return (\@flags, \@new_flags); + # Return the list of flags to update and/or to create. + return (\@flags, \@new_flags); } =pod @@ -944,100 +999,111 @@ from the previous sub-routine as it is called for changing multiple bugs =cut sub multi_extract_flags_from_cgi { - my ($class, $bug, $vars, $skip) = @_; - my $cgi = Bugzilla->cgi; - - my $match_status = Bugzilla::User::match_field({ - '^requestee(_type)?-(\d+)$' => { 'type' => 'multi' }, - }, undef, $skip); - - $vars->{'match_field'} = 'requestee'; - if ($match_status == USER_MATCH_FAILED) { - $vars->{'message'} = 'user_match_failed'; + my ($class, $bug, $vars, $skip) = @_; + my $cgi = Bugzilla->cgi; + + my $match_status + = Bugzilla::User::match_field( + {'^requestee(_type)?-(\d+)$' => {'type' => 'multi'},}, + undef, $skip); + + $vars->{'match_field'} = 'requestee'; + if ($match_status == USER_MATCH_FAILED) { + $vars->{'message'} = 'user_match_failed'; + } + elsif ($match_status == USER_MATCH_MULTIPLE) { + $vars->{'message'} = 'user_match_multiple'; + } + + # Extract a list of flag type IDs from field names. + my @flagtype_ids = map(/^flag_type-(\d+)$/ ? $1 : (), $cgi->param()); + + my (@new_flags, @flags); + + # Get a list of active flag types available for this product/component. + my $flag_types = Bugzilla::FlagType::match({ + 'product_id' => $bug->{'product_id'}, + 'component_id' => $bug->{'component_id'}, + 'is_active' => 1 + }); + + foreach my $flagtype_id (@flagtype_ids) { + + # Checks if there are unexpected flags for the product/component. + if (!scalar(grep { $_->id == $flagtype_id } @$flag_types)) { + $vars->{'message'} = 'unexpected_flag_types'; + last; } - elsif ($match_status == USER_MATCH_MULTIPLE) { - $vars->{'message'} = 'user_match_multiple'; - } - - # Extract a list of flag type IDs from field names. - my @flagtype_ids = map(/^flag_type-(\d+)$/ ? $1 : (), $cgi->param()); - - my (@new_flags, @flags); - - # Get a list of active flag types available for this product/component. - my $flag_types = Bugzilla::FlagType::match( - { 'product_id' => $bug->{'product_id'}, - 'component_id' => $bug->{'component_id'}, - 'is_active' => 1 }); - - foreach my $flagtype_id (@flagtype_ids) { - # Checks if there are unexpected flags for the product/component. - if (!scalar(grep { $_->id == $flagtype_id } @$flag_types)) { - $vars->{'message'} = 'unexpected_flag_types'; - last; - } - } - - foreach my $flag_type (@$flag_types) { - my $type_id = $flag_type->id; - - # Bug flags are only valid for bugs - next unless ($flag_type->target_type eq 'bug'); - - # We are only interested in flags the user tries to create. - next unless scalar(grep { $_ == $type_id } @flagtype_ids); - - # Get the flags of this type already set for this bug. - my $current_flags = $class->match( - { 'type_id' => $type_id, - 'target_type' => 'bug', - 'bug_id' => $bug->bug_id }); - - # We will update existing flags (instead of creating new ones) - # if the flag exists and the user has not chosen the 'always add' - # option - my $update = scalar(@$current_flags) && ! $cgi->param("flags_add-$type_id"); - - my $status = $cgi->param("flag_type-$type_id"); - trick_taint($status); - - my @logins = $cgi->param("requestee_type-$type_id"); - if ($status eq "?" && scalar(@logins)) { - foreach my $login (@logins) { - if ($update) { - foreach my $current_flag (@$current_flags) { - push (@flags, { id => $current_flag->id, - status => $status, - requestee => $login, - skip_roe => $skip }); - } - } - else { - push (@new_flags, { type_id => $type_id, - status => $status, - requestee => $login, - skip_roe => $skip }); - } - - last unless $flag_type->is_multiplicable; - } + } + + foreach my $flag_type (@$flag_types) { + my $type_id = $flag_type->id; + + # Bug flags are only valid for bugs + next unless ($flag_type->target_type eq 'bug'); + + # We are only interested in flags the user tries to create. + next unless scalar(grep { $_ == $type_id } @flagtype_ids); + + # Get the flags of this type already set for this bug. + my $current_flags + = $class->match({ + 'type_id' => $type_id, 'target_type' => 'bug', 'bug_id' => $bug->bug_id + }); + + # We will update existing flags (instead of creating new ones) + # if the flag exists and the user has not chosen the 'always add' + # option + my $update = scalar(@$current_flags) && !$cgi->param("flags_add-$type_id"); + + my $status = $cgi->param("flag_type-$type_id"); + trick_taint($status); + + my @logins = $cgi->param("requestee_type-$type_id"); + if ($status eq "?" && scalar(@logins)) { + foreach my $login (@logins) { + if ($update) { + foreach my $current_flag (@$current_flags) { + push( + @flags, + { + id => $current_flag->id, + status => $status, + requestee => $login, + skip_roe => $skip + } + ); + } } else { - if ($update) { - foreach my $current_flag (@$current_flags) { - push (@flags, { id => $current_flag->id, - status => $status }); - } - } - else { - push (@new_flags, { type_id => $type_id, - status => $status }); + push( + @new_flags, + { + type_id => $type_id, + status => $status, + requestee => $login, + skip_roe => $skip } + ); } + + last unless $flag_type->is_multiplicable; + } } + else { + if ($update) { + foreach my $current_flag (@$current_flags) { + push(@flags, {id => $current_flag->id, status => $status}); + } + } + else { + push(@new_flags, {type_id => $type_id, status => $status}); + } + } + } - # Return the list of flags to update and/or to create. - return (\@flags, \@new_flags); + # Return the list of flags to update and/or to create. + return (\@flags, \@new_flags); } =pod @@ -1054,113 +1120,121 @@ or deleted. =cut sub notify { - my ($class, $flag, $old_flag, $obj, $timestamp) = @_; - - my ($bug, $attachment); - if (blessed($obj) && $obj->isa('Bugzilla::Attachment')) { - $attachment = $obj; - $bug = $attachment->bug; - } - elsif (blessed($obj) && $obj->isa('Bugzilla::Bug')) { - $bug = $obj; - } - else { - # Not a good time to throw an error. - return; - } - - my $addressee; - # If the flag is set to '?', maybe the requestee wants a notification. - if ($flag && $flag->requestee_id - && (!$old_flag || ($old_flag->requestee_id || 0) != $flag->requestee_id)) - { - if ($flag->requestee->wants_mail([EVT_FLAG_REQUESTED])) { - $addressee = $flag->requestee; - } - } - elsif ($old_flag && $old_flag->status eq '?' - && (!$flag || $flag->status ne '?')) - { - if ($old_flag->setter->wants_mail([EVT_REQUESTED_FLAG])) { - $addressee = $old_flag->setter; - } + my ($class, $flag, $old_flag, $obj, $timestamp) = @_; + + my ($bug, $attachment); + if (blessed($obj) && $obj->isa('Bugzilla::Attachment')) { + $attachment = $obj; + $bug = $attachment->bug; + } + elsif (blessed($obj) && $obj->isa('Bugzilla::Bug')) { + $bug = $obj; + } + else { + # Not a good time to throw an error. + return; + } + + my $addressee; + + # If the flag is set to '?', maybe the requestee wants a notification. + if ( $flag + && $flag->requestee_id + && (!$old_flag || ($old_flag->requestee_id || 0) != $flag->requestee_id)) + { + if ($flag->requestee->wants_mail([EVT_FLAG_REQUESTED])) { + $addressee = $flag->requestee; } - - my $cc_list = $flag ? $flag->type->cc_list : $old_flag->type->cc_list; - # Is there someone to notify? - return unless ($addressee || $cc_list); - - # The email client will display the Date: header in the desired timezone, - # so we can always use UTC here. - $timestamp ||= Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); - $timestamp = format_time($timestamp, '%a, %d %b %Y %T %z', 'UTC'); - - # If the target bug is restricted to one or more groups, then we need - # to make sure we don't send email about it to unauthorized users - # on the request type's CC: list, so we have to trawl the list for users - # not in those groups or email addresses that don't have an account. - my @bug_in_groups = grep {$_->{'ison'} || $_->{'mandatory'}} @{$bug->groups}; - my $attachment_is_private = $attachment ? $attachment->isprivate : undef; - - my %recipients; - foreach my $cc (split(/[, ]+/, $cc_list)) { - my $ccuser = new Bugzilla::User({ name => $cc }); - next if (scalar(@bug_in_groups) && (!$ccuser || !$ccuser->can_see_bug($bug->bug_id))); - next if $attachment_is_private && (!$ccuser || !$ccuser->is_insider); - # Prevent duplicated entries due to case sensitivity. - $cc = $ccuser ? $ccuser->email : $cc; - $recipients{$cc} = $ccuser; - } - - # Only notify if the addressee is allowed to receive the email. - if ($addressee && $addressee->email_enabled) { - $recipients{$addressee->email} = $addressee; - } - # Process and send notification for each recipient. - # If there are users in the CC list who don't have an account, - # use the default language for email notifications. - my $default_lang; - if (grep { !$_ } values %recipients) { - $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; - - # 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; - - my $template = Bugzilla->template_inner($lang); - my $message; - $template->process("email/flagmail.txt.tmpl", $vars, \$message) - || ThrowTemplateError($template->error()); - - MessageToMTA($message); + } + elsif ($old_flag + && $old_flag->status eq '?' + && (!$flag || $flag->status ne '?')) + { + if ($old_flag->setter->wants_mail([EVT_REQUESTED_FLAG])) { + $addressee = $old_flag->setter; } + } + + my $cc_list = $flag ? $flag->type->cc_list : $old_flag->type->cc_list; + + # Is there someone to notify? + return unless ($addressee || $cc_list); + + # The email client will display the Date: header in the desired timezone, + # so we can always use UTC here. + $timestamp ||= Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); + $timestamp = format_time($timestamp, '%a, %d %b %Y %T %z', 'UTC'); + + # If the target bug is restricted to one or more groups, then we need + # to make sure we don't send email about it to unauthorized users + # on the request type's CC: list, so we have to trawl the list for users + # not in those groups or email addresses that don't have an account. + my @bug_in_groups = grep { $_->{'ison'} || $_->{'mandatory'} } @{$bug->groups}; + my $attachment_is_private = $attachment ? $attachment->isprivate : undef; + + my %recipients; + foreach my $cc (split(/[, ]+/, $cc_list)) { + my $ccuser = new Bugzilla::User({name => $cc}); + next + if (scalar(@bug_in_groups) + && (!$ccuser || !$ccuser->can_see_bug($bug->bug_id))); + next if $attachment_is_private && (!$ccuser || !$ccuser->is_insider); + + # Prevent duplicated entries due to case sensitivity. + $cc = $ccuser ? $ccuser->email : $cc; + $recipients{$cc} = $ccuser; + } + + # Only notify if the addressee is allowed to receive the email. + if ($addressee && $addressee->email_enabled) { + $recipients{$addressee->email} = $addressee; + } + + # Process and send notification for each recipient. + # If there are users in the CC list who don't have an account, + # use the default language for email notifications. + my $default_lang; + if (grep { !$_ } values %recipients) { + $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; + + # 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; + + my $template = Bugzilla->template_inner($lang); + my $message; + $template->process("email/flagmail.txt.tmpl", $vars, \$message) + || ThrowTemplateError($template->error()); + + MessageToMTA($message); + } } # This is an internal function used by $bug->flag_types @@ -1168,39 +1242,42 @@ sub notify { # flag types and existing flags set on them. You should never # call this function directly. sub _flag_types { - my ($class, $vars) = @_; - - my $target_type = $vars->{target_type}; - my $flags; - - # Retrieve all existing flags for this bug/attachment. - if ($target_type eq 'bug') { - my $bug_id = delete $vars->{bug_id}; - $flags = $class->match({target_type => 'bug', bug_id => $bug_id}); - } - elsif ($target_type eq 'attachment') { - my $attach_id = delete $vars->{attach_id}; - $flags = $class->match({attach_id => $attach_id}); - } - else { - ThrowCodeError('bad_arg', {argument => 'target_type', - function => $class . '->_flag_types'}); - } - - # Get all available flag types for the given product and component. - my $cache = Bugzilla->request_cache->{flag_types_per_component}->{$vars->{target_type}} ||= {}; - my $flag_data = $cache->{$vars->{component_id}} ||= Bugzilla::FlagType::match($vars); - my $flag_types = dclone($flag_data); - - $_->{flags} = [] foreach @$flag_types; - my %flagtypes = map { $_->id => $_ } @$flag_types; - - # Group existing flags per type, and skip those becoming invalid - # (which can happen when a bug is being moved into a new product - # or component). - @$flags = grep { exists $flagtypes{$_->type_id} } @$flags; - push(@{$flagtypes{$_->type_id}->{flags}}, $_) foreach @$flags; - return $flag_types; + my ($class, $vars) = @_; + + my $target_type = $vars->{target_type}; + my $flags; + + # Retrieve all existing flags for this bug/attachment. + if ($target_type eq 'bug') { + my $bug_id = delete $vars->{bug_id}; + $flags = $class->match({target_type => 'bug', bug_id => $bug_id}); + } + elsif ($target_type eq 'attachment') { + my $attach_id = delete $vars->{attach_id}; + $flags = $class->match({attach_id => $attach_id}); + } + else { + ThrowCodeError('bad_arg', + {argument => 'target_type', function => $class . '->_flag_types'}); + } + + # Get all available flag types for the given product and component. + my $cache + = Bugzilla->request_cache->{flag_types_per_component}->{$vars->{target_type}} + ||= {}; + my $flag_data = $cache->{$vars->{component_id}} + ||= Bugzilla::FlagType::match($vars); + my $flag_types = dclone($flag_data); + + $_->{flags} = [] foreach @$flag_types; + my %flagtypes = map { $_->id => $_ } @$flag_types; + + # Group existing flags per type, and skip those becoming invalid + # (which can happen when a bug is being moved into a new product + # or component). + @$flags = grep { exists $flagtypes{$_->type_id} } @$flags; + push(@{$flagtypes{$_->type_id}->{flags}}, $_) foreach @$flags; + return $flag_types; } 1; diff --git a/Bugzilla/FlagType.pm b/Bugzilla/FlagType.pm index 72b3f64c1..78123d548 100644 --- a/Bugzilla/FlagType.pm +++ b/Bugzilla/FlagType.pm @@ -49,113 +49,114 @@ use parent qw(Bugzilla::Object); #### Initialization #### ############################### -use constant DB_TABLE => 'flagtypes'; +use constant DB_TABLE => 'flagtypes'; use constant LIST_ORDER => 'sortkey, name'; use constant DB_COLUMNS => qw( - id - name - description - cc_list - target_type - sortkey - is_active - is_requestable - is_requesteeble - is_multiplicable - grant_group_id - request_group_id + id + name + description + cc_list + target_type + sortkey + is_active + is_requestable + is_requesteeble + is_multiplicable + grant_group_id + request_group_id ); use constant UPDATE_COLUMNS => qw( - name - description - cc_list - sortkey - is_active - is_requestable - is_requesteeble - is_multiplicable - grant_group_id - request_group_id + name + description + cc_list + sortkey + is_active + is_requestable + is_requesteeble + is_multiplicable + grant_group_id + request_group_id ); use constant VALIDATORS => { - name => \&_check_name, - description => \&_check_description, - cc_list => \&_check_cc_list, - target_type => \&_check_target_type, - sortkey => \&_check_sortkey, - is_active => \&Bugzilla::Object::check_boolean, - is_requestable => \&Bugzilla::Object::check_boolean, - is_requesteeble => \&Bugzilla::Object::check_boolean, - is_multiplicable => \&Bugzilla::Object::check_boolean, - grant_group => \&_check_group, - request_group => \&_check_group, + name => \&_check_name, + description => \&_check_description, + cc_list => \&_check_cc_list, + target_type => \&_check_target_type, + sortkey => \&_check_sortkey, + is_active => \&Bugzilla::Object::check_boolean, + is_requestable => \&Bugzilla::Object::check_boolean, + is_requesteeble => \&Bugzilla::Object::check_boolean, + is_multiplicable => \&Bugzilla::Object::check_boolean, + grant_group => \&_check_group, + request_group => \&_check_group, }; -use constant UPDATE_VALIDATORS => { - grant_group_id => \&_check_group, - request_group_id => \&_check_group, -}; +use constant UPDATE_VALIDATORS => + {grant_group_id => \&_check_group, request_group_id => \&_check_group,}; ############################### sub create { - my $class = shift; - my $dbh = Bugzilla->dbh; + my $class = shift; + my $dbh = Bugzilla->dbh; + + $dbh->bz_start_transaction(); - $dbh->bz_start_transaction(); + $class->check_required_create_fields(@_); + my $params = $class->run_create_validators(@_); - $class->check_required_create_fields(@_); - my $params = $class->run_create_validators(@_); - # In the DB, only the first character of the target type is stored. - $params->{target_type} = substr($params->{target_type}, 0, 1); + # In the DB, only the first character of the target type is stored. + $params->{target_type} = substr($params->{target_type}, 0, 1); - # Extract everything which is not a valid column name. - $params->{grant_group_id} = delete $params->{grant_group}; - $params->{request_group_id} = delete $params->{request_group}; - my $inclusions = delete $params->{inclusions}; - my $exclusions = delete $params->{exclusions}; + # Extract everything which is not a valid column name. + $params->{grant_group_id} = delete $params->{grant_group}; + $params->{request_group_id} = delete $params->{request_group}; + my $inclusions = delete $params->{inclusions}; + my $exclusions = delete $params->{exclusions}; - my $flagtype = $class->insert_create_data($params); + my $flagtype = $class->insert_create_data($params); - $flagtype->set_clusions({ inclusions => $inclusions, - exclusions => $exclusions }); - $flagtype->update(); + $flagtype->set_clusions({inclusions => $inclusions, exclusions => $exclusions}); + $flagtype->update(); - $dbh->bz_commit_transaction(); - return $flagtype; + $dbh->bz_commit_transaction(); + return $flagtype; } sub update { - my $self = shift; - my $dbh = Bugzilla->dbh; - my $flag_id = $self->id; + my $self = shift; + my $dbh = Bugzilla->dbh; + my $flag_id = $self->id; - $dbh->bz_start_transaction(); - my $changes = $self->SUPER::update(@_); + $dbh->bz_start_transaction(); + my $changes = $self->SUPER::update(@_); - # Update the flaginclusions and flagexclusions tables. - foreach my $category ('inclusions', 'exclusions') { - next unless delete $self->{"_update_$category"}; + # Update the flaginclusions and flagexclusions tables. + foreach my $category ('inclusions', 'exclusions') { + next unless delete $self->{"_update_$category"}; - $dbh->do("DELETE FROM flag$category WHERE type_id = ?", undef, $flag_id); + $dbh->do("DELETE FROM flag$category WHERE type_id = ?", undef, $flag_id); - my $sth = $dbh->prepare("INSERT INTO flag$category - (type_id, product_id, component_id) VALUES (?, ?, ?)"); + my $sth = $dbh->prepare( + "INSERT INTO flag$category + (type_id, product_id, component_id) VALUES (?, ?, ?)" + ); - foreach my $prod_comp (values %{$self->{$category}}) { - my ($prod_id, $comp_id) = split(':', $prod_comp); - $prod_id ||= undef; - $comp_id ||= undef; - $sth->execute($flag_id, $prod_id, $comp_id); - } - $changes->{$category} = [0, 1]; + foreach my $prod_comp (values %{$self->{$category}}) { + my ($prod_id, $comp_id) = split(':', $prod_comp); + $prod_id ||= undef; + $comp_id ||= undef; + $sth->execute($flag_id, $prod_id, $comp_id); } + $changes->{$category} = [0, 1]; + } - # Clear existing flags for bugs/attachments in categories no longer on - # the list of inclusions or that have been added to the list of exclusions. - my $flag_ids = $dbh->selectcol_arrayref('SELECT DISTINCT flags.id + # Clear existing flags for bugs/attachments in categories no longer on + # the list of inclusions or that have been added to the list of exclusions. + my $flag_ids = $dbh->selectcol_arrayref( + 'SELECT DISTINCT flags.id FROM flags INNER JOIN bugs ON flags.bug_id = bugs.bug_id @@ -166,11 +167,13 @@ sub update { AND (bugs.component_id = i.component_id OR i.component_id IS NULL)) WHERE flags.type_id = ? - AND i.type_id IS NULL', - undef, $self->id); - Bugzilla::Flag->force_retarget($flag_ids); + AND i.type_id IS NULL', undef, + $self->id + ); + Bugzilla::Flag->force_retarget($flag_ids); - $flag_ids = $dbh->selectcol_arrayref('SELECT DISTINCT flags.id + $flag_ids = $dbh->selectcol_arrayref( + 'SELECT DISTINCT flags.id FROM flags INNER JOIN bugs ON flags.bug_id = bugs.bug_id @@ -181,26 +184,29 @@ sub update { OR e.product_id IS NULL) AND (bugs.component_id = e.component_id OR e.component_id IS NULL)', - undef, $self->id); - Bugzilla::Flag->force_retarget($flag_ids); - - # Silently remove requestees from flags which are no longer - # specifically requestable. - if (!$self->is_requesteeble) { - my $ids = $dbh->selectcol_arrayref( - 'SELECT id FROM flags WHERE type_id = ? AND requestee_id IS NOT NULL', - undef, $self->id); - - if (@$ids) { - $dbh->do('UPDATE flags SET requestee_id = NULL WHERE ' . $dbh->sql_in('id', $ids)); - foreach my $id (@$ids) { - Bugzilla->memcached->clear({ table => 'flags', id => $id }); - } - } + undef, $self->id + ); + Bugzilla::Flag->force_retarget($flag_ids); + + # Silently remove requestees from flags which are no longer + # specifically requestable. + if (!$self->is_requesteeble) { + my $ids + = $dbh->selectcol_arrayref( + 'SELECT id FROM flags WHERE type_id = ? AND requestee_id IS NOT NULL', + undef, $self->id); + + if (@$ids) { + $dbh->do( + 'UPDATE flags SET requestee_id = NULL WHERE ' . $dbh->sql_in('id', $ids)); + foreach my $id (@$ids) { + Bugzilla->memcached->clear({table => 'flags', id => $id}); + } } + } - $dbh->bz_commit_transaction(); - return $changes; + $dbh->bz_commit_transaction(); + return $changes; } ############################### @@ -259,172 +265,174 @@ Returns the sortkey of the flagtype. =cut -sub id { return $_[0]->{'id'}; } -sub name { return $_[0]->{'name'}; } -sub description { return $_[0]->{'description'}; } -sub cc_list { return $_[0]->{'cc_list'}; } -sub target_type { return $_[0]->{'target_type'} eq 'b' ? 'bug' : 'attachment'; } -sub is_active { return $_[0]->{'is_active'}; } -sub is_requestable { return $_[0]->{'is_requestable'}; } -sub is_requesteeble { return $_[0]->{'is_requesteeble'}; } +sub id { return $_[0]->{'id'}; } +sub name { return $_[0]->{'name'}; } +sub description { return $_[0]->{'description'}; } +sub cc_list { return $_[0]->{'cc_list'}; } +sub target_type { return $_[0]->{'target_type'} eq 'b' ? 'bug' : 'attachment'; } +sub is_active { return $_[0]->{'is_active'}; } +sub is_requestable { return $_[0]->{'is_requestable'}; } +sub is_requesteeble { return $_[0]->{'is_requesteeble'}; } sub is_multiplicable { return $_[0]->{'is_multiplicable'}; } -sub sortkey { return $_[0]->{'sortkey'}; } +sub sortkey { return $_[0]->{'sortkey'}; } sub request_group_id { return $_[0]->{'request_group_id'}; } -sub grant_group_id { return $_[0]->{'grant_group_id'}; } +sub grant_group_id { return $_[0]->{'grant_group_id'}; } ################################ # Validators ################################ sub _check_name { - my ($invocant, $name) = @_; + my ($invocant, $name) = @_; - $name = trim($name); - ($name && $name !~ /[\s,]/ && length($name) <= 50) - || ThrowUserError('flag_type_name_invalid', { name => $name }); - return $name; + $name = trim($name); + ($name && $name !~ /[\s,]/ && length($name) <= 50) + || ThrowUserError('flag_type_name_invalid', {name => $name}); + return $name; } sub _check_description { - my ($invocant, $desc) = @_; + my ($invocant, $desc) = @_; - $desc = trim($desc); - $desc || ThrowUserError('flag_type_description_invalid'); - return $desc; + $desc = trim($desc); + $desc || ThrowUserError('flag_type_description_invalid'); + return $desc; } sub _check_cc_list { - my ($invocant, $cc_list) = @_; - - length($cc_list) <= 200 - || ThrowUserError('flag_type_cc_list_invalid', { cc_list => $cc_list }); - - my @addresses = split(/[,\s]+/, $cc_list); - my $addr_spec = $Email::Address::addr_spec; - # We do not call check_email_syntax() because these addresses do not - # require to match 'emailregexp' and do not depend on 'emailsuffix'. - foreach my $address (@addresses) { - ($address !~ /\P{ASCII}/ && $address =~ /^$addr_spec$/) - || ThrowUserError('illegal_email_address', - {addr => $address, default => 1}); - } - return $cc_list; + my ($invocant, $cc_list) = @_; + + length($cc_list) <= 200 + || ThrowUserError('flag_type_cc_list_invalid', {cc_list => $cc_list}); + + my @addresses = split(/[,\s]+/, $cc_list); + my $addr_spec = $Email::Address::addr_spec; + + # We do not call check_email_syntax() because these addresses do not + # require to match 'emailregexp' and do not depend on 'emailsuffix'. + foreach my $address (@addresses) { + ($address !~ /\P{ASCII}/ && $address =~ /^$addr_spec$/) + || ThrowUserError('illegal_email_address', {addr => $address, default => 1}); + } + return $cc_list; } sub _check_target_type { - my ($invocant, $target_type) = @_; + my ($invocant, $target_type) = @_; - ($target_type eq 'bug' || $target_type eq 'attachment') - || ThrowCodeError('flag_type_target_type_invalid', { target_type => $target_type }); - return $target_type; + ($target_type eq 'bug' || $target_type eq 'attachment') + || ThrowCodeError('flag_type_target_type_invalid', + {target_type => $target_type}); + return $target_type; } sub _check_sortkey { - my ($invocant, $sortkey) = @_; + my ($invocant, $sortkey) = @_; - (detaint_natural($sortkey) && $sortkey <= MAX_SMALLINT) - || ThrowUserError('flag_type_sortkey_invalid', { sortkey => $sortkey }); - return $sortkey; + (detaint_natural($sortkey) && $sortkey <= MAX_SMALLINT) + || ThrowUserError('flag_type_sortkey_invalid', {sortkey => $sortkey}); + return $sortkey; } sub _check_group { - my ($invocant, $group) = @_; - return unless $group; + my ($invocant, $group) = @_; + return unless $group; - trick_taint($group); - $group = Bugzilla::Group->check($group); - return $group->id; + trick_taint($group); + $group = Bugzilla::Group->check($group); + return $group->id; } ############################### #### Methods #### ############################### -sub set_name { $_[0]->set('name', $_[1]); } -sub set_description { $_[0]->set('description', $_[1]); } -sub set_cc_list { $_[0]->set('cc_list', $_[1]); } -sub set_sortkey { $_[0]->set('sortkey', $_[1]); } -sub set_is_active { $_[0]->set('is_active', $_[1]); } -sub set_is_requestable { $_[0]->set('is_requestable', $_[1]); } -sub set_is_specifically_requestable { $_[0]->set('is_requesteeble', $_[1]); } -sub set_is_multiplicable { $_[0]->set('is_multiplicable', $_[1]); } -sub set_grant_group { $_[0]->set('grant_group_id', $_[1]); } -sub set_request_group { $_[0]->set('request_group_id', $_[1]); } +sub set_name { $_[0]->set('name', $_[1]); } +sub set_description { $_[0]->set('description', $_[1]); } +sub set_cc_list { $_[0]->set('cc_list', $_[1]); } +sub set_sortkey { $_[0]->set('sortkey', $_[1]); } +sub set_is_active { $_[0]->set('is_active', $_[1]); } +sub set_is_requestable { $_[0]->set('is_requestable', $_[1]); } +sub set_is_specifically_requestable { $_[0]->set('is_requesteeble', $_[1]); } +sub set_is_multiplicable { $_[0]->set('is_multiplicable', $_[1]); } +sub set_grant_group { $_[0]->set('grant_group_id', $_[1]); } +sub set_request_group { $_[0]->set('request_group_id', $_[1]); } sub set_clusions { - my ($self, $list) = @_; - my $user = Bugzilla->user; - my %products; - my $params = {}; - - # If the user has editcomponents privs, then we only need to make sure - # that the product exists. - if ($user->in_group('editcomponents')) { - $params->{allow_inaccessible} = 1; - } - - foreach my $category (keys %$list) { - my %clusions; - my %clusions_as_hash; - - foreach my $prod_comp (@{$list->{$category} || []}) { - my ($prod_id, $comp_id) = split(':', $prod_comp); - my $prod_name = '__Any__'; - my $comp_name = '__Any__'; - # Does the product exist? - if ($prod_id) { - detaint_natural($prod_id) - || ThrowCodeError('param_must_be_numeric', - { function => 'Bugzilla::FlagType::set_clusions' }); - - if (!$products{$prod_id}) { - $params->{id} = $prod_id; - $products{$prod_id} = Bugzilla::Product->check($params); - } - $prod_name = $products{$prod_id}->name; - - # Does the component belong to this product? - if ($comp_id) { - detaint_natural($comp_id) - || ThrowCodeError('param_must_be_numeric', - { function => 'Bugzilla::FlagType::set_clusions' }); - - my ($component) = grep { $_->id == $comp_id } @{$products{$prod_id}->components} - or ThrowUserError('product_unknown_component', - { product => $prod_name, comp_id => $comp_id }); - $comp_name = $component->name; - } - else { - $comp_id = 0; - } - } - else { - $prod_id = 0; - $comp_id = 0; - } - $clusions{"$prod_name:$comp_name"} = "$prod_id:$comp_id"; - $clusions_as_hash{$prod_id}->{$comp_id} = 1; + my ($self, $list) = @_; + my $user = Bugzilla->user; + my %products; + my $params = {}; + + # If the user has editcomponents privs, then we only need to make sure + # that the product exists. + if ($user->in_group('editcomponents')) { + $params->{allow_inaccessible} = 1; + } + + foreach my $category (keys %$list) { + my %clusions; + my %clusions_as_hash; + + foreach my $prod_comp (@{$list->{$category} || []}) { + my ($prod_id, $comp_id) = split(':', $prod_comp); + my $prod_name = '__Any__'; + my $comp_name = '__Any__'; + + # Does the product exist? + if ($prod_id) { + detaint_natural($prod_id) + || ThrowCodeError('param_must_be_numeric', + {function => 'Bugzilla::FlagType::set_clusions'}); + + if (!$products{$prod_id}) { + $params->{id} = $prod_id; + $products{$prod_id} = Bugzilla::Product->check($params); } - - # 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 }); - } + $prod_name = $products{$prod_id}->name; + + # Does the component belong to this product? + if ($comp_id) { + detaint_natural($comp_id) + || ThrowCodeError('param_must_be_numeric', + {function => 'Bugzilla::FlagType::set_clusions'}); + + my ($component) = grep { $_->id == $comp_id } @{$products{$prod_id}->components} + or ThrowUserError('product_unknown_component', + {product => $prod_name, comp_id => $comp_id}); + $comp_name = $component->name; + } + else { + $comp_id = 0; } + } + else { + $prod_id = 0; + $comp_id = 0; + } + $clusions{"$prod_name:$comp_name"} = "$prod_id:$comp_id"; + $clusions_as_hash{$prod_id}->{$comp_id} = 1; + } - # Set the changes - $self->{$category} = \%clusions; - $self->{"${category}_as_hash"} = \%clusions_as_hash; - $self->{"_update_$category"} = 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; + } } =pod @@ -465,76 +473,79 @@ explicitly excluded from the flagtype. =cut sub grant_list { - my $self = shift; - require Bugzilla::User; - my @custusers; - my @allusers = @{Bugzilla->user->get_userlist}; - foreach my $user (@allusers) { - my $user_obj = new Bugzilla::User({name => $user->{login}}); - push(@custusers, $user) if $user_obj->can_set_flag($self); - } - return \@custusers; + my $self = shift; + require Bugzilla::User; + my @custusers; + my @allusers = @{Bugzilla->user->get_userlist}; + foreach my $user (@allusers) { + my $user_obj = new Bugzilla::User({name => $user->{login}}); + push(@custusers, $user) if $user_obj->can_set_flag($self); + } + return \@custusers; } sub grant_group { - my $self = shift; + my $self = shift; - if (!defined $self->{'grant_group'} && $self->{'grant_group_id'}) { - $self->{'grant_group'} = new Bugzilla::Group($self->{'grant_group_id'}); - } - return $self->{'grant_group'}; + if (!defined $self->{'grant_group'} && $self->{'grant_group_id'}) { + $self->{'grant_group'} = new Bugzilla::Group($self->{'grant_group_id'}); + } + return $self->{'grant_group'}; } sub request_group { - my $self = shift; + my $self = shift; - if (!defined $self->{'request_group'} && $self->{'request_group_id'}) { - $self->{'request_group'} = new Bugzilla::Group($self->{'request_group_id'}); - } - return $self->{'request_group'}; + if (!defined $self->{'request_group'} && $self->{'request_group_id'}) { + $self->{'request_group'} = new Bugzilla::Group($self->{'request_group_id'}); + } + return $self->{'request_group'}; } sub flag_count { - my $self = shift; - - if (!defined $self->{'flag_count'}) { - $self->{'flag_count'} = - Bugzilla->dbh->selectrow_array('SELECT COUNT(*) FROM flags - WHERE type_id = ?', undef, $self->{'id'}); - } - return $self->{'flag_count'}; + my $self = shift; + + if (!defined $self->{'flag_count'}) { + $self->{'flag_count'} = Bugzilla->dbh->selectrow_array( + 'SELECT COUNT(*) FROM flags + WHERE type_id = ?', undef, $self->{'id'} + ); + } + return $self->{'flag_count'}; } sub inclusions { - my $self = shift; + my $self = shift; - if (!defined $self->{inclusions}) { - ($self->{inclusions}, $self->{inclusions_as_hash}) = get_clusions($self->id, 'in'); - } - return $self->{inclusions}; + if (!defined $self->{inclusions}) { + ($self->{inclusions}, $self->{inclusions_as_hash}) + = get_clusions($self->id, 'in'); + } + return $self->{inclusions}; } sub inclusions_as_hash { - my $self = shift; + my $self = shift; - $self->inclusions unless defined $self->{inclusions_as_hash}; - return $self->{inclusions_as_hash}; + $self->inclusions unless defined $self->{inclusions_as_hash}; + return $self->{inclusions_as_hash}; } sub exclusions { - my $self = shift; + my $self = shift; - if (!defined $self->{exclusions}) { - ($self->{exclusions}, $self->{exclusions_as_hash}) = get_clusions($self->id, 'ex'); - } - return $self->{exclusions}; + if (!defined $self->{exclusions}) { + ($self->{exclusions}, $self->{exclusions_as_hash}) + = get_clusions($self->id, 'ex'); + } + return $self->{exclusions}; } sub exclusions_as_hash { - my $self = shift; + my $self = shift; - $self->exclusions unless defined $self->{exclusions_as_hash}; - return $self->{exclusions_as_hash}; + $self->exclusions unless defined $self->{exclusions_as_hash}; + return $self->{exclusions_as_hash}; } ###################################################################### @@ -558,11 +569,11 @@ $clusions{'product_name:component_name'} = "product_ID:component_ID" =cut sub get_clusions { - my ($id, $type) = @_; - my $dbh = Bugzilla->dbh; + my ($id, $type) = @_; + my $dbh = Bugzilla->dbh; - my $list = - $dbh->selectall_arrayref("SELECT products.id, products.name, + my $list = $dbh->selectall_arrayref( + "SELECT products.id, products.name, components.id, components.name FROM flagtypes INNER JOIN flag${type}clusions @@ -571,19 +582,19 @@ sub get_clusions { ON flag${type}clusions.product_id = products.id LEFT JOIN components ON flag${type}clusions.component_id = components.id - WHERE flagtypes.id = ?", - undef, $id); - my (%clusions, %clusions_as_hash); - foreach my $data (@$list) { - my ($product_id, $product_name, $component_id, $component_name) = @$data; - $product_id ||= 0; - $product_name ||= "__Any__"; - $component_id ||= 0; - $component_name ||= "__Any__"; - $clusions{"$product_name:$component_name"} = "$product_id:$component_id"; - $clusions_as_hash{$product_id}->{$component_id} = 1; - } - return (\%clusions, \%clusions_as_hash); + WHERE flagtypes.id = ?", undef, $id + ); + my (%clusions, %clusions_as_hash); + foreach my $data (@$list) { + my ($product_id, $product_name, $component_id, $component_name) = @$data; + $product_id ||= 0; + $product_name ||= "__Any__"; + $component_id ||= 0; + $component_name ||= "__Any__"; + $clusions{"$product_name:$component_name"} = "$product_id:$component_id"; + $clusions_as_hash{$product_id}->{$component_id} = 1; + } + return (\%clusions, \%clusions_as_hash); } =pod @@ -600,18 +611,19 @@ and returns a list of matching flagtype objects. =cut sub match { - my ($criteria) = @_; - my $dbh = Bugzilla->dbh; + my ($criteria) = @_; + my $dbh = Bugzilla->dbh; - # Depending on the criteria, we may have to append additional tables. - my $tables = [DB_TABLE]; - my @criteria = sqlify_criteria($criteria, $tables); - $tables = join(' ', @$tables); - $criteria = join(' AND ', @criteria); + # Depending on the criteria, we may have to append additional tables. + my $tables = [DB_TABLE]; + my @criteria = sqlify_criteria($criteria, $tables); + $tables = join(' ', @$tables); + $criteria = join(' AND ', @criteria); - my $flagtype_ids = $dbh->selectcol_arrayref("SELECT id FROM $tables WHERE $criteria"); + my $flagtype_ids + = $dbh->selectcol_arrayref("SELECT id FROM $tables WHERE $criteria"); - return Bugzilla::FlagType->new_from_list($flagtype_ids); + return Bugzilla::FlagType->new_from_list($flagtype_ids); } =pod @@ -627,18 +639,20 @@ Returns the total number of flag types matching the given criteria. =cut sub count { - my ($criteria) = @_; - my $dbh = Bugzilla->dbh; - - # Depending on the criteria, we may have to append additional tables. - my $tables = [DB_TABLE]; - my @criteria = sqlify_criteria($criteria, $tables); - $tables = join(' ', @$tables); - $criteria = join(' AND ', @criteria); - - my $count = $dbh->selectrow_array("SELECT COUNT(flagtypes.id) - FROM $tables WHERE $criteria"); - return $count; + my ($criteria) = @_; + my $dbh = Bugzilla->dbh; + + # Depending on the criteria, we may have to append additional tables. + my $tables = [DB_TABLE]; + my @criteria = sqlify_criteria($criteria, $tables); + $tables = join(' ', @$tables); + $criteria = join(' AND ', @criteria); + + my $count = $dbh->selectrow_array( + "SELECT COUNT(flagtypes.id) + FROM $tables WHERE $criteria" + ); + return $count; } ###################################################################### @@ -646,93 +660,98 @@ sub count { ###################################################################### # Converts a hash of criteria into a list of SQL criteria. -# $criteria is a reference to the criteria (field => value), -# $tables is a reference to an array of tables being accessed +# $criteria is a reference to the criteria (field => value), +# $tables is a reference to an array of tables being accessed # by the query. sub sqlify_criteria { - my ($criteria, $tables) = @_; - my $dbh = Bugzilla->dbh; - - # the generated list of SQL criteria; "1=1" is a clever way of making sure - # there's something in the list so calling code doesn't have to check list - # size before building a WHERE clause out of it - my @criteria = ("1=1"); - - if ($criteria->{name}) { - if (ref($criteria->{name}) eq 'ARRAY') { - my @names = map { $dbh->quote($_) } @{$criteria->{name}}; - # Detaint data as we have quoted it. - foreach my $name (@names) { - trick_taint($name); - } - push @criteria, $dbh->sql_in('flagtypes.name', \@names); - } - else { - my $name = $dbh->quote($criteria->{name}); - trick_taint($name); # Detaint data as we have quoted it. - push(@criteria, "flagtypes.name = $name"); - } + my ($criteria, $tables) = @_; + my $dbh = Bugzilla->dbh; + + # the generated list of SQL criteria; "1=1" is a clever way of making sure + # there's something in the list so calling code doesn't have to check list + # size before building a WHERE clause out of it + my @criteria = ("1=1"); + + if ($criteria->{name}) { + if (ref($criteria->{name}) eq 'ARRAY') { + my @names = map { $dbh->quote($_) } @{$criteria->{name}}; + + # Detaint data as we have quoted it. + foreach my $name (@names) { + trick_taint($name); + } + push @criteria, $dbh->sql_in('flagtypes.name', \@names); } - if ($criteria->{target_type}) { - # The target type is stored in the database as a one-character string - # ("a" for attachment and "b" for bug), but this function takes complete - # names ("attachment" and "bug") for clarity, so we must convert them. - my $target_type = $criteria->{target_type} eq 'bug'? 'b' : 'a'; - push(@criteria, "flagtypes.target_type = '$target_type'"); + else { + my $name = $dbh->quote($criteria->{name}); + trick_taint($name); # Detaint data as we have quoted it. + push(@criteria, "flagtypes.name = $name"); } - if (exists($criteria->{is_active})) { - my $is_active = $criteria->{is_active} ? "1" : "0"; - push(@criteria, "flagtypes.is_active = $is_active"); - } - if ($criteria->{product_id}) { - my $product_id = $criteria->{product_id}; - detaint_natural($product_id) - || ThrowCodeError('bad_arg', { argument => 'product_id', - function => 'Bugzilla::FlagType::sqlify_criteria' }); - - # Add inclusions to the query, which simply involves joining the table - # by flag type ID and target product/component. - push(@$tables, "INNER JOIN flaginclusions AS i ON flagtypes.id = i.type_id"); - push(@criteria, "(i.product_id = $product_id OR i.product_id IS NULL)"); - - # Add exclusions to the query, which is more complicated. First of all, - # we do a LEFT JOIN so we don't miss flag types with no exclusions. - # Then, as with inclusions, we join on flag type ID and target product/ - # component. However, since we want flag types that *aren't* on the - # exclusions list, we add a WHERE criteria to use only records with - # NULL exclusion type, i.e. without any exclusions. - my $join_clause = "flagtypes.id = e.type_id "; - - my $addl_join_clause = ""; - if ($criteria->{component_id}) { - my $component_id = $criteria->{component_id}; - detaint_natural($component_id) - || ThrowCodeError('bad_arg', { argument => 'component_id', - function => 'Bugzilla::FlagType::sqlify_criteria' }); - - push(@criteria, "(i.component_id = $component_id OR i.component_id IS NULL)"); - $join_clause .= "AND (e.component_id = $component_id OR e.component_id IS NULL) "; - } - else { - $addl_join_clause = "AND e.component_id IS NULL OR (i.component_id = e.component_id) "; - } - $join_clause .= "AND ((e.product_id = $product_id $addl_join_clause) OR e.product_id IS NULL)"; - - push(@$tables, "LEFT JOIN flagexclusions AS e ON ($join_clause)"); - push(@criteria, "e.type_id IS NULL"); + } + if ($criteria->{target_type}) { + + # The target type is stored in the database as a one-character string + # ("a" for attachment and "b" for bug), but this function takes complete + # names ("attachment" and "bug") for clarity, so we must convert them. + my $target_type = $criteria->{target_type} eq 'bug' ? 'b' : 'a'; + push(@criteria, "flagtypes.target_type = '$target_type'"); + } + if (exists($criteria->{is_active})) { + my $is_active = $criteria->{is_active} ? "1" : "0"; + push(@criteria, "flagtypes.is_active = $is_active"); + } + if ($criteria->{product_id}) { + my $product_id = $criteria->{product_id}; + detaint_natural($product_id) + || ThrowCodeError('bad_arg', + {argument => 'product_id', function => 'Bugzilla::FlagType::sqlify_criteria'}); + + # Add inclusions to the query, which simply involves joining the table + # by flag type ID and target product/component. + push(@$tables, "INNER JOIN flaginclusions AS i ON flagtypes.id = i.type_id"); + push(@criteria, "(i.product_id = $product_id OR i.product_id IS NULL)"); + + # Add exclusions to the query, which is more complicated. First of all, + # we do a LEFT JOIN so we don't miss flag types with no exclusions. + # Then, as with inclusions, we join on flag type ID and target product/ + # component. However, since we want flag types that *aren't* on the + # exclusions list, we add a WHERE criteria to use only records with + # NULL exclusion type, i.e. without any exclusions. + my $join_clause = "flagtypes.id = e.type_id "; + + my $addl_join_clause = ""; + if ($criteria->{component_id}) { + my $component_id = $criteria->{component_id}; + detaint_natural($component_id) || ThrowCodeError('bad_arg', + {argument => 'component_id', function => 'Bugzilla::FlagType::sqlify_criteria'} + ); + + push(@criteria, "(i.component_id = $component_id OR i.component_id IS NULL)"); + $join_clause + .= "AND (e.component_id = $component_id OR e.component_id IS NULL) "; } - if ($criteria->{group}) { - my $gid = $criteria->{group}; - detaint_natural($gid) - || ThrowCodeError('bad_arg', { argument => 'group', - function => 'Bugzilla::FlagType::sqlify_criteria' }); - - push(@criteria, "(flagtypes.grant_group_id = $gid " . - " OR flagtypes.request_group_id = $gid)"); + else { + $addl_join_clause + = "AND e.component_id IS NULL OR (i.component_id = e.component_id) "; } - - return @criteria; + $join_clause + .= "AND ((e.product_id = $product_id $addl_join_clause) OR e.product_id IS NULL)"; + + push(@$tables, "LEFT JOIN flagexclusions AS e ON ($join_clause)"); + push(@criteria, "e.type_id IS NULL"); + } + if ($criteria->{group}) { + my $gid = $criteria->{group}; + detaint_natural($gid) + || ThrowCodeError('bad_arg', + {argument => 'group', function => 'Bugzilla::FlagType::sqlify_criteria'}); + + push(@criteria, + "(flagtypes.grant_group_id = $gid " . " OR flagtypes.request_group_id = $gid)"); + } + + return @criteria; } 1; diff --git a/Bugzilla/Group.pm b/Bugzilla/Group.pm index f7a50f7f1..157f8cd32 100644 --- a/Bugzilla/Group.pm +++ b/Bugzilla/Group.pm @@ -25,13 +25,13 @@ use Bugzilla::Config qw(:admin); use constant IS_CONFIG => 1; use constant DB_COLUMNS => qw( - groups.id - groups.name - groups.description - groups.isbuggroup - groups.userregexp - groups.isactive - groups.icon_url + groups.id + groups.name + groups.description + groups.isbuggroup + groups.userregexp + groups.isactive + groups.icon_url ); use constant DB_TABLE => 'groups'; @@ -39,136 +39,136 @@ use constant DB_TABLE => 'groups'; use constant LIST_ORDER => 'isbuggroup, name'; use constant VALIDATORS => { - name => \&_check_name, - description => \&_check_description, - userregexp => \&_check_user_regexp, - isactive => \&_check_is_active, - isbuggroup => \&_check_is_bug_group, - icon_url => \&_check_icon_url, + name => \&_check_name, + description => \&_check_description, + userregexp => \&_check_user_regexp, + isactive => \&_check_is_active, + isbuggroup => \&_check_is_bug_group, + icon_url => \&_check_icon_url, }; use constant UPDATE_COLUMNS => qw( - name - description - userregexp - isactive - icon_url + name + description + userregexp + isactive + icon_url ); # Parameters that are lists of groups. use constant GROUP_PARAMS => qw( - chartgroup comment_taggers_group debug_group insidergroup - querysharegroup timetrackinggroup + chartgroup comment_taggers_group debug_group insidergroup + querysharegroup timetrackinggroup ); ############################### #### Accessors ###### ############################### -sub description { return $_[0]->{'description'}; } -sub is_bug_group { return $_[0]->{'isbuggroup'}; } -sub user_regexp { return $_[0]->{'userregexp'}; } -sub is_active { return $_[0]->{'isactive'}; } -sub icon_url { return $_[0]->{'icon_url'}; } +sub description { return $_[0]->{'description'}; } +sub is_bug_group { return $_[0]->{'isbuggroup'}; } +sub user_regexp { return $_[0]->{'userregexp'}; } +sub is_active { return $_[0]->{'isactive'}; } +sub icon_url { return $_[0]->{'icon_url'}; } sub bugs { - my $self = shift; - return $self->{bugs} if exists $self->{bugs}; - my $bug_ids = Bugzilla->dbh->selectcol_arrayref( - 'SELECT bug_id FROM bug_group_map WHERE group_id = ?', - undef, $self->id); - require Bugzilla::Bug; - $self->{bugs} = Bugzilla::Bug->new_from_list($bug_ids); - return $self->{bugs}; + my $self = shift; + return $self->{bugs} if exists $self->{bugs}; + my $bug_ids + = Bugzilla->dbh->selectcol_arrayref( + 'SELECT bug_id FROM bug_group_map WHERE group_id = ?', + undef, $self->id); + require Bugzilla::Bug; + $self->{bugs} = Bugzilla::Bug->new_from_list($bug_ids); + return $self->{bugs}; } sub members_direct { - my ($self) = @_; - $self->{members_direct} ||= $self->_get_members(GRANT_DIRECT); - return $self->{members_direct}; + my ($self) = @_; + $self->{members_direct} ||= $self->_get_members(GRANT_DIRECT); + return $self->{members_direct}; } sub members_non_inherited { - my ($self) = @_; - $self->{members_non_inherited} ||= $self->_get_members(); - return $self->{members_non_inherited}; + my ($self) = @_; + $self->{members_non_inherited} ||= $self->_get_members(); + return $self->{members_non_inherited}; } # A helper for members_direct and members_non_inherited sub _get_members { - my ($self, $grant_type) = @_; - my $dbh = Bugzilla->dbh; - my $grant_clause = defined($grant_type) ? "AND grant_type = $grant_type" - : ""; - my $user_ids = $dbh->selectcol_arrayref( - "SELECT DISTINCT user_id + my ($self, $grant_type) = @_; + my $dbh = Bugzilla->dbh; + my $grant_clause = defined($grant_type) ? "AND grant_type = $grant_type" : ""; + my $user_ids = $dbh->selectcol_arrayref( + "SELECT DISTINCT user_id FROM user_group_map - WHERE isbless = 0 $grant_clause AND group_id = ?", undef, $self->id); - require Bugzilla::User; - return Bugzilla::User->new_from_list($user_ids); + WHERE isbless = 0 $grant_clause AND group_id = ?", undef, $self->id + ); + require Bugzilla::User; + return Bugzilla::User->new_from_list($user_ids); } sub flag_types { - my $self = shift; - require Bugzilla::FlagType; - $self->{flag_types} ||= Bugzilla::FlagType::match({ group => $self->id }); - return $self->{flag_types}; + my $self = shift; + require Bugzilla::FlagType; + $self->{flag_types} ||= Bugzilla::FlagType::match({group => $self->id}); + return $self->{flag_types}; } sub grant_direct { - my ($self, $type) = @_; - $self->{grant_direct} ||= {}; - return $self->{grant_direct}->{$type} - if defined $self->{grant_direct}->{$type}; - my $dbh = Bugzilla->dbh; - - my $ids = $dbh->selectcol_arrayref( - "SELECT member_id FROM group_group_map - WHERE grantor_id = ? AND grant_type = $type", - undef, $self->id) || []; - - $self->{grant_direct}->{$type} = $self->new_from_list($ids); - return $self->{grant_direct}->{$type}; + my ($self, $type) = @_; + $self->{grant_direct} ||= {}; + return $self->{grant_direct}->{$type} if defined $self->{grant_direct}->{$type}; + my $dbh = Bugzilla->dbh; + + my $ids = $dbh->selectcol_arrayref( + "SELECT member_id FROM group_group_map + WHERE grantor_id = ? AND grant_type = $type", undef, $self->id + ) || []; + + $self->{grant_direct}->{$type} = $self->new_from_list($ids); + return $self->{grant_direct}->{$type}; } sub granted_by_direct { - my ($self, $type) = @_; - $self->{granted_by_direct} ||= {}; - return $self->{granted_by_direct}->{$type} - if defined $self->{granted_by_direct}->{$type}; - my $dbh = Bugzilla->dbh; - - my $ids = $dbh->selectcol_arrayref( - "SELECT grantor_id FROM group_group_map - WHERE member_id = ? AND grant_type = $type", - undef, $self->id) || []; - - $self->{granted_by_direct}->{$type} = $self->new_from_list($ids); - return $self->{granted_by_direct}->{$type}; + my ($self, $type) = @_; + $self->{granted_by_direct} ||= {}; + return $self->{granted_by_direct}->{$type} + if defined $self->{granted_by_direct}->{$type}; + my $dbh = Bugzilla->dbh; + + my $ids = $dbh->selectcol_arrayref( + "SELECT grantor_id FROM group_group_map + WHERE member_id = ? AND grant_type = $type", undef, $self->id + ) || []; + + $self->{granted_by_direct}->{$type} = $self->new_from_list($ids); + return $self->{granted_by_direct}->{$type}; } sub products { - my $self = shift; - return $self->{products} if exists $self->{products}; - my $product_data = Bugzilla->dbh->selectall_arrayref( - 'SELECT product_id, entry, membercontrol, othercontrol, + my $self = shift; + return $self->{products} if exists $self->{products}; + my $product_data = Bugzilla->dbh->selectall_arrayref( + 'SELECT product_id, entry, membercontrol, othercontrol, canedit, editcomponents, editbugs, canconfirm - FROM group_control_map WHERE group_id = ?', {Slice=>{}}, - $self->id); - my @ids = map { $_->{product_id} } @$product_data; - require Bugzilla::Product; - my $products = Bugzilla::Product->new_from_list(\@ids); - my %data_map = map { $_->{product_id} => $_ } @$product_data; - my @retval; - foreach my $product (@$products) { - # Data doesn't need to contain product_id--we already have - # the product object. - delete $data_map{$product->id}->{product_id}; - push(@retval, { controls => $data_map{$product->id}, - product => $product }); - } - $self->{products} = \@retval; - return $self->{products}; + FROM group_control_map WHERE group_id = ?', {Slice => {}}, $self->id + ); + my @ids = map { $_->{product_id} } @$product_data; + require Bugzilla::Product; + my $products = Bugzilla::Product->new_from_list(\@ids); + my %data_map = map { $_->{product_id} => $_ } @$product_data; + my @retval; + foreach my $product (@$products) { + + # Data doesn't need to contain product_id--we already have + # the product object. + delete $data_map{$product->id}->{product_id}; + push(@retval, {controls => $data_map{$product->id}, product => $product}); + } + $self->{products} = \@retval; + return $self->{products}; } ############################### @@ -176,126 +176,127 @@ sub products { ############################### sub check_members_are_visible { - my $self = shift; - my $user = Bugzilla->user; - return if !Bugzilla->params->{'usevisibilitygroups'}; - - my $group_id = $self->id; - my $is_visible = grep { $_ == $group_id } @{ $user->visible_groups_inherited }; - if (!$is_visible) { - ThrowUserError('group_not_visible', { group => $self }); - } + my $self = shift; + my $user = Bugzilla->user; + return if !Bugzilla->params->{'usevisibilitygroups'}; + + my $group_id = $self->id; + my $is_visible = grep { $_ == $group_id } @{$user->visible_groups_inherited}; + if (!$is_visible) { + ThrowUserError('group_not_visible', {group => $self}); + } } sub set_description { $_[0]->set('description', $_[1]); } -sub set_is_active { $_[0]->set('isactive', $_[1]); } -sub set_name { $_[0]->set('name', $_[1]); } -sub set_user_regexp { $_[0]->set('userregexp', $_[1]); } -sub set_icon_url { $_[0]->set('icon_url', $_[1]); } +sub set_is_active { $_[0]->set('isactive', $_[1]); } +sub set_name { $_[0]->set('name', $_[1]); } +sub set_user_regexp { $_[0]->set('userregexp', $_[1]); } +sub set_icon_url { $_[0]->set('icon_url', $_[1]); } sub update { - my $self = shift; - my $dbh = Bugzilla->dbh; - $dbh->bz_start_transaction(); - my $changes = $self->SUPER::update(@_); - - if (exists $changes->{name}) { - my ($old_name, $new_name) = @{$changes->{name}}; - my $update_params; - foreach my $group (GROUP_PARAMS) { - if ($old_name eq Bugzilla->params->{$group}) { - SetParam($group, $new_name); - $update_params = 1; - } - } - write_params() if $update_params; + my $self = shift; + my $dbh = Bugzilla->dbh; + $dbh->bz_start_transaction(); + my $changes = $self->SUPER::update(@_); + + if (exists $changes->{name}) { + my ($old_name, $new_name) = @{$changes->{name}}; + my $update_params; + foreach my $group (GROUP_PARAMS) { + if ($old_name eq Bugzilla->params->{$group}) { + SetParam($group, $new_name); + $update_params = 1; + } } + write_params() if $update_params; + } - # If we've changed this group to be active, fix any Mandatory groups. - $self->_enforce_mandatory if (exists $changes->{isactive} - && $changes->{isactive}->[1]); + # If we've changed this group to be active, fix any Mandatory groups. + $self->_enforce_mandatory + if (exists $changes->{isactive} && $changes->{isactive}->[1]); - $self->_rederive_regexp() if exists $changes->{userregexp}; + $self->_rederive_regexp() if exists $changes->{userregexp}; - Bugzilla::Hook::process('group_end_of_update', - { group => $self, changes => $changes }); - $dbh->bz_commit_transaction(); - Bugzilla->memcached->clear_config(); - return $changes; + Bugzilla::Hook::process('group_end_of_update', + {group => $self, changes => $changes}); + $dbh->bz_commit_transaction(); + Bugzilla->memcached->clear_config(); + return $changes; } sub check_remove { - my ($self, $params) = @_; - - # System groups cannot be deleted! - if (!$self->is_bug_group) { - ThrowUserError("system_group_not_deletable", { name => $self->name }); + my ($self, $params) = @_; + + # System groups cannot be deleted! + if (!$self->is_bug_group) { + ThrowUserError("system_group_not_deletable", {name => $self->name}); + } + + # Groups having a special role cannot be deleted. + my @special_groups; + foreach my $special_group (GROUP_PARAMS) { + if ($self->name eq Bugzilla->params->{$special_group}) { + push(@special_groups, $special_group); } + } + if (scalar(@special_groups)) { + ThrowUserError('group_has_special_role', + {name => $self->name, groups => \@special_groups}); + } - # Groups having a special role cannot be deleted. - my @special_groups; - foreach my $special_group (GROUP_PARAMS) { - if ($self->name eq Bugzilla->params->{$special_group}) { - push(@special_groups, $special_group); - } - } - if (scalar(@special_groups)) { - ThrowUserError('group_has_special_role', - { name => $self->name, - groups => \@special_groups }); - } + return if $params->{'test_only'}; - return if $params->{'test_only'}; + my $cantdelete = 0; - my $cantdelete = 0; + my $users = $self->members_non_inherited; + if (scalar(@$users) && !$params->{'remove_from_users'}) { + $cantdelete = 1; + } - my $users = $self->members_non_inherited; - if (scalar(@$users) && !$params->{'remove_from_users'}) { - $cantdelete = 1; - } + my $bugs = $self->bugs; + if (scalar(@$bugs) && !$params->{'remove_from_bugs'}) { + $cantdelete = 1; + } - my $bugs = $self->bugs; - if (scalar(@$bugs) && !$params->{'remove_from_bugs'}) { - $cantdelete = 1; - } - - my $products = $self->products; - if (scalar(@$products) && !$params->{'remove_from_products'}) { - $cantdelete = 1; - } + my $products = $self->products; + if (scalar(@$products) && !$params->{'remove_from_products'}) { + $cantdelete = 1; + } - my $flag_types = $self->flag_types; - if (scalar(@$flag_types) && !$params->{'remove_from_flags'}) { - $cantdelete = 1; - } + my $flag_types = $self->flag_types; + if (scalar(@$flag_types) && !$params->{'remove_from_flags'}) { + $cantdelete = 1; + } - ThrowUserError('group_cannot_delete', { group => $self }) if $cantdelete; + ThrowUserError('group_cannot_delete', {group => $self}) if $cantdelete; } sub remove_from_db { - my $self = shift; - my $dbh = Bugzilla->dbh; - $self->check_remove(@_); - $dbh->bz_start_transaction(); - Bugzilla::Hook::process('group_before_delete', { group => $self }); - $dbh->do('DELETE FROM whine_schedules - WHERE mailto_type = ? AND mailto = ?', - undef, MAILTO_GROUP, $self->id); - # All the other tables will be handled by foreign keys when we - # drop the main "groups" row. - $self->SUPER::remove_from_db(@_); - $dbh->bz_commit_transaction(); + my $self = shift; + my $dbh = Bugzilla->dbh; + $self->check_remove(@_); + $dbh->bz_start_transaction(); + Bugzilla::Hook::process('group_before_delete', {group => $self}); + $dbh->do( + 'DELETE FROM whine_schedules + WHERE mailto_type = ? AND mailto = ?', undef, MAILTO_GROUP, $self->id + ); + + # All the other tables will be handled by foreign keys when we + # drop the main "groups" row. + $self->SUPER::remove_from_db(@_); + $dbh->bz_commit_transaction(); } # Add missing entries in bug_group_map for bugs created while # a mandatory group was disabled and which is now enabled again. sub _enforce_mandatory { - my ($self) = @_; - my $dbh = Bugzilla->dbh; - my $gid = $self->id; + my ($self) = @_; + my $dbh = Bugzilla->dbh; + my $gid = $self->id; - my $bug_ids = - $dbh->selectcol_arrayref('SELECT bugs.bug_id + my $bug_ids = $dbh->selectcol_arrayref( + 'SELECT bugs.bug_id FROM bugs INNER JOIN group_control_map ON group_control_map.product_id = bugs.product_id @@ -304,156 +305,171 @@ sub _enforce_mandatory { AND bug_group_map.group_id = group_control_map.group_id WHERE group_control_map.group_id = ? AND group_control_map.membercontrol = ? - AND bug_group_map.group_id IS NULL', - undef, ($gid, CONTROLMAPMANDATORY)); - - my $sth = $dbh->prepare('INSERT INTO bug_group_map (bug_id, group_id) VALUES (?, ?)'); - foreach my $bug_id (@$bug_ids) { - $sth->execute($bug_id, $gid); - } + AND bug_group_map.group_id IS NULL', undef, + ($gid, CONTROLMAPMANDATORY) + ); + + my $sth + = $dbh->prepare('INSERT INTO bug_group_map (bug_id, group_id) VALUES (?, ?)'); + foreach my $bug_id (@$bug_ids) { + $sth->execute($bug_id, $gid); + } } sub is_active_bug_group { - my $self = shift; - return $self->is_active && $self->is_bug_group; + my $self = shift; + return $self->is_active && $self->is_bug_group; } sub _rederive_regexp { - my ($self) = @_; + my ($self) = @_; - my $dbh = Bugzilla->dbh; - my $sth = $dbh->prepare("SELECT userid, login_name, group_id + my $dbh = Bugzilla->dbh; + my $sth = $dbh->prepare( + "SELECT userid, login_name, group_id FROM profiles LEFT JOIN user_group_map ON user_group_map.user_id = profiles.userid AND group_id = ? AND grant_type = ? - AND isbless = 0"); - my $sthadd = $dbh->prepare("INSERT INTO user_group_map + AND isbless = 0" + ); + my $sthadd = $dbh->prepare( + "INSERT INTO user_group_map (user_id, group_id, grant_type, isbless) - VALUES (?, ?, ?, 0)"); - my $sthdel = $dbh->prepare("DELETE FROM user_group_map + VALUES (?, ?, ?, 0)" + ); + my $sthdel = $dbh->prepare( + "DELETE FROM user_group_map WHERE user_id = ? AND group_id = ? - AND grant_type = ? and isbless = 0"); - $sth->execute($self->id, GRANT_REGEXP); - my $regexp = $self->user_regexp; - while (my ($uid, $login, $present) = $sth->fetchrow_array) { - if ($regexp ne '' and $login =~ /$regexp/i) { - $sthadd->execute($uid, $self->id, GRANT_REGEXP) unless $present; - } else { - $sthdel->execute($uid, $self->id, GRANT_REGEXP) if $present; - } + AND grant_type = ? and isbless = 0" + ); + $sth->execute($self->id, GRANT_REGEXP); + my $regexp = $self->user_regexp; + + while (my ($uid, $login, $present) = $sth->fetchrow_array) { + if ($regexp ne '' and $login =~ /$regexp/i) { + $sthadd->execute($uid, $self->id, GRANT_REGEXP) unless $present; + } + else { + $sthdel->execute($uid, $self->id, GRANT_REGEXP) if $present; } + } } sub flatten_group_membership { - my ($self, @groups) = @_; - - my $dbh = Bugzilla->dbh; - my $sth; - my @groupidstocheck = @groups; - my %groupidschecked = (); - $sth = $dbh->prepare("SELECT member_id FROM group_group_map + my ($self, @groups) = @_; + + my $dbh = Bugzilla->dbh; + my $sth; + my @groupidstocheck = @groups; + my %groupidschecked = (); + $sth = $dbh->prepare( + "SELECT member_id FROM group_group_map WHERE grantor_id = ? - AND grant_type = " . GROUP_MEMBERSHIP); - while (my $node = shift @groupidstocheck) { - $sth->execute($node); - my $member; - while (($member) = $sth->fetchrow_array) { - if (!$groupidschecked{$member}) { - $groupidschecked{$member} = 1; - push @groupidstocheck, $member; - push @groups, $member unless grep $_ == $member, @groups; - } - } + AND grant_type = " . GROUP_MEMBERSHIP + ); + while (my $node = shift @groupidstocheck) { + $sth->execute($node); + my $member; + while (($member) = $sth->fetchrow_array) { + if (!$groupidschecked{$member}) { + $groupidschecked{$member} = 1; + push @groupidstocheck, $member; + push @groups, $member unless grep $_ == $member, @groups; + } } - return \@groups; + } + return \@groups; } - - ################################ ##### Module Subroutines ### ################################ sub create { - my $class = shift; - my ($params) = @_; - my $dbh = Bugzilla->dbh; - - my $silently = delete $params->{silently}; - my $use_in_all_products = delete $params->{use_in_all_products}; - if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE and !$silently) { - print get_text('install_group_create', { name => $params->{name} }), - "\n"; - } + my $class = shift; + my ($params) = @_; + my $dbh = Bugzilla->dbh; - $dbh->bz_start_transaction(); + my $silently = delete $params->{silently}; + my $use_in_all_products = delete $params->{use_in_all_products}; + if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE and !$silently) { + print get_text('install_group_create', {name => $params->{name}}), "\n"; + } - my $group = $class->SUPER::create(@_); + $dbh->bz_start_transaction(); - # Since we created a new group, give the "admin" group all privileges - # initially. - my $admin = new Bugzilla::Group({name => 'admin'}); - # This function is also used to create the "admin" group itself, - # so there's a chance it won't exist yet. - if ($admin) { - my $sth = $dbh->prepare('INSERT INTO group_group_map - (member_id, grantor_id, grant_type) - VALUES (?, ?, ?)'); - $sth->execute($admin->id, $group->id, GROUP_MEMBERSHIP); - $sth->execute($admin->id, $group->id, GROUP_BLESS); - $sth->execute($admin->id, $group->id, GROUP_VISIBLE); - } + my $group = $class->SUPER::create(@_); - # Permit all existing products to use the new group if requested. - if ($use_in_all_products) { - $dbh->do('INSERT INTO group_control_map + # Since we created a new group, give the "admin" group all privileges + # initially. + my $admin = new Bugzilla::Group({name => 'admin'}); + + # This function is also used to create the "admin" group itself, + # so there's a chance it won't exist yet. + if ($admin) { + my $sth = $dbh->prepare( + 'INSERT INTO group_group_map + (member_id, grantor_id, grant_type) + VALUES (?, ?, ?)' + ); + $sth->execute($admin->id, $group->id, GROUP_MEMBERSHIP); + $sth->execute($admin->id, $group->id, GROUP_BLESS); + $sth->execute($admin->id, $group->id, GROUP_VISIBLE); + } + + # Permit all existing products to use the new group if requested. + if ($use_in_all_products) { + $dbh->do( + 'INSERT INTO group_control_map (group_id, product_id, membercontrol, othercontrol) - SELECT ?, products.id, ?, ? FROM products', - undef, ($group->id, CONTROLMAPSHOWN, CONTROLMAPNA)); - } + SELECT ?, products.id, ?, ? FROM products', undef, + ($group->id, CONTROLMAPSHOWN, CONTROLMAPNA) + ); + } - $group->_rederive_regexp() if $group->user_regexp; + $group->_rederive_regexp() if $group->user_regexp; - Bugzilla::Hook::process('group_end_of_create', { group => $group }); - $dbh->bz_commit_transaction(); - return $group; + Bugzilla::Hook::process('group_end_of_create', {group => $group}); + $dbh->bz_commit_transaction(); + return $group; } sub ValidateGroupName { - my ($name, @users) = (@_); - my $dbh = Bugzilla->dbh; - my $query = "SELECT id FROM groups " . - "WHERE name = ?"; - if (Bugzilla->params->{'usevisibilitygroups'}) { - my @visible = (-1); - foreach my $user (@users) { - $user && push @visible, @{$user->visible_groups_direct}; - } - my $visible = join(', ', @visible); - $query .= " AND id IN($visible)"; + my ($name, @users) = (@_); + my $dbh = Bugzilla->dbh; + my $query = "SELECT id FROM groups " . "WHERE name = ?"; + if (Bugzilla->params->{'usevisibilitygroups'}) { + my @visible = (-1); + foreach my $user (@users) { + $user && push @visible, @{$user->visible_groups_direct}; } - my $sth = $dbh->prepare($query); - $sth->execute($name); - my ($ret) = $sth->fetchrow_array(); - return $ret; + my $visible = join(', ', @visible); + $query .= " AND id IN($visible)"; + } + my $sth = $dbh->prepare($query); + $sth->execute($name); + my ($ret) = $sth->fetchrow_array(); + return $ret; } sub check_no_disclose { - my ($class, $params) = @_; - my $action = delete $params->{action}; + my ($class, $params) = @_; + my $action = delete $params->{action}; - $action =~ /^(?:add|remove)$/ - or ThrowCodeError('bad_arg', { argument => $action, - function => "${class}::check_no_disclose" }); + $action =~ /^(?:add|remove)$/ + or ThrowCodeError('bad_arg', + {argument => $action, function => "${class}::check_no_disclose"}); - $params->{_error} = ($action eq 'add') ? 'group_restriction_not_allowed' - : 'group_invalid_removal'; + $params->{_error} + = ($action eq 'add') + ? 'group_restriction_not_allowed' + : 'group_invalid_removal'; - my $group = $class->check($params); - return $group; + my $group = $class->check($params); + return $group; } ############################### @@ -461,34 +477,36 @@ sub check_no_disclose { ############################### sub _check_name { - my ($invocant, $name) = @_; - $name = trim($name); - $name || ThrowUserError("empty_group_name"); - # If we're creating a Group or changing the name... - if (!ref($invocant) || lc($invocant->name) ne lc($name)) { - my $exists = new Bugzilla::Group({name => $name }); - ThrowUserError("group_exists", { name => $name }) if $exists; - } - return $name; + my ($invocant, $name) = @_; + $name = trim($name); + $name || ThrowUserError("empty_group_name"); + + # If we're creating a Group or changing the name... + if (!ref($invocant) || lc($invocant->name) ne lc($name)) { + my $exists = new Bugzilla::Group({name => $name}); + ThrowUserError("group_exists", {name => $name}) if $exists; + } + return $name; } sub _check_description { - my ($invocant, $desc) = @_; - $desc = trim($desc); - $desc || ThrowUserError("empty_group_description"); - return $desc; + my ($invocant, $desc) = @_; + $desc = trim($desc); + $desc || ThrowUserError("empty_group_description"); + return $desc; } sub _check_user_regexp { - my ($invocant, $regex) = @_; - $regex = trim($regex) || ''; - ThrowUserError("invalid_regexp") unless (eval {qr/$regex/}); - return $regex; + my ($invocant, $regex) = @_; + $regex = trim($regex) || ''; + ThrowUserError("invalid_regexp") unless (eval {qr/$regex/}); + return $regex; } sub _check_is_active { return $_[1] ? 1 : 0; } + sub _check_is_bug_group { - return $_[1] ? 1 : 0; + return $_[1] ? 1 : 0; } sub _check_icon_url { return $_[1] ? clean_text($_[1]) : undef; } diff --git a/Bugzilla/Hook.pm b/Bugzilla/Hook.pm index d8ae67463..3575d30c1 100644 --- a/Bugzilla/Hook.pm +++ b/Bugzilla/Hook.pm @@ -12,33 +12,33 @@ use strict; use warnings; sub process { - my ($name, $args) = @_; + my ($name, $args) = @_; - _entering($name); + _entering($name); - foreach my $extension (@{ Bugzilla->extensions }) { - if ($extension->can($name)) { - $extension->$name($args); - } + foreach my $extension (@{Bugzilla->extensions}) { + if ($extension->can($name)) { + $extension->$name($args); } + } - _leaving($name); + _leaving($name); } sub in { - my $hook_name = shift; - my $currently_in = Bugzilla->request_cache->{hook_stack}->[-1] || ''; - return $hook_name eq $currently_in ? 1 : 0; + my $hook_name = shift; + my $currently_in = Bugzilla->request_cache->{hook_stack}->[-1] || ''; + return $hook_name eq $currently_in ? 1 : 0; } sub _entering { - my ($hook_name) = @_; - my $hook_stack = Bugzilla->request_cache->{hook_stack} ||= []; - push(@$hook_stack, $hook_name); + my ($hook_name) = @_; + my $hook_stack = Bugzilla->request_cache->{hook_stack} ||= []; + push(@$hook_stack, $hook_name); } sub _leaving { - pop @{ Bugzilla->request_cache->{hook_stack} }; + pop @{Bugzilla->request_cache->{hook_stack}}; } 1; diff --git a/Bugzilla/Install.pm b/Bugzilla/Install.pm index 07bc9d6c3..cb05c44e9 100644 --- a/Bugzilla/Install.pm +++ b/Bugzilla/Install.pm @@ -7,7 +7,7 @@ package Bugzilla::Install; -# Functions in this this package can assume that the database +# Functions in this this package can assume that the database # has been set up, params are available, localconfig is # available, and any module can be used. # @@ -31,412 +31,424 @@ use Bugzilla::Util qw(get_text); use Bugzilla::Version; use constant STATUS_WORKFLOW => ( - [undef, 'UNCONFIRMED'], - [undef, 'CONFIRMED'], - [undef, 'IN_PROGRESS'], - ['UNCONFIRMED', 'CONFIRMED'], - ['UNCONFIRMED', 'IN_PROGRESS'], - ['UNCONFIRMED', 'RESOLVED'], - ['CONFIRMED', 'IN_PROGRESS'], - ['CONFIRMED', 'RESOLVED'], - ['IN_PROGRESS', 'CONFIRMED'], - ['IN_PROGRESS', 'RESOLVED'], - ['RESOLVED', 'UNCONFIRMED'], - ['RESOLVED', 'CONFIRMED'], - ['RESOLVED', 'VERIFIED'], - ['VERIFIED', 'UNCONFIRMED'], - ['VERIFIED', 'CONFIRMED'], + [undef, 'UNCONFIRMED'], + [undef, 'CONFIRMED'], + [undef, 'IN_PROGRESS'], + ['UNCONFIRMED', 'CONFIRMED'], + ['UNCONFIRMED', 'IN_PROGRESS'], + ['UNCONFIRMED', 'RESOLVED'], + ['CONFIRMED', 'IN_PROGRESS'], + ['CONFIRMED', 'RESOLVED'], + ['IN_PROGRESS', 'CONFIRMED'], + ['IN_PROGRESS', 'RESOLVED'], + ['RESOLVED', 'UNCONFIRMED'], + ['RESOLVED', 'CONFIRMED'], + ['RESOLVED', 'VERIFIED'], + ['VERIFIED', 'UNCONFIRMED'], + ['VERIFIED', 'CONFIRMED'], ); sub SETTINGS { - return { + return { # 2005-03-03 travis@sedsystems.ca -- Bug 41972 - display_quips => { options => ["on", "off"], default => "on" }, + display_quips => {options => ["on", "off"], default => "on"}, + # 2005-03-10 travis@sedsystems.ca -- Bug 199048 - comment_sort_order => { options => ["oldest_to_newest", "newest_to_oldest", - "newest_to_oldest_desc_first"], - default => "oldest_to_newest" }, + comment_sort_order => { + options => + ["oldest_to_newest", "newest_to_oldest", "newest_to_oldest_desc_first"], + default => "oldest_to_newest" + }, + # 2005-05-12 bugzilla@glob.com.au -- Bug 63536 - post_bug_submit_action => { options => ["next_bug", "same_bug", "nothing"], - default => "next_bug" }, + post_bug_submit_action => + {options => ["next_bug", "same_bug", "nothing"], default => "next_bug"}, + # 2005-06-29 wurblzap@gmail.com -- Bug 257767 - csv_colsepchar => { options => [',',';'], default => ',' }, + csv_colsepchar => {options => [',', ';'], default => ','}, + # 2005-10-26 wurblzap@gmail.com -- Bug 291459 - zoom_textareas => { options => ["on", "off"], default => "on" }, + zoom_textareas => {options => ["on", "off"], default => "on"}, + # 2006-05-01 olav@bkor.dhs.org -- Bug 7710 - state_addselfcc => { options => ['always', 'never', 'cc_unless_role'], - default => 'cc_unless_role' }, + state_addselfcc => { + options => ['always', 'never', 'cc_unless_role'], + default => 'cc_unless_role' + }, + # 2006-08-04 wurblzap@gmail.com -- Bug 322693 - skin => { subclass => 'Skin', default => 'Dusk' }, + skin => {subclass => 'Skin', default => 'Dusk'}, + # 2006-12-10 LpSolit@gmail.com -- Bug 297186 - lang => { subclass => 'Lang', - default => ${Bugzilla->languages}[0] }, + lang => {subclass => 'Lang', default => ${Bugzilla->languages}[0]}, + # 2007-07-02 altlist@gmail.com -- Bug 225731 - quote_replies => { options => ['quoted_reply', 'simple_reply', 'off'], - default => "quoted_reply" }, + quote_replies => { + options => ['quoted_reply', 'simple_reply', 'off'], + default => "quoted_reply" + }, + # 2009-02-01 mozilla@matt.mchenryfamily.org -- Bug 398473 - comment_box_position => { options => ['before_comments', 'after_comments'], - default => 'before_comments' }, + comment_box_position => { + options => ['before_comments', 'after_comments'], + default => 'before_comments' + }, + # 2008-08-27 LpSolit@gmail.com -- Bug 182238 - timezone => { subclass => 'Timezone', default => 'local' }, + timezone => {subclass => 'Timezone', default => 'local'}, + # 2011-02-07 dkl@mozilla.com -- Bug 580490 - quicksearch_fulltext => { options => ['on', 'off'], default => 'on' }, + quicksearch_fulltext => {options => ['on', 'off'], default => 'on'}, + # 2011-06-21 glob@mozilla.com -- Bug 589128 - email_format => { options => ['html', 'text_only'], - default => 'html' }, + email_format => {options => ['html', 'text_only'], default => 'html'}, + # 2011-10-11 glob@mozilla.com -- Bug 301656 - requestee_cc => { options => ['on', 'off'], default => 'on' }, + requestee_cc => {options => ['on', 'off'], default => 'on'}, + # 2012-04-30 glob@mozilla.com -- Bug 663747 - bugmail_new_prefix => { options => ['on', 'off'], default => 'on' }, + bugmail_new_prefix => {options => ['on', 'off'], default => 'on'}, + # 2013-07-26 joshi_sunil@in.com -- Bug 669535 - possible_duplicates => { options => ['on', 'off'], default => 'on' }, - } -}; + possible_duplicates => {options => ['on', 'off'], default => 'on'}, + }; +} use constant SYSTEM_GROUPS => ( - { - name => 'admin', - description => 'Administrators' - }, - { - name => 'tweakparams', - description => 'Can change Parameters' - }, - { - name => 'editusers', - description => 'Can edit or disable users' - }, - { - name => 'creategroups', - description => 'Can create and destroy groups' - }, - { - name => 'editclassifications', - description => 'Can create, destroy, and edit classifications' - }, - { - name => 'editcomponents', - description => 'Can create, destroy, and edit components' - }, - { - name => 'editkeywords', - description => 'Can create, destroy, and edit keywords' - }, - { - name => 'editbugs', - description => 'Can edit all bug fields', - userregexp => '.*' - }, - { - name => 'canconfirm', - description => 'Can confirm a bug or mark it a duplicate' - }, - { - name => 'bz_canusewhineatothers', - description => 'Can configure whine reports for other users', - }, - { - name => 'bz_canusewhines', - description => 'User can configure whine reports for self', - # inherited_by means that users in the groups listed below are - # automatically members of bz_canusewhines. - inherited_by => ['editbugs', 'bz_canusewhineatothers'], - }, - { - name => 'bz_sudoers', - description => 'Can perform actions as other users', - }, - { - name => 'bz_sudo_protect', - description => 'Can not be impersonated by other users', - inherited_by => ['bz_sudoers'], - }, - { - name => 'bz_quip_moderators', - description => 'Can moderate quips', - }, + {name => 'admin', description => 'Administrators'}, + {name => 'tweakparams', description => 'Can change Parameters'}, + {name => 'editusers', description => 'Can edit or disable users'}, + {name => 'creategroups', description => 'Can create and destroy groups'}, + { + name => 'editclassifications', + description => 'Can create, destroy, and edit classifications' + }, + { + name => 'editcomponents', + description => 'Can create, destroy, and edit components' + }, + { + name => 'editkeywords', + description => 'Can create, destroy, and edit keywords' + }, + { + name => 'editbugs', + description => 'Can edit all bug fields', + userregexp => '.*' + }, + { + name => 'canconfirm', + description => 'Can confirm a bug or mark it a duplicate' + }, + { + name => 'bz_canusewhineatothers', + description => 'Can configure whine reports for other users', + }, + { + name => 'bz_canusewhines', + description => 'User can configure whine reports for self', + + # inherited_by means that users in the groups listed below are + # automatically members of bz_canusewhines. + inherited_by => ['editbugs', 'bz_canusewhineatothers'], + }, + {name => 'bz_sudoers', description => 'Can perform actions as other users',}, + { + name => 'bz_sudo_protect', + description => 'Can not be impersonated by other users', + inherited_by => ['bz_sudoers'], + }, + {name => 'bz_quip_moderators', description => 'Can moderate quips',}, ); -use constant DEFAULT_CLASSIFICATION => { - name => 'Unclassified', - description => 'Not assigned to any classification' -}; +use constant DEFAULT_CLASSIFICATION => + {name => 'Unclassified', description => 'Not assigned to any classification'}; use constant DEFAULT_PRODUCT => { - name => 'TestProduct', - description => 'This is a test product.' - . ' This ought to be blown away and replaced with real stuff in a' - . ' finished installation of bugzilla.', - version => Bugzilla::Version::DEFAULT_VERSION, - classification => 'Unclassified', - defaultmilestone => DEFAULT_MILESTONE, + name => 'TestProduct', + description => 'This is a test product.' + . ' This ought to be blown away and replaced with real stuff in a' + . ' finished installation of bugzilla.', + version => Bugzilla::Version::DEFAULT_VERSION, + classification => 'Unclassified', + defaultmilestone => DEFAULT_MILESTONE, }; use constant DEFAULT_COMPONENT => { - name => 'TestComponent', - description => 'This is a test component in the test product database.' - . ' This ought to be blown away and replaced with real stuff in' - . ' a finished installation of Bugzilla.' + name => 'TestComponent', + description => 'This is a test component in the test product database.' + . ' This ought to be blown away and replaced with real stuff in' + . ' a finished installation of Bugzilla.' }; sub update_settings { - my $dbh = Bugzilla->dbh; - # If we're setting up settings for the first time, we want to be quieter. - my $any_settings = $dbh->selectrow_array( - 'SELECT 1 FROM setting ' . $dbh->sql_limit(1)); - if (!$any_settings) { - say get_text('install_setting_setup'); - } - - my %settings = %{SETTINGS()}; - foreach my $setting (keys %settings) { - add_setting($setting, - $settings{$setting}->{options}, - $settings{$setting}->{default}, - $settings{$setting}->{subclass}, undef, - !$any_settings); - } - - # Delete the obsolete 'per_bug_queries' user preference. Bug 616191. - $dbh->do('DELETE FROM setting WHERE name = ?', undef, 'per_bug_queries'); + my $dbh = Bugzilla->dbh; + + # If we're setting up settings for the first time, we want to be quieter. + my $any_settings + = $dbh->selectrow_array('SELECT 1 FROM setting ' . $dbh->sql_limit(1)); + if (!$any_settings) { + say get_text('install_setting_setup'); + } + + my %settings = %{SETTINGS()}; + foreach my $setting (keys %settings) { + add_setting( + $setting, + $settings{$setting}->{options}, + $settings{$setting}->{default}, + $settings{$setting}->{subclass}, + undef, !$any_settings + ); + } + + # Delete the obsolete 'per_bug_queries' user preference. Bug 616191. + $dbh->do('DELETE FROM setting WHERE name = ?', undef, 'per_bug_queries'); } sub update_system_groups { - my $dbh = Bugzilla->dbh; - - $dbh->bz_start_transaction(); - - # If there is no editbugs group, this is the first time we're - # adding groups. - my $editbugs_exists = new Bugzilla::Group({ name => 'editbugs' }); - if (!$editbugs_exists) { - say get_text('install_groups_setup'); - } - - # Create most of the system groups - foreach my $definition (SYSTEM_GROUPS) { - my $exists = new Bugzilla::Group({ name => $definition->{name} }); - if (!$exists) { - $definition->{isbuggroup} = 0; - $definition->{silently} = !$editbugs_exists; - my $inherited_by = delete $definition->{inherited_by}; - my $created = Bugzilla::Group->create($definition); - # Each group in inherited_by is automatically a member of this - # group. - if ($inherited_by) { - foreach my $name (@$inherited_by) { - my $member = Bugzilla::Group->check($name); - $dbh->do('INSERT INTO group_group_map (grantor_id, - member_id) VALUES (?,?)', - undef, $created->id, $member->id); - } - } + my $dbh = Bugzilla->dbh; + + $dbh->bz_start_transaction(); + + # If there is no editbugs group, this is the first time we're + # adding groups. + my $editbugs_exists = new Bugzilla::Group({name => 'editbugs'}); + if (!$editbugs_exists) { + say get_text('install_groups_setup'); + } + + # Create most of the system groups + foreach my $definition (SYSTEM_GROUPS) { + my $exists = new Bugzilla::Group({name => $definition->{name}}); + if (!$exists) { + $definition->{isbuggroup} = 0; + $definition->{silently} = !$editbugs_exists; + my $inherited_by = delete $definition->{inherited_by}; + my $created = Bugzilla::Group->create($definition); + + # Each group in inherited_by is automatically a member of this + # group. + if ($inherited_by) { + foreach my $name (@$inherited_by) { + my $member = Bugzilla::Group->check($name); + $dbh->do( + 'INSERT INTO group_group_map (grantor_id, + member_id) VALUES (?,?)', undef, $created->id, + $member->id + ); } + } } + } - $dbh->bz_commit_transaction(); + $dbh->bz_commit_transaction(); } sub create_default_classification { - my $dbh = Bugzilla->dbh; - - # Make the default Classification if it doesn't already exist. - if (!$dbh->selectrow_array('SELECT 1 FROM classifications')) { - print get_text('install_default_classification', - { name => DEFAULT_CLASSIFICATION->{name} }) . "\n"; - Bugzilla::Classification->create(DEFAULT_CLASSIFICATION); - } + my $dbh = Bugzilla->dbh; + + # Make the default Classification if it doesn't already exist. + if (!$dbh->selectrow_array('SELECT 1 FROM classifications')) { + print get_text('install_default_classification', + {name => DEFAULT_CLASSIFICATION->{name}}) + . "\n"; + Bugzilla::Classification->create(DEFAULT_CLASSIFICATION); + } } # This function should be called only after creating the admin user. sub create_default_product { - my $dbh = Bugzilla->dbh; - - # And same for the default product/component. - if (!$dbh->selectrow_array('SELECT 1 FROM products')) { - print get_text('install_default_product', - { name => DEFAULT_PRODUCT->{name} }) . "\n"; - - my $product = Bugzilla::Product->create(DEFAULT_PRODUCT); - - # Get the user who will be the owner of the Component. - # We pick the admin with the lowest id, which is probably the - # admin checksetup.pl just created. - my $admin_group = new Bugzilla::Group({name => 'admin'}); - my ($admin_id) = $dbh->selectrow_array( - 'SELECT user_id FROM user_group_map WHERE group_id = ? - ORDER BY user_id ' . $dbh->sql_limit(1), - undef, $admin_group->id); - my $admin = Bugzilla::User->new($admin_id); - - Bugzilla::Component->create({ - %{ DEFAULT_COMPONENT() }, product => $product, - initialowner => $admin->login }); - } + my $dbh = Bugzilla->dbh; + + # And same for the default product/component. + if (!$dbh->selectrow_array('SELECT 1 FROM products')) { + print get_text('install_default_product', {name => DEFAULT_PRODUCT->{name}}) + . "\n"; + + my $product = Bugzilla::Product->create(DEFAULT_PRODUCT); + + # Get the user who will be the owner of the Component. + # We pick the admin with the lowest id, which is probably the + # admin checksetup.pl just created. + my $admin_group = new Bugzilla::Group({name => 'admin'}); + my ($admin_id) = $dbh->selectrow_array( + 'SELECT user_id FROM user_group_map WHERE group_id = ? + ORDER BY user_id ' . $dbh->sql_limit(1), undef, $admin_group->id + ); + my $admin = Bugzilla::User->new($admin_id); + + Bugzilla::Component->create({ + %{DEFAULT_COMPONENT()}, product => $product, initialowner => $admin->login + }); + } } sub init_workflow { - my $dbh = Bugzilla->dbh; - my $has_workflow = $dbh->selectrow_array('SELECT 1 FROM status_workflow'); - return if $has_workflow; - - say get_text('install_workflow_init'); - - my %status_ids = @{ $dbh->selectcol_arrayref( - 'SELECT value, id FROM bug_status', {Columns=>[1,2]}) }; - - foreach my $pair (STATUS_WORKFLOW) { - my $old_id = $pair->[0] ? $status_ids{$pair->[0]} : undef; - my $new_id = $status_ids{$pair->[1]}; - $dbh->do('INSERT INTO status_workflow (old_status, new_status) - VALUES (?,?)', undef, $old_id, $new_id); - } + my $dbh = Bugzilla->dbh; + my $has_workflow = $dbh->selectrow_array('SELECT 1 FROM status_workflow'); + return if $has_workflow; + + say get_text('install_workflow_init'); + + my %status_ids = @{ + $dbh->selectcol_arrayref('SELECT value, id FROM bug_status', + {Columns => [1, 2]}) + }; + + foreach my $pair (STATUS_WORKFLOW) { + my $old_id = $pair->[0] ? $status_ids{$pair->[0]} : undef; + my $new_id = $status_ids{$pair->[1]}; + $dbh->do( + 'INSERT INTO status_workflow (old_status, new_status) + VALUES (?,?)', undef, $old_id, $new_id + ); + } } sub create_admin { - my ($params) = @_; - my $dbh = Bugzilla->dbh; - my $template = Bugzilla->template; - - my $admin_group = new Bugzilla::Group({ name => 'admin' }); - my $admin_inheritors = - Bugzilla::Group->flatten_group_membership($admin_group->id); - my $admin_group_ids = join(',', @$admin_inheritors); - - my ($admin_count) = $dbh->selectrow_array( - "SELECT COUNT(*) FROM user_group_map - WHERE group_id IN ($admin_group_ids)"); - - return if $admin_count; - - my %answer = %{Bugzilla->installation_answers}; - my $login = $answer{'ADMIN_EMAIL'}; - my $password = $answer{'ADMIN_PASSWORD'}; - my $full_name = $answer{'ADMIN_REALNAME'}; - - if (!$login || !$password || !$full_name) { - say "\n" . get_text('install_admin_setup') . "\n"; - } - - while (!$login) { - print get_text('install_admin_get_email') . ' '; - $login = ; - chomp $login; - eval { Bugzilla::User->check_login_name($login); }; - if ($@) { - say $@; - undef $login; - } - } - - while (!defined $full_name) { - print get_text('install_admin_get_name') . ' '; - $full_name = ; - chomp($full_name); - } - - if (!$password) { - $password = _prompt_for_password( - get_text('install_admin_get_password')); + my ($params) = @_; + my $dbh = Bugzilla->dbh; + my $template = Bugzilla->template; + + my $admin_group = new Bugzilla::Group({name => 'admin'}); + my $admin_inheritors + = Bugzilla::Group->flatten_group_membership($admin_group->id); + my $admin_group_ids = join(',', @$admin_inheritors); + + my ($admin_count) = $dbh->selectrow_array( + "SELECT COUNT(*) FROM user_group_map + WHERE group_id IN ($admin_group_ids)" + ); + + return if $admin_count; + + my %answer = %{Bugzilla->installation_answers}; + my $login = $answer{'ADMIN_EMAIL'}; + my $password = $answer{'ADMIN_PASSWORD'}; + my $full_name = $answer{'ADMIN_REALNAME'}; + + if (!$login || !$password || !$full_name) { + say "\n" . get_text('install_admin_setup') . "\n"; + } + + while (!$login) { + print get_text('install_admin_get_email') . ' '; + $login = ; + chomp $login; + eval { Bugzilla::User->check_login_name($login); }; + if ($@) { + say $@; + undef $login; } - - my $admin = Bugzilla::User->create({ login_name => $login, - realname => $full_name, - cryptpassword => $password }); - make_admin($admin); + } + + while (!defined $full_name) { + print get_text('install_admin_get_name') . ' '; + $full_name = ; + chomp($full_name); + } + + if (!$password) { + $password = _prompt_for_password(get_text('install_admin_get_password')); + } + + my $admin + = Bugzilla::User->create({ + login_name => $login, realname => $full_name, cryptpassword => $password + }); + make_admin($admin); } sub make_admin { - my ($user) = @_; - my $dbh = Bugzilla->dbh; - - $user = ref($user) ? $user - : new Bugzilla::User(login_to_id($user, THROW_ERROR)); - - my $group_insert = $dbh->prepare( - 'INSERT INTO user_group_map (user_id, group_id, isbless, grant_type) - VALUES (?, ?, ?, ?)'); - - # Admins get explicit membership and bless capability for the admin group - my $admin_group = new Bugzilla::Group({ name => 'admin' }); - # These are run in an eval so that we can ignore the error of somebody - # already being granted these things. - eval { - $group_insert->execute($user->id, $admin_group->id, 0, GRANT_DIRECT); - }; - eval { - $group_insert->execute($user->id, $admin_group->id, 1, GRANT_DIRECT); - }; - - # Admins should also have editusers directly, even though they'll usually - # inherit it. People could have changed their inheritance structure. - my $editusers = new Bugzilla::Group({ name => 'editusers' }); - eval { - $group_insert->execute($user->id, $editusers->id, 0, GRANT_DIRECT); - }; - - # If there is no maintainer set, make this user the maintainer. - if (!Bugzilla->params->{'maintainer'}) { - SetParam('maintainer', $user->email); - write_params(); - } - - # Make sure the new admin isn't disabled - if ($user->disabledtext) { - $user->set_disabledtext(''); - $user->update(); - } + my ($user) = @_; + my $dbh = Bugzilla->dbh; + + $user + = ref($user) ? $user : new Bugzilla::User(login_to_id($user, THROW_ERROR)); + + my $group_insert = $dbh->prepare( + 'INSERT INTO user_group_map (user_id, group_id, isbless, grant_type) + VALUES (?, ?, ?, ?)' + ); + + # Admins get explicit membership and bless capability for the admin group + my $admin_group = new Bugzilla::Group({name => 'admin'}); + + # These are run in an eval so that we can ignore the error of somebody + # already being granted these things. + eval { $group_insert->execute($user->id, $admin_group->id, 0, GRANT_DIRECT); }; + eval { $group_insert->execute($user->id, $admin_group->id, 1, GRANT_DIRECT); }; + + # Admins should also have editusers directly, even though they'll usually + # inherit it. People could have changed their inheritance structure. + my $editusers = new Bugzilla::Group({name => 'editusers'}); + eval { $group_insert->execute($user->id, $editusers->id, 0, GRANT_DIRECT); }; + + # If there is no maintainer set, make this user the maintainer. + if (!Bugzilla->params->{'maintainer'}) { + SetParam('maintainer', $user->email); + write_params(); + } + + # Make sure the new admin isn't disabled + if ($user->disabledtext) { + $user->set_disabledtext(''); + $user->update(); + } - if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) { - say "\n", get_text('install_admin_created', { user => $user }); - } + if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) { + say "\n", get_text('install_admin_created', {user => $user}); + } } sub _prompt_for_password { - my $prompt = shift; - - my $password; - while (!$password) { - # trap a few interrupts so we can fix the echo if we get aborted. - local $SIG{HUP} = \&_password_prompt_exit; - local $SIG{INT} = \&_password_prompt_exit; - local $SIG{QUIT} = \&_password_prompt_exit; - local $SIG{TERM} = \&_password_prompt_exit; - - system("stty","-echo") unless ON_WINDOWS; # disable input echoing - - print $prompt, ' '; - $password = ; - chomp $password; - print "\n", get_text('install_confirm_password'), ' '; - my $pass2 = ; - chomp $pass2; - eval { validate_password($password, $pass2); }; - if ($@) { - say "\n$@"; - undef $password; - } - system("stty","echo") unless ON_WINDOWS; + my $prompt = shift; + + my $password; + while (!$password) { + + # trap a few interrupts so we can fix the echo if we get aborted. + local $SIG{HUP} = \&_password_prompt_exit; + local $SIG{INT} = \&_password_prompt_exit; + local $SIG{QUIT} = \&_password_prompt_exit; + local $SIG{TERM} = \&_password_prompt_exit; + + system("stty", "-echo") unless ON_WINDOWS; # disable input echoing + + print $prompt, ' '; + $password = ; + chomp $password; + print "\n", get_text('install_confirm_password'), ' '; + my $pass2 = ; + chomp $pass2; + eval { validate_password($password, $pass2); }; + if ($@) { + say "\n$@"; + undef $password; } - return $password; + system("stty", "echo") unless ON_WINDOWS; + } + return $password; } # This is just in case we get interrupted while getting a password. sub _password_prompt_exit { - # re-enable input echoing - system("stty","echo") unless ON_WINDOWS; - exit 1; + + # re-enable input echoing + system("stty", "echo") unless ON_WINDOWS; + exit 1; } sub reset_password { - my $login = shift; - my $user = Bugzilla::User->check($login); - my $prompt = "\n" . get_text('install_reset_password', { user => $user }); - my $password = _prompt_for_password($prompt); - $user->set_password($password); - $user->update(); - say "\n", get_text('install_reset_password_done'); + my $login = shift; + my $user = Bugzilla::User->check($login); + my $prompt = "\n" . get_text('install_reset_password', {user => $user}); + my $password = _prompt_for_password($prompt); + $user->set_password($password); + $user->update(); + say "\n", get_text('install_reset_password_done'); } 1; diff --git a/Bugzilla/Install/CPAN.pm b/Bugzilla/Install/CPAN.pm index 094784e1a..4321cac61 100644 --- a/Bugzilla/Install/CPAN.pm +++ b/Bugzilla/Install/CPAN.pm @@ -13,11 +13,11 @@ use warnings; use parent qw(Exporter); our @EXPORT = qw( - BZ_LIB + BZ_LIB - check_cpan_requirements - set_cpan_config - install_module + check_cpan_requirements + set_cpan_config + install_module ); use Bugzilla::Constants; @@ -32,32 +32,28 @@ use File::Path qw(rmtree); # These are required for install-module.pl to be able to install # all modules properly. use constant REQUIREMENTS => ( - { - module => 'CPAN', - package => 'CPAN', - version => '1.81', - }, - { - # When Module::Build isn't installed, the YAML module allows - # CPAN to read META.yml to determine that Module::Build first - # needs to be installed to compile a module. - module => 'YAML', - package => 'YAML', - version => 0, - }, - { - # Many modules on CPAN are now built with Dist::Zilla, which - # unfortunately means they require this version of EU::MM to install. - module => 'ExtUtils::MakeMaker', - package => 'ExtUtils-MakeMaker', - version => '6.31', - }, + {module => 'CPAN', package => 'CPAN', version => '1.81',}, + { + # When Module::Build isn't installed, the YAML module allows + # CPAN to read META.yml to determine that Module::Build first + # needs to be installed to compile a module. + module => 'YAML', + package => 'YAML', + version => 0, + }, + { + # Many modules on CPAN are now built with Dist::Zilla, which + # unfortunately means they require this version of EU::MM to install. + module => 'ExtUtils::MakeMaker', + package => 'ExtUtils-MakeMaker', + version => '6.31', + }, ); # We need the absolute path of ext_libpath, because CPAN chdirs around # and so we can't use a relative directory. # -# We need it often enough (and at compile time, in install-module.pl) so +# We need it often enough (and at compile time, in install-module.pl) so # we make it a constant. use constant BZ_LIB => abs_path(bz_locations()->{ext_libpath}); @@ -66,217 +62,229 @@ use constant BZ_LIB => abs_path(bz_locations()->{ext_libpath}); # defaults here for most of the required parameters we know about, in case # any of them aren't set. The rest are handled by set_cpan_defaults(). use constant CPAN_DEFAULTS => { - auto_commit => 0, - # We always force builds, so there's no reason to cache them. - build_cache => 0, - build_requires_install_policy => 'yes', - cache_metadata => 1, - colorize_output => 1, - colorize_print => 'bold', - index_expire => 1, - scan_cache => 'atstart', - - inhibit_startup_message => 1, - - bzip2 => bin_loc('bzip2'), - curl => bin_loc('curl'), - gzip => bin_loc('gzip'), - links => bin_loc('links'), - lynx => bin_loc('lynx'), - make => bin_loc('make'), - pager => bin_loc('less'), - tar => bin_loc('tar'), - unzip => bin_loc('unzip'), - wget => bin_loc('wget'), - - urllist => ['http://www.cpan.org/'], + auto_commit => 0, + + # We always force builds, so there's no reason to cache them. + build_cache => 0, + build_requires_install_policy => 'yes', + cache_metadata => 1, + colorize_output => 1, + colorize_print => 'bold', + index_expire => 1, + scan_cache => 'atstart', + + inhibit_startup_message => 1, + + bzip2 => bin_loc('bzip2'), + curl => bin_loc('curl'), + gzip => bin_loc('gzip'), + links => bin_loc('links'), + lynx => bin_loc('lynx'), + make => bin_loc('make'), + pager => bin_loc('less'), + tar => bin_loc('tar'), + unzip => bin_loc('unzip'), + wget => bin_loc('wget'), + + urllist => ['http://www.cpan.org/'], }; sub check_cpan_requirements { - my ($original_dir, $original_args) = @_; + my ($original_dir, $original_args) = @_; - _require_compiler(); + _require_compiler(); - my @install; - foreach my $module (REQUIREMENTS) { - my $installed = have_vers($module, 1); - push(@install, $module) if !$installed; - } + my @install; + foreach my $module (REQUIREMENTS) { + my $installed = have_vers($module, 1); + push(@install, $module) if !$installed; + } - return if !@install; + return if !@install; - my $restart_required; - foreach my $module (@install) { - $restart_required = 1 if $module->{module} eq 'CPAN'; - install_module($module->{module}, 1); - } + my $restart_required; + foreach my $module (@install) { + $restart_required = 1 if $module->{module} eq 'CPAN'; + install_module($module->{module}, 1); + } - if ($restart_required) { - chdir $original_dir; - exec($^X, $0, @$original_args); - } + if ($restart_required) { + chdir $original_dir; + exec($^X, $0, @$original_args); + } } sub _require_compiler { - my @errors; + my @errors; - my $cc_name = $Config{cc}; - my $cc_exists = bin_loc($cc_name); + my $cc_name = $Config{cc}; + my $cc_exists = bin_loc($cc_name); - if (!$cc_exists) { - push(@errors, install_string('install_no_compiler')); - } + if (!$cc_exists) { + push(@errors, install_string('install_no_compiler')); + } - my $make_name = $CPAN::Config->{make}; - my $make_exists = bin_loc($make_name); + my $make_name = $CPAN::Config->{make}; + my $make_exists = bin_loc($make_name); - if (!$make_exists) { - push(@errors, install_string('install_no_make')); - } + if (!$make_exists) { + push(@errors, install_string('install_no_make')); + } - die @errors if @errors; + die @errors if @errors; } sub install_module { - my ($name, $test) = @_; - my $bzlib = BZ_LIB; - - # Make Module::AutoInstall install all dependencies and never prompt. - local $ENV{PERL_AUTOINSTALL} = '--alldeps'; - # This makes Net::SSLeay not prompt the user, if it gets installed. - # It also makes any other MakeMaker prompts accept their defaults. - local $ENV{PERL_MM_USE_DEFAULT} = 1; - - # Certain modules require special stuff in order to not prompt us. - my $original_makepl = $CPAN::Config->{makepl_arg}; - # This one's a regex in case we're doing Template::Plugin::GD and it - # pulls in Template-Toolkit as a dependency. - if ($name =~ /^Template/) { - $CPAN::Config->{makepl_arg} .= " TT_ACCEPT=y TT_EXTRAS=n"; - } - elsif ($name eq 'XML::Twig') { - $CPAN::Config->{makepl_arg} = "-n $original_makepl"; - } - elsif ($name eq 'SOAP::Lite') { - $CPAN::Config->{makepl_arg} .= " --noprompt"; - } - - my $module = CPAN::Shell->expand('Module', $name); - if (!$module) { - die install_string('no_such_module', { module => $name }) . "\n"; - } - - print install_string('install_module', - { module => $name, version => $module->cpan_version }) . "\n"; - - if ($test) { - CPAN::Shell->force('install', $name); - } - else { - CPAN::Shell->notest('install', $name); - } - - # If it installed any binaries in the Bugzilla directory, delete them. - if (-d "$bzlib/bin") { - File::Path::rmtree("$bzlib/bin"); - } - - $CPAN::Config->{makepl_arg} = $original_makepl; + my ($name, $test) = @_; + my $bzlib = BZ_LIB; + + # Make Module::AutoInstall install all dependencies and never prompt. + local $ENV{PERL_AUTOINSTALL} = '--alldeps'; + + # This makes Net::SSLeay not prompt the user, if it gets installed. + # It also makes any other MakeMaker prompts accept their defaults. + local $ENV{PERL_MM_USE_DEFAULT} = 1; + + # Certain modules require special stuff in order to not prompt us. + my $original_makepl = $CPAN::Config->{makepl_arg}; + + # This one's a regex in case we're doing Template::Plugin::GD and it + # pulls in Template-Toolkit as a dependency. + if ($name =~ /^Template/) { + $CPAN::Config->{makepl_arg} .= " TT_ACCEPT=y TT_EXTRAS=n"; + } + elsif ($name eq 'XML::Twig') { + $CPAN::Config->{makepl_arg} = "-n $original_makepl"; + } + elsif ($name eq 'SOAP::Lite') { + $CPAN::Config->{makepl_arg} .= " --noprompt"; + } + + my $module = CPAN::Shell->expand('Module', $name); + if (!$module) { + die install_string('no_such_module', {module => $name}) . "\n"; + } + + print install_string('install_module', + {module => $name, version => $module->cpan_version}) + . "\n"; + + if ($test) { + CPAN::Shell->force('install', $name); + } + else { + CPAN::Shell->notest('install', $name); + } + + # If it installed any binaries in the Bugzilla directory, delete them. + if (-d "$bzlib/bin") { + File::Path::rmtree("$bzlib/bin"); + } + + $CPAN::Config->{makepl_arg} = $original_makepl; } sub set_cpan_config { - my $do_global = shift; - my $bzlib = BZ_LIB; - - # We set defaults before we do anything, otherwise CPAN will - # start asking us questions as soon as we load its configuration. - eval { require CPAN::Config; }; - _set_cpan_defaults(); - - # 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'); - eval { CPAN->ignore_this_error_message_from_bugzilla; }; - undef $@; - close(STDOUT); - open(STDOUT, '>&', $saveout); - - my $dir = $CPAN::Config->{cpan_home}; - if (!defined $dir || !-w $dir) { - # If we can't use the standard CPAN build dir, we try to make one. - $dir = "$ENV{HOME}/.cpan"; - mkdir $dir; - - # If we can't make one, we finally try to use the Bugzilla directory. - if (!-w $dir) { - print STDERR install_string('cpan_bugzilla_home'), "\n"; - $dir = "$bzlib/.cpan"; - } - } - $CPAN::Config->{cpan_home} = $dir; - $CPAN::Config->{build_dir} = "$dir/build"; - # We always force builds, so there's no reason to cache them. - $CPAN::Config->{keep_source_where} = "$dir/source"; - # This is set both here and in defaults so that it's always true. - $CPAN::Config->{inhibit_startup_message} = 1; - # Automatically install dependencies. - $CPAN::Config->{prerequisites_policy} = 'follow'; - - # Unless specified, we install the modules into the Bugzilla directory. - if (!$do_global) { - require Config; - - $CPAN::Config->{makepl_arg} .= " LIB=\"$bzlib\"" - . " INSTALLMAN1DIR=\"$bzlib/man/man1\"" - . " INSTALLMAN3DIR=\"$bzlib/man/man3\"" - # The bindirs are here because otherwise we'll try to write to - # the system binary dirs, and that will cause CPAN to die. - . " INSTALLBIN=\"$bzlib/bin\"" - . " INSTALLSCRIPT=\"$bzlib/bin\"" - # INSTALLDIRS=perl is set because that makes sure that MakeMaker - # always uses the directories we've specified here. - . " INSTALLDIRS=perl"; - $CPAN::Config->{mbuild_arg} = " --install_base \"$bzlib\"" - . " --install_path lib=\"$bzlib\"" - . " --install_path arch=\"$bzlib/$Config::Config{archname}\""; - $CPAN::Config->{mbuild_install_arg} = $CPAN::Config->{mbuild_arg}; - - # When we're not root, sometimes newer versions of CPAN will - # try to read/modify things that belong to root, unless we set - # certain config variables. - $CPAN::Config->{histfile} = "$dir/histfile"; - $CPAN::Config->{use_sqlite} = 0; - $CPAN::Config->{prefs_dir} = "$dir/prefs"; - - # Unless we actually set PERL5LIB, some modules can't install - # themselves, like DBD::mysql, DBD::Pg, and XML::Twig. - my $current_lib = $ENV{PERL5LIB} ? $ENV{PERL5LIB} . ':' : ''; - $ENV{PERL5LIB} = $current_lib . $bzlib; + my $do_global = shift; + my $bzlib = BZ_LIB; + + # We set defaults before we do anything, otherwise CPAN will + # start asking us questions as soon as we load its configuration. + eval { require CPAN::Config; }; + _set_cpan_defaults(); + + # 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'); + eval { CPAN->ignore_this_error_message_from_bugzilla; }; + undef $@; + close(STDOUT); + open(STDOUT, '>&', $saveout); + + my $dir = $CPAN::Config->{cpan_home}; + if (!defined $dir || !-w $dir) { + + # If we can't use the standard CPAN build dir, we try to make one. + $dir = "$ENV{HOME}/.cpan"; + mkdir $dir; + + # If we can't make one, we finally try to use the Bugzilla directory. + if (!-w $dir) { + print STDERR install_string('cpan_bugzilla_home'), "\n"; + $dir = "$bzlib/.cpan"; } + } + $CPAN::Config->{cpan_home} = $dir; + $CPAN::Config->{build_dir} = "$dir/build"; + + # We always force builds, so there's no reason to cache them. + $CPAN::Config->{keep_source_where} = "$dir/source"; + + # This is set both here and in defaults so that it's always true. + $CPAN::Config->{inhibit_startup_message} = 1; + + # Automatically install dependencies. + $CPAN::Config->{prerequisites_policy} = 'follow'; + + # Unless specified, we install the modules into the Bugzilla directory. + if (!$do_global) { + require Config; + + $CPAN::Config->{makepl_arg} + .= " LIB=\"$bzlib\"" + . " INSTALLMAN1DIR=\"$bzlib/man/man1\"" + . " INSTALLMAN3DIR=\"$bzlib/man/man3\"" + + # The bindirs are here because otherwise we'll try to write to + # the system binary dirs, and that will cause CPAN to die. + . " INSTALLBIN=\"$bzlib/bin\"" . " INSTALLSCRIPT=\"$bzlib/bin\"" + + # INSTALLDIRS=perl is set because that makes sure that MakeMaker + # always uses the directories we've specified here. + . " INSTALLDIRS=perl"; + $CPAN::Config->{mbuild_arg} + = " --install_base \"$bzlib\"" + . " --install_path lib=\"$bzlib\"" + . " --install_path arch=\"$bzlib/$Config::Config{archname}\""; + $CPAN::Config->{mbuild_install_arg} = $CPAN::Config->{mbuild_arg}; + + # When we're not root, sometimes newer versions of CPAN will + # try to read/modify things that belong to root, unless we set + # certain config variables. + $CPAN::Config->{histfile} = "$dir/histfile"; + $CPAN::Config->{use_sqlite} = 0; + $CPAN::Config->{prefs_dir} = "$dir/prefs"; + + # Unless we actually set PERL5LIB, some modules can't install + # themselves, like DBD::mysql, DBD::Pg, and XML::Twig. + my $current_lib = $ENV{PERL5LIB} ? $ENV{PERL5LIB} . ':' : ''; + $ENV{PERL5LIB} = $current_lib . $bzlib; + } } sub _set_cpan_defaults { - # If CPAN hasn't been configured, we try to use some reasonable defaults. - foreach my $key (keys %{CPAN_DEFAULTS()}) { - $CPAN::Config->{$key} = CPAN_DEFAULTS->{$key} - if !defined $CPAN::Config->{$key}; - } - my @missing; - # In newer CPANs, this is in HandleConfig. In older CPANs, it's in - # Config. - if (eval { require CPAN::HandleConfig }) { - @missing = CPAN::HandleConfig->missing_config_data; - } - else { - @missing = CPAN::Config->missing_config_data; - } - - foreach my $key (@missing) { - $CPAN::Config->{$key} = ''; - } + # If CPAN hasn't been configured, we try to use some reasonable defaults. + foreach my $key (keys %{CPAN_DEFAULTS()}) { + $CPAN::Config->{$key} = CPAN_DEFAULTS->{$key} if !defined $CPAN::Config->{$key}; + } + + my @missing; + + # In newer CPANs, this is in HandleConfig. In older CPANs, it's in + # Config. + if (eval { require CPAN::HandleConfig }) { + @missing = CPAN::HandleConfig->missing_config_data; + } + else { + @missing = CPAN::Config->missing_config_data; + } + + foreach my $key (@missing) { + $CPAN::Config->{$key} = ''; + } } 1; diff --git a/Bugzilla/Install/DB.pm b/Bugzilla/Install/DB.pm index ed2539251..248e72852 100644 --- a/Bugzilla/Install/DB.pm +++ b/Bugzilla/Install/DB.pm @@ -8,7 +8,7 @@ package Bugzilla::Install::DB; # NOTE: This package may "use" any modules that it likes, -# localconfig is available, and params are up to date. +# localconfig is available, and params are up to date. use 5.10.1; use strict; @@ -34,99 +34,104 @@ use URI::QueryParam; # NOTE: This is NOT the function for general table updates. See # update_table_definitions for that. This is only for the fielddefs table. sub update_fielddefs_definition { - my $dbh = Bugzilla->dbh; - - # 2005-02-21 - LpSolit@gmail.com - Bug 279910 - # qacontact_accessible and assignee_accessible field names no longer exist - # in the 'bugs' table. Their corresponding entries in the 'bugs_activity' - # table should therefore be marked as obsolete, meaning that they cannot - # be used anymore when querying the database - they are not deleted in - # order to keep track of these fields in the activity table. - if (!$dbh->bz_column_info('fielddefs', 'obsolete')) { - $dbh->bz_add_column('fielddefs', 'obsolete', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); - print "Marking qacontact_accessible and assignee_accessible as", - " obsolete fields...\n"; - $dbh->do("UPDATE fielddefs SET obsolete = 1 + my $dbh = Bugzilla->dbh; + + # 2005-02-21 - LpSolit@gmail.com - Bug 279910 + # qacontact_accessible and assignee_accessible field names no longer exist + # in the 'bugs' table. Their corresponding entries in the 'bugs_activity' + # table should therefore be marked as obsolete, meaning that they cannot + # be used anymore when querying the database - they are not deleted in + # order to keep track of these fields in the activity table. + if (!$dbh->bz_column_info('fielddefs', 'obsolete')) { + $dbh->bz_add_column('fielddefs', 'obsolete', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); + print "Marking qacontact_accessible and assignee_accessible as", + " obsolete fields...\n"; + $dbh->do( + "UPDATE fielddefs SET obsolete = 1 WHERE name = 'qacontact_accessible' - OR name = 'assignee_accessible'"); - } - - # 2005-08-10 Myk Melez bug 287325 - # Record each field's type and whether or not it's a custom field, - # in fielddefs. - $dbh->bz_add_column('fielddefs', 'type', - {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0}); - $dbh->bz_add_column('fielddefs', 'custom', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); - - $dbh->bz_add_column('fielddefs', 'enter_bug', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); - - # Change the name of the fieldid column to id, so that fielddefs - # can use Bugzilla::Object easily. We have to do this up here, because - # otherwise adding these field definitions will fail. - $dbh->bz_rename_column('fielddefs', 'fieldid', 'id'); - - # If the largest fielddefs sortkey is less than 100, then - # we're using the old sorting system, and we should convert - # it to the new one before adding any new definitions. - if (!$dbh->selectrow_arrayref( - 'SELECT COUNT(id) FROM fielddefs WHERE sortkey >= 100')) - { - print "Updating the sortkeys for the fielddefs table...\n"; - my $field_ids = $dbh->selectcol_arrayref( - 'SELECT id FROM fielddefs ORDER BY sortkey'); - my $sortkey = 100; - foreach my $field_id (@$field_ids) { - $dbh->do('UPDATE fielddefs SET sortkey = ? WHERE id = ?', - undef, $sortkey, $field_id); - $sortkey += 100; - } + OR name = 'assignee_accessible'" + ); + } + + # 2005-08-10 Myk Melez bug 287325 + # Record each field's type and whether or not it's a custom field, + # in fielddefs. + $dbh->bz_add_column('fielddefs', 'type', + {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0}); + $dbh->bz_add_column('fielddefs', 'custom', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); + + $dbh->bz_add_column('fielddefs', 'enter_bug', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); + + # Change the name of the fieldid column to id, so that fielddefs + # can use Bugzilla::Object easily. We have to do this up here, because + # otherwise adding these field definitions will fail. + $dbh->bz_rename_column('fielddefs', 'fieldid', 'id'); + + # If the largest fielddefs sortkey is less than 100, then + # we're using the old sorting system, and we should convert + # it to the new one before adding any new definitions. + if (!$dbh->selectrow_arrayref( + 'SELECT COUNT(id) FROM fielddefs WHERE sortkey >= 100')) + { + print "Updating the sortkeys for the fielddefs table...\n"; + my $field_ids + = $dbh->selectcol_arrayref('SELECT id FROM fielddefs ORDER BY sortkey'); + my $sortkey = 100; + foreach my $field_id (@$field_ids) { + $dbh->do('UPDATE fielddefs SET sortkey = ? WHERE id = ?', + undef, $sortkey, $field_id); + $sortkey += 100; } + } - $dbh->bz_add_column('fielddefs', 'visibility_field_id', {TYPE => 'INT3'}); - $dbh->bz_add_column('fielddefs', 'value_field_id', {TYPE => 'INT3'}); - $dbh->bz_add_index('fielddefs', 'fielddefs_value_field_id_idx', - ['value_field_id']); + $dbh->bz_add_column('fielddefs', 'visibility_field_id', {TYPE => 'INT3'}); + $dbh->bz_add_column('fielddefs', 'value_field_id', {TYPE => 'INT3'}); + $dbh->bz_add_index('fielddefs', 'fielddefs_value_field_id_idx', + ['value_field_id']); - # Bug 344878 - if (!$dbh->bz_column_info('fielddefs', 'buglist')) { - $dbh->bz_add_column('fielddefs', 'buglist', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); - # Set non-multiselect custom fields as valid buglist fields - # Note that default fields will be handled in Field.pm - $dbh->do('UPDATE fielddefs SET buglist = 1 WHERE custom = 1 AND type != ' . FIELD_TYPE_MULTI_SELECT); - } + # Bug 344878 + if (!$dbh->bz_column_info('fielddefs', 'buglist')) { + $dbh->bz_add_column('fielddefs', 'buglist', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); - #2008-08-26 elliotte_martin@yahoo.com - Bug 251556 - $dbh->bz_add_column('fielddefs', 'reverse_desc', {TYPE => 'TINYTEXT'}); + # Set non-multiselect custom fields as valid buglist fields + # Note that default fields will be handled in Field.pm + $dbh->do('UPDATE fielddefs SET buglist = 1 WHERE custom = 1 AND type != ' + . FIELD_TYPE_MULTI_SELECT); + } - $dbh->do('UPDATE fielddefs SET buglist = 1 - WHERE custom = 1 AND type = ' . FIELD_TYPE_MULTI_SELECT); + #2008-08-26 elliotte_martin@yahoo.com - Bug 251556 + $dbh->bz_add_column('fielddefs', 'reverse_desc', {TYPE => 'TINYTEXT'}); - $dbh->bz_add_column('fielddefs', 'is_mandatory', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); - $dbh->bz_add_index('fielddefs', 'fielddefs_is_mandatory_idx', - ['is_mandatory']); + $dbh->do( + 'UPDATE fielddefs SET buglist = 1 + WHERE custom = 1 AND type = ' . FIELD_TYPE_MULTI_SELECT + ); - # 2010-04-05 dkl@redhat.com - Bug 479400 - _migrate_field_visibility_value(); + $dbh->bz_add_column('fielddefs', 'is_mandatory', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); + $dbh->bz_add_index('fielddefs', 'fielddefs_is_mandatory_idx', ['is_mandatory']); - $dbh->bz_add_column('fielddefs', 'is_numeric', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); - $dbh->do('UPDATE fielddefs SET is_numeric = 1 WHERE type = ' - . FIELD_TYPE_BUG_ID); + # 2010-04-05 dkl@redhat.com - Bug 479400 + _migrate_field_visibility_value(); - # 2012-04-12 aliustek@gmail.com - Bug 728138 - $dbh->bz_add_column('fielddefs', 'long_desc', - {TYPE => 'varchar(255)', NOTNULL => 1, DEFAULT => "''"}, ''); + $dbh->bz_add_column('fielddefs', 'is_numeric', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); + $dbh->do( + 'UPDATE fielddefs SET is_numeric = 1 WHERE type = ' . FIELD_TYPE_BUG_ID); - Bugzilla::Hook::process('install_update_db_fielddefs'); + # 2012-04-12 aliustek@gmail.com - Bug 728138 + $dbh->bz_add_column('fielddefs', 'long_desc', + {TYPE => 'varchar(255)', NOTNULL => 1, DEFAULT => "''"}, ''); - # Remember, this is not the function for adding general table changes. - # That is below. Add new changes to the fielddefs table above this - # comment. + Bugzilla::Hook::process('install_update_db_fielddefs'); + + # Remember, this is not the function for adding general table changes. + # That is below. Add new changes to the fielddefs table above this + # comment. } # Small changes can be put directly into this function. @@ -136,14 +141,14 @@ sub update_fielddefs_definition { # # This function runs in historical order--from upgrades that older # installations need, to upgrades that newer installations need. -# The order of items inside this function should only be changed if +# The order of items inside this function should only be changed if # absolutely necessary. # # The subroutines should have long, descriptive names, so that you # can easily see what is being done, just by reading this function. # # This function is mostly self-documenting. If you're curious about -# what each of the added/removed columns does, you should see the schema +# what each of the added/removed columns does, you should see the schema # docs at: # http://www.ravenbrook.com/project/p4dti/tool/cgi/bugzilla-schema/ # @@ -152,1062 +157,1091 @@ sub update_fielddefs_definition { # the purpose of a column. # sub update_table_definitions { - my $old_params = shift; - my $dbh = Bugzilla->dbh; - _update_pre_checksetup_bugzillas(); - - $dbh->bz_add_column('attachments', 'submitter_id', - {TYPE => 'INT3', NOTNULL => 1}, 0); - - $dbh->bz_rename_column('bugs_activity', 'when', 'bug_when'); - - _add_bug_vote_cache(); - _update_product_name_definition(); - - $dbh->bz_add_column('profiles', 'disabledtext', - {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, ''); - - _populate_longdescs(); - _update_bugs_activity_field_to_fieldid(); - - if (!$dbh->bz_column_info('bugs', 'lastdiffed')) { - $dbh->bz_add_column('bugs', 'lastdiffed', {TYPE =>'DATETIME'}); - $dbh->do('UPDATE bugs SET lastdiffed = NOW()'); - } - - _add_unique_login_name_index_to_profiles(); - - $dbh->bz_add_column('profiles', 'mybugslink', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); - - _update_component_user_fields_to_ids(); - - $dbh->bz_add_column('bugs', 'everconfirmed', - {TYPE => 'BOOLEAN', NOTNULL => 1}, 1); + my $old_params = shift; + my $dbh = Bugzilla->dbh; + _update_pre_checksetup_bugzillas(); - _populate_milestones_table(); + $dbh->bz_add_column('attachments', 'submitter_id', + {TYPE => 'INT3', NOTNULL => 1}, 0); - _add_products_defaultmilestone(); + $dbh->bz_rename_column('bugs_activity', 'when', 'bug_when'); - # 2000-03-24 Added unique indexes into the cc and keyword tables. This - # prevents certain database inconsistencies, and, moreover, is required for - # new generalized list code to work. - if (!$dbh->bz_index_info('cc', 'cc_bug_id_idx') - || !$dbh->bz_index_info('cc', 'cc_bug_id_idx')->{TYPE}) - { - $dbh->bz_drop_index('cc', 'cc_bug_id_idx'); - $dbh->bz_add_index('cc', 'cc_bug_id_idx', - {TYPE => 'UNIQUE', FIELDS => [qw(bug_id who)]}); - } - if (!$dbh->bz_index_info('keywords', 'keywords_bug_id_idx') - || !$dbh->bz_index_info('keywords', 'keywords_bug_id_idx')->{TYPE}) - { - $dbh->bz_drop_index('keywords', 'keywords_bug_id_idx'); - $dbh->bz_add_index('keywords', 'keywords_bug_id_idx', - {TYPE => 'UNIQUE', FIELDS => [qw(bug_id keywordid)]}); - } + _add_bug_vote_cache(); + _update_product_name_definition(); - _copy_from_comments_to_longdescs(); - _populate_duplicates_table(); + $dbh->bz_add_column('profiles', 'disabledtext', + {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, ''); - if (!$dbh->bz_column_info('email_setting', 'user_id')) { - $dbh->bz_add_column('profiles', 'emailflags', {TYPE => 'MEDIUMTEXT'}); - } - - $dbh->bz_add_column('groups', 'isactive', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); + _populate_longdescs(); + _update_bugs_activity_field_to_fieldid(); - $dbh->bz_add_column('attachments', 'isobsolete', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); + if (!$dbh->bz_column_info('bugs', 'lastdiffed')) { + $dbh->bz_add_column('bugs', 'lastdiffed', {TYPE => 'DATETIME'}); + $dbh->do('UPDATE bugs SET lastdiffed = NOW()'); + } - $dbh->bz_drop_column("profiles", "emailnotification"); - $dbh->bz_drop_column("profiles", "newemailtech"); + _add_unique_login_name_index_to_profiles(); - # 2003-11-19; chicks@chicks.net; bug 225973: fix field size to accommodate - # wider algorithms such as Blowfish. Note that this needs to be run - # before recrypting passwords in the following block. - $dbh->bz_alter_column('profiles', 'cryptpassword', - {TYPE => 'varchar(128)'}); + $dbh->bz_add_column('profiles', 'mybugslink', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); - _recrypt_plaintext_passwords(); + _update_component_user_fields_to_ids(); - # 2001-06-15 kiko@async.com.br - Change bug:version size to avoid - # truncates re http://bugzilla.mozilla.org/show_bug.cgi?id=9352 - $dbh->bz_alter_column('bugs', 'version', - {TYPE => 'varchar(64)', NOTNULL => 1}); + $dbh->bz_add_column('bugs', 'everconfirmed', {TYPE => 'BOOLEAN', NOTNULL => 1}, + 1); - _update_bugs_activity_to_only_record_changes(); + _populate_milestones_table(); - # bug 90933: Make disabledtext NOT NULL - if (!$dbh->bz_column_info('profiles', 'disabledtext')->{NOTNULL}) { - $dbh->bz_alter_column("profiles", "disabledtext", - {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, ''); - } + _add_products_defaultmilestone(); - $dbh->bz_add_column("bugs", "reporter_accessible", - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); - $dbh->bz_add_column("bugs", "cclist_accessible", - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); + # 2000-03-24 Added unique indexes into the cc and keyword tables. This + # prevents certain database inconsistencies, and, moreover, is required for + # new generalized list code to work. + if ( !$dbh->bz_index_info('cc', 'cc_bug_id_idx') + || !$dbh->bz_index_info('cc', 'cc_bug_id_idx')->{TYPE}) + { + $dbh->bz_drop_index('cc', 'cc_bug_id_idx'); + $dbh->bz_add_index('cc', 'cc_bug_id_idx', + {TYPE => 'UNIQUE', FIELDS => [qw(bug_id who)]}); + } + if ( !$dbh->bz_index_info('keywords', 'keywords_bug_id_idx') + || !$dbh->bz_index_info('keywords', 'keywords_bug_id_idx')->{TYPE}) + { + $dbh->bz_drop_index('keywords', 'keywords_bug_id_idx'); + $dbh->bz_add_index('keywords', 'keywords_bug_id_idx', + {TYPE => 'UNIQUE', FIELDS => [qw(bug_id keywordid)]}); + } - $dbh->bz_add_column("bugs_activity", "attach_id", {TYPE => 'INT3'}); + _copy_from_comments_to_longdescs(); + _populate_duplicates_table(); - _delete_logincookies_cryptpassword_and_handle_invalid_cookies(); + if (!$dbh->bz_column_info('email_setting', 'user_id')) { + $dbh->bz_add_column('profiles', 'emailflags', {TYPE => 'MEDIUMTEXT'}); + } - # qacontact/assignee should always be able to see bugs: bug 97471 - $dbh->bz_drop_column("bugs", "qacontact_accessible"); - $dbh->bz_drop_column("bugs", "assignee_accessible"); + $dbh->bz_add_column('groups', 'isactive', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); - # 2002-02-20 jeff.hedlund@matrixsi.com - bug 24789 time tracking - $dbh->bz_add_column("longdescs", "work_time", - {TYPE => 'decimal(5,2)', NOTNULL => 1, DEFAULT => '0'}); - $dbh->bz_add_column("bugs", "estimated_time", - {TYPE => 'decimal(5,2)', NOTNULL => 1, DEFAULT => '0'}); - $dbh->bz_add_column("bugs", "remaining_time", - {TYPE => 'decimal(5,2)', NOTNULL => 1, DEFAULT => '0'}); - $dbh->bz_add_column("bugs", "deadline", {TYPE => 'DATETIME'}); + $dbh->bz_add_column('attachments', 'isobsolete', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); - _use_ip_instead_of_hostname_in_logincookies(); + $dbh->bz_drop_column("profiles", "emailnotification"); + $dbh->bz_drop_column("profiles", "newemailtech"); - $dbh->bz_add_column('longdescs', 'isprivate', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); - $dbh->bz_add_column('attachments', 'isprivate', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); + # 2003-11-19; chicks@chicks.net; bug 225973: fix field size to accommodate + # wider algorithms such as Blowfish. Note that this needs to be run + # before recrypting passwords in the following block. + $dbh->bz_alter_column('profiles', 'cryptpassword', {TYPE => 'varchar(128)'}); + + _recrypt_plaintext_passwords(); + + # 2001-06-15 kiko@async.com.br - Change bug:version size to avoid + # truncates re http://bugzilla.mozilla.org/show_bug.cgi?id=9352 + $dbh->bz_alter_column('bugs', 'version', {TYPE => 'varchar(64)', NOTNULL => 1}); - _move_quips_into_db(); + _update_bugs_activity_to_only_record_changes(); - $dbh->bz_drop_column("namedqueries", "watchfordiffs"); + # bug 90933: Make disabledtext NOT NULL + if (!$dbh->bz_column_info('profiles', 'disabledtext')->{NOTNULL}) { + $dbh->bz_alter_column("profiles", "disabledtext", + {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, ''); + } - _use_ids_for_products_and_components(); - _convert_groups_system_from_groupset(); - _convert_attachment_statuses_to_flags(); - _remove_spaces_and_commas_from_flagtypes(); - _setup_usebuggroups_backward_compatibility(); - _remove_user_series_map(); + $dbh->bz_add_column("bugs", "reporter_accessible", + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); + $dbh->bz_add_column("bugs", "cclist_accessible", + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); - # 2006-08-03 remi_zara@mac.com bug 346241, make series.creator nullable - # This must happen before calling _copy_old_charts_into_database(). - if ($dbh->bz_column_info('series', 'creator')->{NOTNULL}) { - $dbh->bz_alter_column('series', 'creator', {TYPE => 'INT3'}); - $dbh->do("UPDATE series SET creator = NULL WHERE creator = 0"); - } + $dbh->bz_add_column("bugs_activity", "attach_id", {TYPE => 'INT3'}); - _copy_old_charts_into_database(); + _delete_logincookies_cryptpassword_and_handle_invalid_cookies(); - _add_user_group_map_grant_type(); - _add_group_group_map_grant_type(); + # qacontact/assignee should always be able to see bugs: bug 97471 + $dbh->bz_drop_column("bugs", "qacontact_accessible"); + $dbh->bz_drop_column("bugs", "assignee_accessible"); - $dbh->bz_add_column("profiles", "extern_id", {TYPE => 'varchar(64)'}); + # 2002-02-20 jeff.hedlund@matrixsi.com - bug 24789 time tracking + $dbh->bz_add_column("longdescs", "work_time", + {TYPE => 'decimal(5,2)', NOTNULL => 1, DEFAULT => '0'}); + $dbh->bz_add_column("bugs", "estimated_time", + {TYPE => 'decimal(5,2)', NOTNULL => 1, DEFAULT => '0'}); + $dbh->bz_add_column("bugs", "remaining_time", + {TYPE => 'decimal(5,2)', NOTNULL => 1, DEFAULT => '0'}); + $dbh->bz_add_column("bugs", "deadline", {TYPE => 'DATETIME'}); - $dbh->bz_add_column('flagtypes', 'grant_group_id', {TYPE => 'INT3'}); - $dbh->bz_add_column('flagtypes', 'request_group_id', {TYPE => 'INT3'}); + _use_ip_instead_of_hostname_in_logincookies(); - # mailto is no longer just userids - $dbh->bz_rename_column('whine_schedules', 'mailto_userid', 'mailto'); - $dbh->bz_add_column('whine_schedules', 'mailto_type', - {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '0'}); + $dbh->bz_add_column('longdescs', 'isprivate', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); + $dbh->bz_add_column('attachments', 'isprivate', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); + + _move_quips_into_db(); + + $dbh->bz_drop_column("namedqueries", "watchfordiffs"); + + _use_ids_for_products_and_components(); + _convert_groups_system_from_groupset(); + _convert_attachment_statuses_to_flags(); + _remove_spaces_and_commas_from_flagtypes(); + _setup_usebuggroups_backward_compatibility(); + _remove_user_series_map(); + + # 2006-08-03 remi_zara@mac.com bug 346241, make series.creator nullable + # This must happen before calling _copy_old_charts_into_database(). + if ($dbh->bz_column_info('series', 'creator')->{NOTNULL}) { + $dbh->bz_alter_column('series', 'creator', {TYPE => 'INT3'}); + $dbh->do("UPDATE series SET creator = NULL WHERE creator = 0"); + } + + _copy_old_charts_into_database(); + + _add_user_group_map_grant_type(); + _add_group_group_map_grant_type(); + + $dbh->bz_add_column("profiles", "extern_id", {TYPE => 'varchar(64)'}); + + $dbh->bz_add_column('flagtypes', 'grant_group_id', {TYPE => 'INT3'}); + $dbh->bz_add_column('flagtypes', 'request_group_id', {TYPE => 'INT3'}); + + # mailto is no longer just userids + $dbh->bz_rename_column('whine_schedules', 'mailto_userid', 'mailto'); + $dbh->bz_add_column('whine_schedules', 'mailto_type', + {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '0'}); + + _add_longdescs_already_wrapped(); - _add_longdescs_already_wrapped(); + # Moved enum types to separate tables so we need change the old enum + # types to standard varchars in the bugs table. + $dbh->bz_alter_column('bugs', 'bug_status', + {TYPE => 'varchar(64)', NOTNULL => 1}); + + # 2005-03-23 Tomas.Kopal@altap.cz - add default value to resolution, + # bug 286695 + $dbh->bz_alter_column('bugs', 'resolution', + {TYPE => 'varchar(64)', NOTNULL => 1, DEFAULT => "''"}); + $dbh->bz_alter_column('bugs', 'priority', + {TYPE => 'varchar(64)', NOTNULL => 1}); + $dbh->bz_alter_column('bugs', 'bug_severity', + {TYPE => 'varchar(64)', NOTNULL => 1}); + $dbh->bz_alter_column('bugs', 'rep_platform', + {TYPE => 'varchar(64)', NOTNULL => 1}, ''); + $dbh->bz_alter_column('bugs', 'op_sys', {TYPE => 'varchar(64)', NOTNULL => 1}); + + # When migrating quips from the '$datadir/comments' file to the DB, + # the user ID should be NULL instead of 0 (which is an invalid user ID). + if ($dbh->bz_column_info('quips', 'userid')->{NOTNULL}) { + $dbh->bz_alter_column('quips', 'userid', {TYPE => 'INT3'}); + print "Changing owner to NULL for quips where the owner is", " unknown...\n"; + $dbh->do('UPDATE quips SET userid = NULL WHERE userid = 0'); + } + + _convert_attachments_filename_from_mediumtext(); + + $dbh->bz_add_column('quips', 'approved', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); + + # 2002-12-20 Bug 180870 - remove manual shadowdb replication code + $dbh->bz_drop_table("shadowlog"); + + _rename_votes_count_and_force_group_refresh(); - # Moved enum types to separate tables so we need change the old enum - # types to standard varchars in the bugs table. - $dbh->bz_alter_column('bugs', 'bug_status', - {TYPE => 'varchar(64)', NOTNULL => 1}); - # 2005-03-23 Tomas.Kopal@altap.cz - add default value to resolution, - # bug 286695 - $dbh->bz_alter_column('bugs', 'resolution', - {TYPE => 'varchar(64)', NOTNULL => 1, DEFAULT => "''"}); - $dbh->bz_alter_column('bugs', 'priority', - {TYPE => 'varchar(64)', NOTNULL => 1}); - $dbh->bz_alter_column('bugs', 'bug_severity', - {TYPE => 'varchar(64)', NOTNULL => 1}); - $dbh->bz_alter_column('bugs', 'rep_platform', - {TYPE => 'varchar(64)', NOTNULL => 1}, ''); - $dbh->bz_alter_column('bugs', 'op_sys', - {TYPE => 'varchar(64)', NOTNULL => 1}); + # 2004/02/15 - Summaries shouldn't be null - see bug 220232 + if (!exists $dbh->bz_column_info('bugs', 'short_desc')->{NOTNULL}) { + $dbh->bz_alter_column('bugs', 'short_desc', + {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, ''); + } - # When migrating quips from the '$datadir/comments' file to the DB, - # the user ID should be NULL instead of 0 (which is an invalid user ID). - if ($dbh->bz_column_info('quips', 'userid')->{NOTNULL}) { - $dbh->bz_alter_column('quips', 'userid', {TYPE => 'INT3'}); - print "Changing owner to NULL for quips where the owner is", - " unknown...\n"; - $dbh->do('UPDATE quips SET userid = NULL WHERE userid = 0'); + $dbh->bz_add_column('products', 'classification_id', + {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '1'}); + + _fix_group_with_empty_name(); + + $dbh->bz_add_index('bugs_activity', 'bugs_activity_who_idx', [qw(who)]); + + # Add defaults for some fields that should have them but didn't. + $dbh->bz_alter_column('bugs', 'status_whiteboard', + {TYPE => 'MEDIUMTEXT', NOTNULL => 1, DEFAULT => "''"}); + if ($dbh->bz_column_info('bugs', 'votes')) { + $dbh->bz_alter_column('bugs', 'votes', + {TYPE => 'INT3', NOTNULL => 1, DEFAULT => '0'}); + } + + $dbh->bz_alter_column('bugs', 'lastdiffed', {TYPE => 'DATETIME'}); + + # 2005-03-09 qa_contact should be NULL instead of 0, bug 285534 + if ($dbh->bz_column_info('bugs', 'qa_contact')->{NOTNULL}) { + $dbh->bz_alter_column('bugs', 'qa_contact', {TYPE => 'INT3'}); + $dbh->do("UPDATE bugs SET qa_contact = NULL WHERE qa_contact = 0"); + } + + # 2005-03-27 initialqacontact should be NULL instead of 0, bug 287483 + if ($dbh->bz_column_info('components', 'initialqacontact')->{NOTNULL}) { + $dbh->bz_alter_column('components', 'initialqacontact', {TYPE => 'INT3'}); + } + $dbh->do("UPDATE components SET initialqacontact = NULL " + . "WHERE initialqacontact = 0"); + + _migrate_email_prefs_to_new_table(); + _initialize_new_email_prefs(); + _change_all_mysql_booleans_to_tinyint(); + + # make classification_id field type be consistent with DB:Schema + $dbh->bz_alter_column('products', 'classification_id', + {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '1'}); + + # initialowner was accidentally NULL when we checked-in Schema, + # when it really should be NOT NULL. + $dbh->bz_alter_column('components', 'initialowner', + {TYPE => 'INT3', NOTNULL => 1}, 0); + + # 2005-03-28 - bug 238800 - index flags.type_id for editflagtypes.cgi + $dbh->bz_add_index('flags', 'flags_type_id_idx', [qw(type_id)]); + + # For a short time, the flags_type_id_idx was misnamed in upgraded installs. + $dbh->bz_drop_index('flags', 'type_id'); + + # 2005-04-28 - LpSolit@gmail.com - Bug 7233: add an index to versions + $dbh->bz_alter_column('versions', 'value', + {TYPE => 'varchar(64)', NOTNULL => 1}); + _add_versions_product_id_index(); + + if (!exists $dbh->bz_column_info('milestones', 'sortkey')->{DEFAULT}) { + $dbh->bz_alter_column('milestones', 'sortkey', + {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0}); + } + + # 2005-06-14 - LpSolit@gmail.com - Bug 292544 + $dbh->bz_alter_column('bugs', 'creation_ts', {TYPE => 'DATETIME'}); + + _fix_whine_queries_title_and_op_sys_value(); + _fix_attachments_submitter_id_idx(); + _copy_attachments_thedata_to_attach_data(); + _fix_broken_all_closed_series(); + + # 2005-08-14 bugreport@peshkin.net -- Bug 304583 + # Get rid of leftover DERIVED group permissions + use constant GRANT_DERIVED => 1; + $dbh->do("DELETE FROM user_group_map WHERE grant_type = " . GRANT_DERIVED); + + _rederive_regex_groups(); + + # PUBLIC is a reserved word in Oracle. + $dbh->bz_rename_column('series', 'public', 'is_public'); + + # 2005-11-04 LpSolit@gmail.com - Bug 305927 + $dbh->bz_alter_column('groups', 'userregexp', + {TYPE => 'TINYTEXT', NOTNULL => 1, DEFAULT => "''"}); + + # 2005-09-26 - olav@bkor.dhs.org - Bug 119524 + $dbh->bz_alter_column('logincookies', 'cookie', + {TYPE => 'varchar(16)', PRIMARYKEY => 1, NOTNULL => 1}); + + _clean_control_characters_from_short_desc(); + + # 2005-12-07 altlst@sonic.net -- Bug 225221 + $dbh->bz_add_column('longdescs', 'comment_id', + {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); + + _stop_storing_inactive_flags(); + _change_short_desc_from_mediumtext_to_varchar(); + + # 2006-07-01 wurblzap@gmail.com -- Bug 69000 + $dbh->bz_add_column('namedqueries', 'id', + {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); + _move_namedqueries_linkinfooter_to_its_own_table(); + + _add_classifications_sortkey(); + _move_data_nomail_into_db(); + + # The products table lacked sensible defaults. + if ($dbh->bz_column_info('products', 'milestoneurl')) { + $dbh->bz_alter_column('products', 'milestoneurl', + {TYPE => 'TINYTEXT', NOTNULL => 1, DEFAULT => "''"}); + } + if ($dbh->bz_column_info('products', 'disallownew')) { + $dbh->bz_alter_column('products', 'disallownew', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 0}); + + if ($dbh->bz_column_info('products', 'votesperuser')) { + $dbh->bz_alter_column('products', 'votesperuser', + {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0}); + $dbh->bz_alter_column('products', 'votestoconfirm', + {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0}); } + } - _convert_attachments_filename_from_mediumtext(); + # 2006-08-04 LpSolit@gmail.com - Bug 305941 + $dbh->bz_drop_column('profiles', 'refreshed_when'); + $dbh->bz_drop_column('groups', 'last_changed'); - $dbh->bz_add_column('quips', 'approved', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); + # 2006-08-06 LpSolit@gmail.com - Bug 347521 + $dbh->bz_alter_column('flagtypes', 'id', + {TYPE => 'SMALLSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); - # 2002-12-20 Bug 180870 - remove manual shadowdb replication code - $dbh->bz_drop_table("shadowlog"); + $dbh->bz_alter_column('keyworddefs', 'id', + {TYPE => 'SMALLSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); - _rename_votes_count_and_force_group_refresh(); + # 2006-08-19 LpSolit@gmail.com - Bug 87795 + $dbh->bz_alter_column('tokens', 'userid', {TYPE => 'INT3'}); - # 2004/02/15 - Summaries shouldn't be null - see bug 220232 - if (!exists $dbh->bz_column_info('bugs', 'short_desc')->{NOTNULL}) { - $dbh->bz_alter_column('bugs', 'short_desc', - {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, ''); - } + $dbh->bz_drop_index('bugs', 'bugs_short_desc_idx'); - $dbh->bz_add_column('products', 'classification_id', - {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '1'}); + # The profiles table was missing some defaults. + $dbh->bz_alter_column('profiles', 'disabledtext', + {TYPE => 'MEDIUMTEXT', NOTNULL => 1, DEFAULT => "''"}); + $dbh->bz_alter_column('profiles', 'realname', + {TYPE => 'varchar(255)', NOTNULL => 1, DEFAULT => "''"}); - _fix_group_with_empty_name(); + _update_longdescs_who_index(); - $dbh->bz_add_index('bugs_activity', 'bugs_activity_who_idx', [qw(who)]); + $dbh->bz_add_column('setting', 'subclass', {TYPE => 'varchar(32)'}); - # Add defaults for some fields that should have them but didn't. - $dbh->bz_alter_column('bugs', 'status_whiteboard', - {TYPE => 'MEDIUMTEXT', NOTNULL => 1, DEFAULT => "''"}); - if ($dbh->bz_column_info('bugs', 'votes')) { - $dbh->bz_alter_column('bugs', 'votes', - {TYPE => 'INT3', NOTNULL => 1, DEFAULT => '0'}); - } + $dbh->bz_alter_column('longdescs', 'thetext', + {TYPE => 'LONGTEXT', NOTNULL => 1}, ''); - $dbh->bz_alter_column('bugs', 'lastdiffed', {TYPE => 'DATETIME'}); + # 2006-10-20 LpSolit@gmail.com - Bug 189627 + $dbh->bz_add_column('group_control_map', 'editcomponents', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); + $dbh->bz_add_column('group_control_map', 'editbugs', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); + $dbh->bz_add_column('group_control_map', 'canconfirm', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); - # 2005-03-09 qa_contact should be NULL instead of 0, bug 285534 - if ($dbh->bz_column_info('bugs', 'qa_contact')->{NOTNULL}) { - $dbh->bz_alter_column('bugs', 'qa_contact', {TYPE => 'INT3'}); - $dbh->do("UPDATE bugs SET qa_contact = NULL WHERE qa_contact = 0"); - } + # 2006-11-07 LpSolit@gmail.com - Bug 353656 + $dbh->bz_add_column('longdescs', 'type', + {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '0'}); + $dbh->bz_add_column('longdescs', 'extra_data', {TYPE => 'varchar(255)'}); - # 2005-03-27 initialqacontact should be NULL instead of 0, bug 287483 - if ($dbh->bz_column_info('components', 'initialqacontact')->{NOTNULL}) { - $dbh->bz_alter_column('components', 'initialqacontact', - {TYPE => 'INT3'}); - } - $dbh->do("UPDATE components SET initialqacontact = NULL " . - "WHERE initialqacontact = 0"); + $dbh->bz_add_column('versions', 'id', + {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); + $dbh->bz_add_column('milestones', 'id', + {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); - _migrate_email_prefs_to_new_table(); - _initialize_new_email_prefs(); - _change_all_mysql_booleans_to_tinyint(); + _fix_uppercase_custom_field_names(); + _fix_uppercase_index_names(); - # make classification_id field type be consistent with DB:Schema - $dbh->bz_alter_column('products', 'classification_id', - {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '1'}); + # 2007-05-17 LpSolit@gmail.com - Bug 344965 + _initialize_workflow_for_upgrade($old_params); - # initialowner was accidentally NULL when we checked-in Schema, - # when it really should be NOT NULL. - $dbh->bz_alter_column('components', 'initialowner', - {TYPE => 'INT3', NOTNULL => 1}, 0); + # 2007-08-08 LpSolit@gmail.com - Bug 332149 + $dbh->bz_add_column('groups', 'icon_url', {TYPE => 'TINYTEXT'}); - # 2005-03-28 - bug 238800 - index flags.type_id for editflagtypes.cgi - $dbh->bz_add_index('flags', 'flags_type_id_idx', [qw(type_id)]); + # 2007-08-21 wurblzap@gmail.com - Bug 365378 + _make_lang_setting_dynamic(); - # For a short time, the flags_type_id_idx was misnamed in upgraded installs. - $dbh->bz_drop_index('flags', 'type_id'); + # 2007-11-29 xiaoou.wu@oracle.com - Bug 153129 + _change_text_types(); - # 2005-04-28 - LpSolit@gmail.com - Bug 7233: add an index to versions - $dbh->bz_alter_column('versions', 'value', - {TYPE => 'varchar(64)', NOTNULL => 1}); - _add_versions_product_id_index(); + # 2007-09-09 LpSolit@gmail.com - Bug 99215 + _fix_attachment_modification_date(); - if (!exists $dbh->bz_column_info('milestones', 'sortkey')->{DEFAULT}) { - $dbh->bz_alter_column('milestones', 'sortkey', - {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0}); - } - - # 2005-06-14 - LpSolit@gmail.com - Bug 292544 - $dbh->bz_alter_column('bugs', 'creation_ts', {TYPE => 'DATETIME'}); + $dbh->bz_drop_index('longdescs', 'longdescs_thetext_idx'); + _populate_bugs_fulltext(); - _fix_whine_queries_title_and_op_sys_value(); - _fix_attachments_submitter_id_idx(); - _copy_attachments_thedata_to_attach_data(); - _fix_broken_all_closed_series(); - # 2005-08-14 bugreport@peshkin.net -- Bug 304583 - # Get rid of leftover DERIVED group permissions - use constant GRANT_DERIVED => 1; - $dbh->do("DELETE FROM user_group_map WHERE grant_type = " . GRANT_DERIVED); + # 2008-01-18 xiaoou.wu@oracle.com - Bug 414292 + $dbh->bz_alter_column('series', 'query', {TYPE => 'MEDIUMTEXT', NOTNULL => 1}); - _rederive_regex_groups(); + # Add FK to multi select field tables + _add_foreign_keys_to_multiselects(); - # PUBLIC is a reserved word in Oracle. - $dbh->bz_rename_column('series', 'public', 'is_public'); + # 2008-07-28 tfu@redhat.com - Bug 431669 + $dbh->bz_alter_column('group_control_map', 'product_id', + {TYPE => 'INT2', NOTNULL => 1}); - # 2005-11-04 LpSolit@gmail.com - Bug 305927 - $dbh->bz_alter_column('groups', 'userregexp', - {TYPE => 'TINYTEXT', NOTNULL => 1, DEFAULT => "''"}); + # 2008-09-07 LpSolit@gmail.com - Bug 452893 + _fix_illegal_flag_modification_dates(); - # 2005-09-26 - olav@bkor.dhs.org - Bug 119524 - $dbh->bz_alter_column('logincookies', 'cookie', - {TYPE => 'varchar(16)', PRIMARYKEY => 1, NOTNULL => 1}); + _add_visiblity_value_to_value_tables(); - _clean_control_characters_from_short_desc(); - - # 2005-12-07 altlst@sonic.net -- Bug 225221 - $dbh->bz_add_column('longdescs', 'comment_id', - {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); + # 2009-03-02 arbingersys@gmail.com - Bug 423613 + _add_extern_id_index(); - _stop_storing_inactive_flags(); - _change_short_desc_from_mediumtext_to_varchar(); + # 2009-03-31 LpSolit@gmail.com - Bug 478972 + $dbh->bz_alter_column('group_control_map', 'entry', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); + $dbh->bz_alter_column('group_control_map', 'canedit', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); - # 2006-07-01 wurblzap@gmail.com -- Bug 69000 - $dbh->bz_add_column('namedqueries', 'id', - {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); - _move_namedqueries_linkinfooter_to_its_own_table(); + # 2009-01-16 oreomike@gmail.com - Bug 302420 + $dbh->bz_add_column('whine_events', 'mailifnobugs', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); - _add_classifications_sortkey(); - _move_data_nomail_into_db(); + _convert_disallownew_to_isactive(); - # The products table lacked sensible defaults. - if ($dbh->bz_column_info('products', 'milestoneurl')) { - $dbh->bz_alter_column('products', 'milestoneurl', - {TYPE => 'TINYTEXT', NOTNULL => 1, DEFAULT => "''"}); - } - if ($dbh->bz_column_info('products', 'disallownew')){ - $dbh->bz_alter_column('products', 'disallownew', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 0}); - - if ($dbh->bz_column_info('products', 'votesperuser')) { - $dbh->bz_alter_column('products', 'votesperuser', - {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0}); - $dbh->bz_alter_column('products', 'votestoconfirm', - {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0}); - } - } + $dbh->bz_alter_column('bugs_activity', 'added', {TYPE => 'varchar(255)'}); + $dbh->bz_add_index('bugs_activity', 'bugs_activity_added_idx', ['added']); - # 2006-08-04 LpSolit@gmail.com - Bug 305941 - $dbh->bz_drop_column('profiles', 'refreshed_when'); - $dbh->bz_drop_column('groups', 'last_changed'); + # 2009-09-28 LpSolit@gmail.com - Bug 519032 + $dbh->bz_drop_column('series', 'last_viewed'); - # 2006-08-06 LpSolit@gmail.com - Bug 347521 - $dbh->bz_alter_column('flagtypes', 'id', - {TYPE => 'SMALLSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); + # 2009-09-28 LpSolit@gmail.com - Bug 399073 + _fix_logincookies_ipaddr(); - $dbh->bz_alter_column('keyworddefs', 'id', - {TYPE => 'SMALLSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); + # 2009-11-01 LpSolit@gmail.com - Bug 525025 + _fix_invalid_custom_field_names(); - # 2006-08-19 LpSolit@gmail.com - Bug 87795 - $dbh->bz_alter_column('tokens', 'userid', {TYPE => 'INT3'}); + _set_attachment_comment_types(); - $dbh->bz_drop_index('bugs', 'bugs_short_desc_idx'); + $dbh->bz_drop_column('products', 'milestoneurl'); - # The profiles table was missing some defaults. - $dbh->bz_alter_column('profiles', 'disabledtext', - {TYPE => 'MEDIUMTEXT', NOTNULL => 1, DEFAULT => "''"}); - $dbh->bz_alter_column('profiles', 'realname', - {TYPE => 'varchar(255)', NOTNULL => 1, DEFAULT => "''"}); + _add_allows_unconfirmed_to_product_table(); + _convert_flagtypes_fks_to_set_null(); + _fix_decimal_types(); + _fix_series_creator_fk(); - _update_longdescs_who_index(); + # 2009-11-14 dkl@redhat.com - Bug 310450 + $dbh->bz_add_column('bugs_activity', 'comment_id', {TYPE => 'INT4'}); - $dbh->bz_add_column('setting', 'subclass', {TYPE => 'varchar(32)'}); + # 2010-04-07 LpSolit@gmail.com - Bug 69621 + $dbh->bz_drop_column('bugs', 'keywords'); - $dbh->bz_alter_column('longdescs', 'thetext', - {TYPE => 'LONGTEXT', NOTNULL => 1}, ''); + # 2010-05-07 ewong@pw-wspx.org - Bug 463945 + $dbh->bz_alter_column('group_control_map', 'membercontrol', + {TYPE => 'INT1', NOTNULL => 1, DEFAULT => CONTROLMAPNA}); + $dbh->bz_alter_column('group_control_map', 'othercontrol', + {TYPE => 'INT1', NOTNULL => 1, DEFAULT => CONTROLMAPNA}); - # 2006-10-20 LpSolit@gmail.com - Bug 189627 - $dbh->bz_add_column('group_control_map', 'editcomponents', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); - $dbh->bz_add_column('group_control_map', 'editbugs', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); - $dbh->bz_add_column('group_control_map', 'canconfirm', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); + # Add NOT NULL to some columns that need it, and DEFAULT to + # attachments.ispatch. + $dbh->bz_alter_column('attachments', 'ispatch', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); + $dbh->bz_alter_column('keyworddefs', 'description', + {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, ''); + $dbh->bz_alter_column('products', 'description', + {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, ''); - # 2006-11-07 LpSolit@gmail.com - Bug 353656 - $dbh->bz_add_column('longdescs', 'type', - {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '0'}); - $dbh->bz_add_column('longdescs', 'extra_data', {TYPE => 'varchar(255)'}); + # Change the default of allows_unconfirmed to TRUE as part + # of the new workflow. + $dbh->bz_alter_column('products', 'allows_unconfirmed', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); - $dbh->bz_add_column('versions', 'id', - {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); - $dbh->bz_add_column('milestones', 'id', - {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); + # 2010-07-18 LpSolit@gmail.com - Bug 119703 + _remove_attachment_isurl(); - _fix_uppercase_custom_field_names(); - _fix_uppercase_index_names(); + # 2009-05-07 ghendricks@novell.com - Bug 77193 + _add_isactive_to_product_fields(); - # 2007-05-17 LpSolit@gmail.com - Bug 344965 - _initialize_workflow_for_upgrade($old_params); + # 2010-10-09 LpSolit@gmail.com - Bug 505165 + $dbh->bz_alter_column('flags', 'setter_id', {TYPE => 'INT3', NOTNULL => 1}); - # 2007-08-08 LpSolit@gmail.com - Bug 332149 - $dbh->bz_add_column('groups', 'icon_url', {TYPE => 'TINYTEXT'}); + # 2010-10-09 LpSolit@gmail.com - Bug 451735 + _fix_series_indexes(); - # 2007-08-21 wurblzap@gmail.com - Bug 365378 - _make_lang_setting_dynamic(); - - # 2007-11-29 xiaoou.wu@oracle.com - Bug 153129 - _change_text_types(); + $dbh->bz_add_column('bug_see_also', 'id', + {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); - # 2007-09-09 LpSolit@gmail.com - Bug 99215 - _fix_attachment_modification_date(); + _rename_tags_to_tag(); - $dbh->bz_drop_index('longdescs', 'longdescs_thetext_idx'); - _populate_bugs_fulltext(); + # 2011-01-29 LpSolit@gmail.com - Bug 616185 + _migrate_user_tags(); - # 2008-01-18 xiaoou.wu@oracle.com - Bug 414292 - $dbh->bz_alter_column('series', 'query', - { TYPE => 'MEDIUMTEXT', NOTNULL => 1 }); + _populate_bug_see_also_class(); - # Add FK to multi select field tables - _add_foreign_keys_to_multiselects(); + # 2011-06-15 dkl@mozilla.com - Bug 658929 + _migrate_disabledtext_boolean(); - # 2008-07-28 tfu@redhat.com - Bug 431669 - $dbh->bz_alter_column('group_control_map', 'product_id', - { TYPE => 'INT2', NOTNULL => 1 }); + # 2011-11-01 glob@mozilla.com - Bug 240437 + $dbh->bz_add_column('profiles', 'last_seen_date', {TYPE => 'DATETIME'}); - # 2008-09-07 LpSolit@gmail.com - Bug 452893 - _fix_illegal_flag_modification_dates(); + # 2011-10-11 miketosh - Bug 690173 + _on_delete_set_null_for_audit_log_userid(); - _add_visiblity_value_to_value_tables(); + # 2011-11-23 gerv@gerv.net - Bug 705058 - make filenames longer + $dbh->bz_alter_column('attachments', 'filename', + {TYPE => 'varchar(255)', NOTNULL => 1}); - # 2009-03-02 arbingersys@gmail.com - Bug 423613 - _add_extern_id_index(); + # 2011-11-28 dkl@mozilla.com - Bug 685611 + _fix_notnull_defaults(); - # 2009-03-31 LpSolit@gmail.com - Bug 478972 - $dbh->bz_alter_column('group_control_map', 'entry', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); - $dbh->bz_alter_column('group_control_map', 'canedit', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); + # 2012-02-15 LpSolit@gmail.com - Bug 722113 + if ($dbh->bz_index_info('profile_search', 'profile_search_user_id')) { + $dbh->bz_drop_index('profile_search', 'profile_search_user_id'); + $dbh->bz_add_index('profile_search', 'profile_search_user_id_idx', + [qw(user_id)]); + } - # 2009-01-16 oreomike@gmail.com - Bug 302420 - $dbh->bz_add_column('whine_events', 'mailifnobugs', - { TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); - - _convert_disallownew_to_isactive(); + # 2012-03-23 LpSolit@gmail.com - Bug 448551 + $dbh->bz_alter_column('bugs', 'target_milestone', + {TYPE => 'varchar(64)', NOTNULL => 1, DEFAULT => "'---'"}); - $dbh->bz_alter_column('bugs_activity', 'added', - { TYPE => 'varchar(255)' }); - $dbh->bz_add_index('bugs_activity', 'bugs_activity_added_idx', ['added']); + $dbh->bz_alter_column('milestones', 'value', + {TYPE => 'varchar(64)', NOTNULL => 1}); - # 2009-09-28 LpSolit@gmail.com - Bug 519032 - $dbh->bz_drop_column('series', 'last_viewed'); + $dbh->bz_alter_column('products', 'defaultmilestone', + {TYPE => 'varchar(64)', NOTNULL => 1, DEFAULT => "'---'"}); - # 2009-09-28 LpSolit@gmail.com - Bug 399073 - _fix_logincookies_ipaddr(); + # 2012-04-15 Frank@Frank-Becker.de - Bug 740536 + $dbh->bz_add_index('audit_log', 'audit_log_class_idx', ['class', 'at_time']); - # 2009-11-01 LpSolit@gmail.com - Bug 525025 - _fix_invalid_custom_field_names(); + # 2012-06-06 dkl@mozilla.com - Bug 762288 + $dbh->bz_alter_column('bugs_activity', 'removed', {TYPE => 'varchar(255)'}); + $dbh->bz_add_index('bugs_activity', 'bugs_activity_removed_idx', ['removed']); - _set_attachment_comment_types(); + # 2012-06-13 dkl@mozilla.com - Bug 764457 + $dbh->bz_add_column('bugs_activity', 'id', + {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); - $dbh->bz_drop_column('products', 'milestoneurl'); + # 2012-06-13 dkl@mozilla.com - Bug 764466 + $dbh->bz_add_column('profiles_activity', 'id', + {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); - _add_allows_unconfirmed_to_product_table(); - _convert_flagtypes_fks_to_set_null(); - _fix_decimal_types(); - _fix_series_creator_fk(); + # 2012-07-24 dkl@mozilla.com - Bug 776972 + $dbh->bz_alter_column('bugs_activity', 'id', + {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); - # 2009-11-14 dkl@redhat.com - Bug 310450 - $dbh->bz_add_column('bugs_activity', 'comment_id', {TYPE => 'INT4'}); - # 2010-04-07 LpSolit@gmail.com - Bug 69621 - $dbh->bz_drop_column('bugs', 'keywords'); - - # 2010-05-07 ewong@pw-wspx.org - Bug 463945 - $dbh->bz_alter_column('group_control_map', 'membercontrol', - {TYPE => 'INT1', NOTNULL => 1, DEFAULT => CONTROLMAPNA}); - $dbh->bz_alter_column('group_control_map', 'othercontrol', - {TYPE => 'INT1', NOTNULL => 1, DEFAULT => CONTROLMAPNA}); + # 2012-07-24 dkl@mozilla.com - Bug 776982 + _fix_longdescs_primary_key(); - # Add NOT NULL to some columns that need it, and DEFAULT to - # attachments.ispatch. - $dbh->bz_alter_column('attachments', 'ispatch', - { TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); - $dbh->bz_alter_column('keyworddefs', 'description', - { TYPE => 'MEDIUMTEXT', NOTNULL => 1 }, ''); - $dbh->bz_alter_column('products', 'description', - { TYPE => 'MEDIUMTEXT', NOTNULL => 1 }, ''); + # 2012-08-02 dkl@mozilla.com - Bug 756953 + _fix_dependencies_dupes(); - # Change the default of allows_unconfirmed to TRUE as part - # of the new workflow. - $dbh->bz_alter_column('products', 'allows_unconfirmed', - { TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE' }); + # 2012-08-01 koosha.khajeh@gmail.com - Bug 187753 + _shorten_long_quips(); - # 2010-07-18 LpSolit@gmail.com - Bug 119703 - _remove_attachment_isurl(); + # 2012-12-29 reed@reedloden.com - Bug 785283 + _add_password_salt_separator(); - # 2009-05-07 ghendricks@novell.com - Bug 77193 - _add_isactive_to_product_fields(); + # 2013-01-02 LpSolit@gmail.com - Bug 824361 + _fix_longdescs_indexes(); - # 2010-10-09 LpSolit@gmail.com - Bug 505165 - $dbh->bz_alter_column('flags', 'setter_id', {TYPE => 'INT3', NOTNULL => 1}); + # 2013-02-04 dkl@mozilla.com - Bug 824346 + _fix_flagclusions_indexes(); - # 2010-10-09 LpSolit@gmail.com - Bug 451735 - _fix_series_indexes(); + # 2013-08-26 sgreen@redhat.com - Bug 903895 + _fix_components_primary_key(); - $dbh->bz_add_column('bug_see_also', 'id', - {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); + # 2014-06-09 dylan@mozilla.com - Bug 1022923 + $dbh->bz_add_index('bug_user_last_visit', + 'bug_user_last_visit_last_visit_ts_idx', + ['last_visit_ts']); - _rename_tags_to_tag(); + # 2014-07-14 sgreen@redhat.com - Bug 726696 + $dbh->bz_alter_column('tokens', 'tokentype', + {TYPE => 'varchar(16)', NOTNULL => 1}); - # 2011-01-29 LpSolit@gmail.com - Bug 616185 - _migrate_user_tags(); + # 2014-07-27 LpSolit@gmail.com - Bug 1044561 + _fix_user_api_keys_indexes(); - _populate_bug_see_also_class(); + # 2014-08-11 sgreen@redhat.com - Bug 1012506 + _update_alias(); - # 2011-06-15 dkl@mozilla.com - Bug 658929 - _migrate_disabledtext_boolean(); + # 2014-11-10 dkl@mozilla.com - Bug 1093928 + $dbh->bz_drop_column('longdescs', 'is_markdown'); - # 2011-11-01 glob@mozilla.com - Bug 240437 - $dbh->bz_add_column('profiles', 'last_seen_date', {TYPE => 'DATETIME'}); + # 2015-12-16 LpSolit@gmail.com - Bug 1232578 + _sanitize_audit_log_table(); - # 2011-10-11 miketosh - Bug 690173 - _on_delete_set_null_for_audit_log_userid(); - - # 2011-11-23 gerv@gerv.net - Bug 705058 - make filenames longer - $dbh->bz_alter_column('attachments', 'filename', - { TYPE => 'varchar(255)', NOTNULL => 1 }); + ################################################################ + # New --TABLE-- changes should go *** A B O V E *** this point # + ################################################################ - # 2011-11-28 dkl@mozilla.com - Bug 685611 - _fix_notnull_defaults(); + Bugzilla::Hook::process('install_update_db'); - # 2012-02-15 LpSolit@gmail.com - Bug 722113 - if ($dbh->bz_index_info('profile_search', 'profile_search_user_id')) { - $dbh->bz_drop_index('profile_search', 'profile_search_user_id'); - $dbh->bz_add_index('profile_search', 'profile_search_user_id_idx', [qw(user_id)]); - } + # We do this here because otherwise the foreign key from + # products.classification_id to classifications.id will fail + # (because products.classification_id defaults to "1", so on upgraded + # installations it's already been set before the first Classification + # exists). + Bugzilla::Install::create_default_classification(); - # 2012-03-23 LpSolit@gmail.com - Bug 448551 - $dbh->bz_alter_column('bugs', 'target_milestone', - {TYPE => 'varchar(64)', NOTNULL => 1, DEFAULT => "'---'"}); - - $dbh->bz_alter_column('milestones', 'value', {TYPE => 'varchar(64)', NOTNULL => 1}); - - $dbh->bz_alter_column('products', 'defaultmilestone', - {TYPE => 'varchar(64)', NOTNULL => 1, DEFAULT => "'---'"}); - - # 2012-04-15 Frank@Frank-Becker.de - Bug 740536 - $dbh->bz_add_index('audit_log', 'audit_log_class_idx', ['class', 'at_time']); - - # 2012-06-06 dkl@mozilla.com - Bug 762288 - $dbh->bz_alter_column('bugs_activity', 'removed', - { TYPE => 'varchar(255)' }); - $dbh->bz_add_index('bugs_activity', 'bugs_activity_removed_idx', ['removed']); - - # 2012-06-13 dkl@mozilla.com - Bug 764457 - $dbh->bz_add_column('bugs_activity', 'id', - {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); - - # 2012-06-13 dkl@mozilla.com - Bug 764466 - $dbh->bz_add_column('profiles_activity', 'id', - {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); - - # 2012-07-24 dkl@mozilla.com - Bug 776972 - $dbh->bz_alter_column('bugs_activity', 'id', - {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); - - - # 2012-07-24 dkl@mozilla.com - Bug 776982 - _fix_longdescs_primary_key(); - - # 2012-08-02 dkl@mozilla.com - Bug 756953 - _fix_dependencies_dupes(); - - # 2012-08-01 koosha.khajeh@gmail.com - Bug 187753 - _shorten_long_quips(); - - # 2012-12-29 reed@reedloden.com - Bug 785283 - _add_password_salt_separator(); - - # 2013-01-02 LpSolit@gmail.com - Bug 824361 - _fix_longdescs_indexes(); - - # 2013-02-04 dkl@mozilla.com - Bug 824346 - _fix_flagclusions_indexes(); - - # 2013-08-26 sgreen@redhat.com - Bug 903895 - _fix_components_primary_key(); - - # 2014-06-09 dylan@mozilla.com - Bug 1022923 - $dbh->bz_add_index('bug_user_last_visit', - 'bug_user_last_visit_last_visit_ts_idx', - ['last_visit_ts']); - - # 2014-07-14 sgreen@redhat.com - Bug 726696 - $dbh->bz_alter_column('tokens', 'tokentype', - {TYPE => 'varchar(16)', NOTNULL => 1}); - - # 2014-07-27 LpSolit@gmail.com - Bug 1044561 - _fix_user_api_keys_indexes(); - - # 2014-08-11 sgreen@redhat.com - Bug 1012506 - _update_alias(); - - # 2014-11-10 dkl@mozilla.com - Bug 1093928 - $dbh->bz_drop_column('longdescs', 'is_markdown'); - - # 2015-12-16 LpSolit@gmail.com - Bug 1232578 - _sanitize_audit_log_table(); - - ################################################################ - # New --TABLE-- changes should go *** A B O V E *** this point # - ################################################################ - - Bugzilla::Hook::process('install_update_db'); - - # We do this here because otherwise the foreign key from - # products.classification_id to classifications.id will fail - # (because products.classification_id defaults to "1", so on upgraded - # installations it's already been set before the first Classification - # exists). - Bugzilla::Install::create_default_classification(); - - $dbh->bz_setup_foreign_keys(); + $dbh->bz_setup_foreign_keys(); } # Subroutines should be ordered in the order that they are called. # Thus, newer subroutines should be at the bottom. sub _update_pre_checksetup_bugzillas { - my $dbh = Bugzilla->dbh; - # really old fields that were added before checksetup.pl existed - # but aren't in very old bugzilla's (like 2.1) - # Steve Stock (sstock@iconnect-inc.com) - - $dbh->bz_add_column('bugs', 'target_milestone', - {TYPE => 'varchar(20)', NOTNULL => 1, DEFAULT => "'---'"}); - $dbh->bz_add_column('bugs', 'qa_contact', {TYPE => 'INT3'}); - $dbh->bz_add_column('bugs', 'status_whiteboard', - {TYPE => 'MEDIUMTEXT', NOTNULL => 1, DEFAULT => "''"}); - if (!$dbh->bz_column_info('products', 'isactive')){ - $dbh->bz_add_column('products', 'disallownew', - {TYPE => 'BOOLEAN', NOTNULL => 1}, 0); - } - - $dbh->bz_add_column('components', 'initialqacontact', - {TYPE => 'TINYTEXT'}); - $dbh->bz_add_column('components', 'description', - {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, ''); + my $dbh = Bugzilla->dbh; + + # really old fields that were added before checksetup.pl existed + # but aren't in very old bugzilla's (like 2.1) + # Steve Stock (sstock@iconnect-inc.com) + + $dbh->bz_add_column('bugs', 'target_milestone', + {TYPE => 'varchar(20)', NOTNULL => 1, DEFAULT => "'---'"}); + $dbh->bz_add_column('bugs', 'qa_contact', {TYPE => 'INT3'}); + $dbh->bz_add_column('bugs', 'status_whiteboard', + {TYPE => 'MEDIUMTEXT', NOTNULL => 1, DEFAULT => "''"}); + if (!$dbh->bz_column_info('products', 'isactive')) { + $dbh->bz_add_column('products', 'disallownew', + {TYPE => 'BOOLEAN', NOTNULL => 1}, 0); + } + + $dbh->bz_add_column('components', 'initialqacontact', {TYPE => 'TINYTEXT'}); + $dbh->bz_add_column('components', 'description', + {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, ''); } sub _add_bug_vote_cache { - my $dbh = Bugzilla->dbh; - # 1999-10-11 Restructured voting database to add a cached value in each - # bug recording how many total votes that bug has. While I'm at it, - # I removed the unused "area" field from the bugs database. It is - # distressing to realize that the bugs table has reached the maximum - # number of indices allowed by MySQL (16), which may make future - # enhancements awkward. - # (P.S. All is not lost; it appears that the latest betas of MySQL - # support a new table format which will allow 32 indices.) - - if ($dbh->bz_column_info('bugs', 'area')) { - $dbh->bz_drop_column('bugs', 'area'); - $dbh->bz_add_column('bugs', 'votes', {TYPE => 'INT3', NOTNULL => 1, - DEFAULT => 0}); - $dbh->bz_add_index('bugs', 'bugs_votes_idx', [qw(votes)]); - $dbh->bz_add_column('products', 'votesperuser', - {TYPE => 'INT2', NOTNULL => 1}, 0); - } + my $dbh = Bugzilla->dbh; + + # 1999-10-11 Restructured voting database to add a cached value in each + # bug recording how many total votes that bug has. While I'm at it, + # I removed the unused "area" field from the bugs database. It is + # distressing to realize that the bugs table has reached the maximum + # number of indices allowed by MySQL (16), which may make future + # enhancements awkward. + # (P.S. All is not lost; it appears that the latest betas of MySQL + # support a new table format which will allow 32 indices.) + + if ($dbh->bz_column_info('bugs', 'area')) { + $dbh->bz_drop_column('bugs', 'area'); + $dbh->bz_add_column('bugs', 'votes', + {TYPE => 'INT3', NOTNULL => 1, DEFAULT => 0}); + $dbh->bz_add_index('bugs', 'bugs_votes_idx', [qw(votes)]); + $dbh->bz_add_column('products', 'votesperuser', {TYPE => 'INT2', NOTNULL => 1}, + 0); + } } sub _update_product_name_definition { - my $dbh = Bugzilla->dbh; - # The product name used to be very different in various tables. - # - # It was varchar(16) in bugs - # tinytext in components - # tinytext in products - # tinytext in versions - # - # tinytext is equivalent to varchar(255), which is quite huge, so I change - # them all to varchar(64). - - # Only do this if these fields still exist - they're removed in - # a later change - if ($dbh->bz_column_info('products', 'product')) { - $dbh->bz_alter_column('bugs', 'product', - {TYPE => 'varchar(64)', NOTNULL => 1}); - $dbh->bz_alter_column('components', 'program', {TYPE => 'varchar(64)'}); - $dbh->bz_alter_column('products', 'product', {TYPE => 'varchar(64)'}); - $dbh->bz_alter_column('versions', 'program', - {TYPE => 'varchar(64)', NOTNULL => 1}); - } + my $dbh = Bugzilla->dbh; + + # The product name used to be very different in various tables. + # + # It was varchar(16) in bugs + # tinytext in components + # tinytext in products + # tinytext in versions + # + # tinytext is equivalent to varchar(255), which is quite huge, so I change + # them all to varchar(64). + + # Only do this if these fields still exist - they're removed in + # a later change + if ($dbh->bz_column_info('products', 'product')) { + $dbh->bz_alter_column('bugs', 'product', {TYPE => 'varchar(64)', NOTNULL => 1}); + $dbh->bz_alter_column('components', 'program', {TYPE => 'varchar(64)'}); + $dbh->bz_alter_column('products', 'product', {TYPE => 'varchar(64)'}); + $dbh->bz_alter_column('versions', 'program', + {TYPE => 'varchar(64)', NOTNULL => 1}); + } } # A helper for the function below. sub _write_one_longdesc { - my ($id, $who, $when, $buffer) = (@_); - my $dbh = Bugzilla->dbh; - $buffer = trim($buffer); - return if !$buffer; - $dbh->do("INSERT INTO longdescs (bug_id, who, bug_when, thetext) - VALUES (?,?,?,?)", undef, $id, $who, - time2str("%Y/%m/%d %H:%M:%S", $when), $buffer); + my ($id, $who, $when, $buffer) = (@_); + my $dbh = Bugzilla->dbh; + $buffer = trim($buffer); + return if !$buffer; + $dbh->do( + "INSERT INTO longdescs (bug_id, who, bug_when, thetext) + VALUES (?,?,?,?)", undef, $id, $who, + time2str("%Y/%m/%d %H:%M:%S", $when), $buffer + ); } sub _populate_longdescs { - my $dbh = Bugzilla->dbh; - # 2000-01-20 Added a new "longdescs" table, which is supposed to have - # all the long descriptions in it, replacing the old long_desc field - # in the bugs table. The below hideous code populates this new table - # with things from the old field, with ugly parsing and heuristics. - - if ($dbh->bz_column_info('bugs', 'long_desc')) { - my ($total) = $dbh->selectrow_array("SELECT COUNT(*) FROM bugs"); - - print "Populating new long_desc table. This is slow. There are", - " $total\nbugs to process; a line of dots will be printed", - " for each 50.\n\n"; - local $| = 1; - - # On MySQL, longdescs doesn't benefit from transactions, but this - # doesn't hurt. - $dbh->bz_start_transaction(); - - $dbh->do('DELETE FROM longdescs'); - - my $sth = $dbh->prepare("SELECT bug_id, creation_ts, reporter, - long_desc FROM bugs ORDER BY bug_id"); - $sth->execute(); - my $count = 0; - while (my ($id, $createtime, $reporterid, $desc) = - $sth->fetchrow_array()) + my $dbh = Bugzilla->dbh; + + # 2000-01-20 Added a new "longdescs" table, which is supposed to have + # all the long descriptions in it, replacing the old long_desc field + # in the bugs table. The below hideous code populates this new table + # with things from the old field, with ugly parsing and heuristics. + + if ($dbh->bz_column_info('bugs', 'long_desc')) { + my ($total) = $dbh->selectrow_array("SELECT COUNT(*) FROM bugs"); + + print "Populating new long_desc table. This is slow. There are", + " $total\nbugs to process; a line of dots will be printed", + " for each 50.\n\n"; + local $| = 1; + + # On MySQL, longdescs doesn't benefit from transactions, but this + # doesn't hurt. + $dbh->bz_start_transaction(); + + $dbh->do('DELETE FROM longdescs'); + + my $sth = $dbh->prepare( + "SELECT bug_id, creation_ts, reporter, + long_desc FROM bugs ORDER BY bug_id" + ); + $sth->execute(); + my $count = 0; + while (my ($id, $createtime, $reporterid, $desc) = $sth->fetchrow_array()) { + $count++; + indicate_progress({total => $total, current => $count}); + $desc =~ s/\r//g; + my $who = $reporterid; + my $when = str2time($createtime); + my $buffer = ""; + foreach my $line (split(/\n/, $desc)) { + $line =~ s/\s+$//g; # Trim trailing whitespace. + if ($line =~ /^------- Additional Comments From ([^\s]+)\s+(\d.+\d)\s+-------$/) { - $count++; - indicate_progress({ total => $total, current => $count }); - $desc =~ s/\r//g; - my $who = $reporterid; - my $when = str2time($createtime); - my $buffer = ""; - foreach my $line (split(/\n/, $desc)) { - $line =~ s/\s+$//g; # Trim trailing whitespace. - if ($line =~ /^------- Additional Comments From ([^\s]+)\s+(\d.+\d)\s+-------$/) - { - my $name = $1; - my $date = str2time($2); - # Oy, what a hack. The creation time is accurate to the - # second. But the long text only contains things accurate - # to the And so, if someone makes a comment within a - # minute of the original bug creation, then the comment can - # come *before* the bug creation. So, we add 59 seconds to - # the time of all comments, so that they are always - # considered to have happened at the *end* of the given - # minute, not the beginning. - $date += 59; - if ($date >= $when) { - _write_one_longdesc($id, $who, $when, $buffer); - $buffer = ""; - $when = $date; - my $s2 = $dbh->prepare("SELECT userid FROM profiles " . - "WHERE login_name = ?"); - $s2->execute($name); - ($who) = ($s2->fetchrow_array()); - - if (!$who) { - # This username doesn't exist. Maybe someone - # renamed them or something. Invent a new profile - # entry disabled, just to represent them. - $dbh->do("INSERT INTO profiles (login_name, + my $name = $1; + my $date = str2time($2); + + # Oy, what a hack. The creation time is accurate to the + # second. But the long text only contains things accurate + # to the And so, if someone makes a comment within a + # minute of the original bug creation, then the comment can + # come *before* the bug creation. So, we add 59 seconds to + # the time of all comments, so that they are always + # considered to have happened at the *end* of the given + # minute, not the beginning. + $date += 59; + if ($date >= $when) { + _write_one_longdesc($id, $who, $when, $buffer); + $buffer = ""; + $when = $date; + my $s2 = $dbh->prepare("SELECT userid FROM profiles " . "WHERE login_name = ?"); + $s2->execute($name); + ($who) = ($s2->fetchrow_array()); + + if (!$who) { + + # This username doesn't exist. Maybe someone + # renamed them or something. Invent a new profile + # entry disabled, just to represent them. + $dbh->do( + "INSERT INTO profiles (login_name, cryptpassword, disabledtext) VALUES (?,?,?)", undef, $name, '*', - "Account created only to maintain" - . " database integrity"); - $who = $dbh->bz_last_key('profiles', 'userid'); - } - next; - } - } - $buffer .= $line . "\n"; + "Account created only to maintain" . " database integrity" + ); + $who = $dbh->bz_last_key('profiles', 'userid'); } - _write_one_longdesc($id, $who, $when, $buffer); - } # while loop + next; + } + } + $buffer .= $line . "\n"; + } + _write_one_longdesc($id, $who, $when, $buffer); + } # while loop - print "\n\n"; - $dbh->bz_drop_column('bugs', 'long_desc'); - $dbh->bz_commit_transaction(); - } # main if + print "\n\n"; + $dbh->bz_drop_column('bugs', 'long_desc'); + $dbh->bz_commit_transaction(); + } # main if } sub _update_bugs_activity_field_to_fieldid { - my $dbh = Bugzilla->dbh; + my $dbh = Bugzilla->dbh; - # 2000-01-18 Added a new table fielddefs that records information about the - # different fields we keep an activity log on. The bugs_activity table - # now has a pointer into that table instead of recording the name directly. - if ($dbh->bz_column_info('bugs_activity', 'field')) { - $dbh->bz_add_column('bugs_activity', 'fieldid', - {TYPE => 'INT3', NOTNULL => 1}, 0); + # 2000-01-18 Added a new table fielddefs that records information about the + # different fields we keep an activity log on. The bugs_activity table + # now has a pointer into that table instead of recording the name directly. + if ($dbh->bz_column_info('bugs_activity', 'field')) { + $dbh->bz_add_column('bugs_activity', 'fieldid', {TYPE => 'INT3', NOTNULL => 1}, + 0); - $dbh->bz_add_index('bugs_activity', 'bugs_activity_fieldid_idx', - [qw(fieldid)]); - print "Populating new bugs_activity.fieldid field...\n"; + $dbh->bz_add_index('bugs_activity', 'bugs_activity_fieldid_idx', [qw(fieldid)]); + print "Populating new bugs_activity.fieldid field...\n"; - $dbh->bz_start_transaction(); + $dbh->bz_start_transaction(); - my $ids = $dbh->selectall_arrayref( - 'SELECT DISTINCT fielddefs.id, bugs_activity.field + my $ids = $dbh->selectall_arrayref( + 'SELECT DISTINCT fielddefs.id, bugs_activity.field FROM bugs_activity LEFT JOIN fielddefs - ON bugs_activity.field = fielddefs.name', {Slice=>{}}); - - foreach my $item (@$ids) { - my $id = $item->{id}; - my $field = $item->{field}; - # If the id is NULL - if (!$id) { - $dbh->do("INSERT INTO fielddefs (name, description) VALUES " . - "(?, ?)", undef, $field, $field); - $id = $dbh->bz_last_key('fielddefs', 'id'); - } - $dbh->do("UPDATE bugs_activity SET fieldid = ? WHERE field = ?", - undef, $id, $field); - } - $dbh->bz_commit_transaction(); + ON bugs_activity.field = fielddefs.name', {Slice => {}} + ); - $dbh->bz_drop_column('bugs_activity', 'field'); + foreach my $item (@$ids) { + my $id = $item->{id}; + my $field = $item->{field}; + + # If the id is NULL + if (!$id) { + $dbh->do("INSERT INTO fielddefs (name, description) VALUES " . "(?, ?)", + undef, $field, $field); + $id = $dbh->bz_last_key('fielddefs', 'id'); + } + $dbh->do("UPDATE bugs_activity SET fieldid = ? WHERE field = ?", + undef, $id, $field); } + $dbh->bz_commit_transaction(); + + $dbh->bz_drop_column('bugs_activity', 'field'); + } } sub _add_unique_login_name_index_to_profiles { - my $dbh = Bugzilla->dbh; - - # 2000-01-22 The "login_name" field in the "profiles" table was not - # declared to be unique. Sure enough, somehow, I got 22 duplicated entries - # in my database. This code detects that, cleans up the duplicates, and - # then tweaks the table to declare the field to be unique. What a pain. - if (!$dbh->bz_index_info('profiles', 'profiles_login_name_idx') - || !$dbh->bz_index_info('profiles', 'profiles_login_name_idx')->{TYPE}) - { - print "Searching for duplicate entries in the profiles table...\n"; - while (1) { - # This code is weird in that it loops around and keeps doing this - # select again. That's because I'm paranoid about deleting entries - # out from under us in the profiles table. Things get weird if - # there are *three* or more entries for the same user... - my $sth = $dbh->prepare("SELECT p1.userid, p2.userid, p1.login_name + my $dbh = Bugzilla->dbh; + + # 2000-01-22 The "login_name" field in the "profiles" table was not + # declared to be unique. Sure enough, somehow, I got 22 duplicated entries + # in my database. This code detects that, cleans up the duplicates, and + # then tweaks the table to declare the field to be unique. What a pain. + if ( !$dbh->bz_index_info('profiles', 'profiles_login_name_idx') + || !$dbh->bz_index_info('profiles', 'profiles_login_name_idx')->{TYPE}) + { + print "Searching for duplicate entries in the profiles table...\n"; + while (1) { + + # This code is weird in that it loops around and keeps doing this + # select again. That's because I'm paranoid about deleting entries + # out from under us in the profiles table. Things get weird if + # there are *three* or more entries for the same user... + my $sth = $dbh->prepare( + "SELECT p1.userid, p2.userid, p1.login_name FROM profiles AS p1, profiles AS p2 WHERE p1.userid < p2.userid AND p1.login_name = p2.login_name - ORDER BY p1.login_name"); - $sth->execute(); - my ($u1, $u2, $n) = ($sth->fetchrow_array); - last if !$u1; - - print "Both $u1 & $u2 are ids for $n! Merging $u2 into $u1...\n"; - foreach my $i (["bugs", "reporter"], - ["bugs", "assigned_to"], - ["bugs", "qa_contact"], - ["attachments", "submitter_id"], - ["bugs_activity", "who"], - ["cc", "who"], - ["votes", "who"], - ["longdescs", "who"]) { - my ($table, $field) = (@$i); - if ($dbh->bz_table_info($table)) { - print " Updating $table.$field...\n"; - $dbh->do("UPDATE $table SET $field = $u1 " . - "WHERE $field = $u2"); - } - } - $dbh->do("DELETE FROM profiles WHERE userid = $u2"); + ORDER BY p1.login_name" + ); + $sth->execute(); + my ($u1, $u2, $n) = ($sth->fetchrow_array); + last if !$u1; + + print "Both $u1 & $u2 are ids for $n! Merging $u2 into $u1...\n"; + foreach my $i ( + ["bugs", "reporter"], + ["bugs", "assigned_to"], + ["bugs", "qa_contact"], + ["attachments", "submitter_id"], + ["bugs_activity", "who"], + ["cc", "who"], + ["votes", "who"], + ["longdescs", "who"] + ) + { + my ($table, $field) = (@$i); + if ($dbh->bz_table_info($table)) { + print " Updating $table.$field...\n"; + $dbh->do("UPDATE $table SET $field = $u1 " . "WHERE $field = $u2"); } - print "OK, changing index type to prevent duplicates in the", - " future...\n"; - - $dbh->bz_drop_index('profiles', 'profiles_login_name_idx'); - $dbh->bz_add_index('profiles', 'profiles_login_name_idx', - {TYPE => 'UNIQUE', FIELDS => [qw(login_name)]}); + } + $dbh->do("DELETE FROM profiles WHERE userid = $u2"); } + print "OK, changing index type to prevent duplicates in the", " future...\n"; + + $dbh->bz_drop_index('profiles', 'profiles_login_name_idx'); + $dbh->bz_add_index('profiles', 'profiles_login_name_idx', + {TYPE => 'UNIQUE', FIELDS => [qw(login_name)]}); + } } sub _update_component_user_fields_to_ids { - my $dbh = Bugzilla->dbh; - - # components.initialowner - my $comp_init_owner = $dbh->bz_column_info('components', 'initialowner'); - if ($comp_init_owner && $comp_init_owner->{TYPE} eq 'TINYTEXT') { - my $sth = $dbh->prepare("SELECT program, value, initialowner - FROM components"); - $sth->execute(); - while (my ($program, $value, $initialowner) = $sth->fetchrow_array()) { - my ($id) = $dbh->selectrow_array( - "SELECT userid FROM profiles WHERE login_name = ?", - undef, $initialowner); - - unless (defined $id) { - print "Warning: You have an invalid default assignee", - " '$initialowner'\n in component '$value' of program", - " '$program'!\n"; - $id = 0; - } + my $dbh = Bugzilla->dbh; - $dbh->do("UPDATE components SET initialowner = ? - WHERE program = ? AND value = ?", undef, - $id, $program, $value); - } - $dbh->bz_alter_column('components','initialowner',{TYPE => 'INT3'}); + # components.initialowner + my $comp_init_owner = $dbh->bz_column_info('components', 'initialowner'); + if ($comp_init_owner && $comp_init_owner->{TYPE} eq 'TINYTEXT') { + my $sth = $dbh->prepare( + "SELECT program, value, initialowner + FROM components" + ); + $sth->execute(); + while (my ($program, $value, $initialowner) = $sth->fetchrow_array()) { + my ($id) + = $dbh->selectrow_array("SELECT userid FROM profiles WHERE login_name = ?", + undef, $initialowner); + + unless (defined $id) { + print "Warning: You have an invalid default assignee", + " '$initialowner'\n in component '$value' of program", " '$program'!\n"; + $id = 0; + } + + $dbh->do( + "UPDATE components SET initialowner = ? + WHERE program = ? AND value = ?", undef, $id, $program, $value + ); } + $dbh->bz_alter_column('components', 'initialowner', {TYPE => 'INT3'}); + } - # components.initialqacontact - my $comp_init_qa = $dbh->bz_column_info('components', 'initialqacontact'); - if ($comp_init_qa && $comp_init_qa->{TYPE} eq 'TINYTEXT') { - my $sth = $dbh->prepare("SELECT program, value, initialqacontact - FROM components"); - $sth->execute(); - while (my ($program, $value, $initialqacontact) = - $sth->fetchrow_array()) - { - my ($id) = $dbh->selectrow_array( - "SELECT userid FROM profiles WHERE login_name = ?", - undef, $initialqacontact); - - unless (defined $id) { - if ($initialqacontact) { - print "Warning: You have an invalid default QA contact", - " $initialqacontact' in program '$program',", - " component '$value'!\n"; - } - $id = 0; - } - - $dbh->do("UPDATE components SET initialqacontact = ? - WHERE program = ? AND value = ?", undef, - $id, $program, $value); + # components.initialqacontact + my $comp_init_qa = $dbh->bz_column_info('components', 'initialqacontact'); + if ($comp_init_qa && $comp_init_qa->{TYPE} eq 'TINYTEXT') { + my $sth = $dbh->prepare( + "SELECT program, value, initialqacontact + FROM components" + ); + $sth->execute(); + while (my ($program, $value, $initialqacontact) = $sth->fetchrow_array()) { + my ($id) + = $dbh->selectrow_array("SELECT userid FROM profiles WHERE login_name = ?", + undef, $initialqacontact); + + unless (defined $id) { + if ($initialqacontact) { + print "Warning: You have an invalid default QA contact", + " $initialqacontact' in program '$program',", " component '$value'!\n"; } + $id = 0; + } - $dbh->bz_alter_column('components','initialqacontact',{TYPE => 'INT3'}); + $dbh->do( + "UPDATE components SET initialqacontact = ? + WHERE program = ? AND value = ?", undef, $id, $program, $value + ); } + + $dbh->bz_alter_column('components', 'initialqacontact', {TYPE => 'INT3'}); + } } sub _populate_milestones_table { - my $dbh = Bugzilla->dbh; - # 2000-03-21 Adding a table for target milestones to - # database - matthew@zeroknowledge.com - # If the milestones table is empty, and we're still back in a Bugzilla - # that has a bugs.product field, that means that we just created - # the milestones table and it needs to be populated. - my $milestones_exist = $dbh->selectrow_array( - "SELECT DISTINCT 1 FROM milestones"); - if (!$milestones_exist && $dbh->bz_column_info('bugs', 'product')) { - print "Replacing blank milestones...\n"; - - $dbh->do("UPDATE bugs + my $dbh = Bugzilla->dbh; + + # 2000-03-21 Adding a table for target milestones to + # database - matthew@zeroknowledge.com + # If the milestones table is empty, and we're still back in a Bugzilla + # that has a bugs.product field, that means that we just created + # the milestones table and it needs to be populated. + my $milestones_exist + = $dbh->selectrow_array("SELECT DISTINCT 1 FROM milestones"); + if (!$milestones_exist && $dbh->bz_column_info('bugs', 'product')) { + print "Replacing blank milestones...\n"; + + $dbh->do( + "UPDATE bugs SET target_milestone = '---' - WHERE target_milestone = ' '"); - - # If we are upgrading from 2.8 or earlier, we will have *created* - # the milestones table with a product_id field, but Bugzilla expects - # it to have a "product" field. So we change the field backward so - # other code can run. The change will be reversed later in checksetup. - if ($dbh->bz_column_info('milestones', 'product_id')) { - # Dropping the column leaves us with a milestones_product_id_idx - # index that is only on the "value" column. We need to drop the - # whole index so that it can be correctly re-created later. - $dbh->bz_drop_index('milestones', 'milestones_product_id_idx'); - $dbh->bz_drop_column('milestones', 'product_id'); - $dbh->bz_add_column('milestones', 'product', - {TYPE => 'varchar(64)', NOTNULL => 1}, ''); - } + WHERE target_milestone = ' '" + ); - # Populate the milestone table with all existing values in the database - my $sth = $dbh->prepare("SELECT DISTINCT target_milestone, product - FROM bugs"); - $sth->execute(); + # If we are upgrading from 2.8 or earlier, we will have *created* + # the milestones table with a product_id field, but Bugzilla expects + # it to have a "product" field. So we change the field backward so + # other code can run. The change will be reversed later in checksetup. + if ($dbh->bz_column_info('milestones', 'product_id')) { + + # Dropping the column leaves us with a milestones_product_id_idx + # index that is only on the "value" column. We need to drop the + # whole index so that it can be correctly re-created later. + $dbh->bz_drop_index('milestones', 'milestones_product_id_idx'); + $dbh->bz_drop_column('milestones', 'product_id'); + $dbh->bz_add_column('milestones', 'product', + {TYPE => 'varchar(64)', NOTNULL => 1}, ''); + } - print "Populating milestones table...\n"; + # Populate the milestone table with all existing values in the database + my $sth = $dbh->prepare( + "SELECT DISTINCT target_milestone, product + FROM bugs" + ); + $sth->execute(); - while (my ($value, $product) = $sth->fetchrow_array()) { - # check if the value already exists - my $sortkey = substr($value, 1); - if ($sortkey !~ /^\d+$/) { - $sortkey = 0; - } else { - $sortkey *= 10; - } - my $ms_exists = $dbh->selectrow_array( - "SELECT value FROM milestones - WHERE value = ? AND product = ?", undef, $value, $product); + print "Populating milestones table...\n"; - if (!$ms_exists) { - $dbh->do("INSERT INTO milestones(value, product, sortkey) - VALUES (?,?,?)", undef, $value, $product, $sortkey); - } - } + while (my ($value, $product) = $sth->fetchrow_array()) { + + # check if the value already exists + my $sortkey = substr($value, 1); + if ($sortkey !~ /^\d+$/) { + $sortkey = 0; + } + else { + $sortkey *= 10; + } + my $ms_exists = $dbh->selectrow_array( + "SELECT value FROM milestones + WHERE value = ? AND product = ?", undef, $value, $product + ); + + if (!$ms_exists) { + $dbh->do( + "INSERT INTO milestones(value, product, sortkey) + VALUES (?,?,?)", undef, $value, $product, $sortkey + ); + } } + } } sub _add_products_defaultmilestone { - my $dbh = Bugzilla->dbh; - - # 2000-03-23 Added a defaultmilestone field to the products table, so that - # we know which milestone to initially assign bugs to. - if (!$dbh->bz_column_info('products', 'defaultmilestone')) { - $dbh->bz_add_column('products', 'defaultmilestone', - {TYPE => 'varchar(20)', NOTNULL => 1, DEFAULT => "'---'"}); - my $sth = $dbh->prepare( - "SELECT product, defaultmilestone FROM products"); - $sth->execute(); - while (my ($product, $default_ms) = $sth->fetchrow_array()) { - my $exists = $dbh->selectrow_array( - "SELECT value FROM milestones - WHERE value = ? AND product = ?", - undef, $default_ms, $product); - if (!$exists) { - $dbh->do("INSERT INTO milestones(value, product) " . - "VALUES (?, ?)", undef, $default_ms, $product); - } - } + my $dbh = Bugzilla->dbh; + + # 2000-03-23 Added a defaultmilestone field to the products table, so that + # we know which milestone to initially assign bugs to. + if (!$dbh->bz_column_info('products', 'defaultmilestone')) { + $dbh->bz_add_column('products', 'defaultmilestone', + {TYPE => 'varchar(20)', NOTNULL => 1, DEFAULT => "'---'"}); + my $sth = $dbh->prepare("SELECT product, defaultmilestone FROM products"); + $sth->execute(); + while (my ($product, $default_ms) = $sth->fetchrow_array()) { + my $exists = $dbh->selectrow_array( + "SELECT value FROM milestones + WHERE value = ? AND product = ?", undef, $default_ms, $product + ); + if (!$exists) { + $dbh->do("INSERT INTO milestones(value, product) " . "VALUES (?, ?)", + undef, $default_ms, $product); + } } + } } sub _copy_from_comments_to_longdescs { - my $dbh = Bugzilla->dbh; - # 2000-11-27 For Bugzilla 2.5 and later. Copy data from 'comments' to - # 'longdescs' - the new name of the comments table. - if ($dbh->bz_table_info('comments')) { - print "Copying data from 'comments' to 'longdescs'...\n"; - my $quoted_when = $dbh->quote_identifier('when'); - $dbh->do("INSERT INTO longdescs (bug_when, bug_id, who, thetext) + my $dbh = Bugzilla->dbh; + + # 2000-11-27 For Bugzilla 2.5 and later. Copy data from 'comments' to + # 'longdescs' - the new name of the comments table. + if ($dbh->bz_table_info('comments')) { + print "Copying data from 'comments' to 'longdescs'...\n"; + my $quoted_when = $dbh->quote_identifier('when'); + $dbh->do( + "INSERT INTO longdescs (bug_when, bug_id, who, thetext) SELECT $quoted_when, bug_id, who, comment - FROM comments"); - $dbh->bz_drop_table("comments"); - } + FROM comments" + ); + $dbh->bz_drop_table("comments"); + } } sub _populate_duplicates_table { - my $dbh = Bugzilla->dbh; - # 2000-07-15 Added duplicates table so Bugzilla tracks duplicates in a - # better way than it used to. This code searches the comments to populate - # the table initially. It's executed if the table is empty; if it's - # empty because there are no dupes (as opposed to having just created - # the table) it won't have any effect anyway, so it doesn't matter. - my ($dups_exist) = $dbh->selectrow_array( - "SELECT DISTINCT 1 FROM duplicates"); - # We also check against a schema change that happened later. - if (!$dups_exist && !$dbh->bz_column_info('groups', 'isactive')) { - # populate table - print "Populating duplicates table from comments...\n"; - - my $sth = $dbh->prepare( - "SELECT longdescs.bug_id, thetext + my $dbh = Bugzilla->dbh; + + # 2000-07-15 Added duplicates table so Bugzilla tracks duplicates in a + # better way than it used to. This code searches the comments to populate + # the table initially. It's executed if the table is empty; if it's + # empty because there are no dupes (as opposed to having just created + # the table) it won't have any effect anyway, so it doesn't matter. + my ($dups_exist) = $dbh->selectrow_array("SELECT DISTINCT 1 FROM duplicates"); + + # We also check against a schema change that happened later. + if (!$dups_exist && !$dbh->bz_column_info('groups', 'isactive')) { + + # populate table + print "Populating duplicates table from comments...\n"; + + my $sth = $dbh->prepare( + "SELECT longdescs.bug_id, thetext FROM longdescs LEFT JOIN bugs ON longdescs.bug_id = bugs.bug_id - WHERE (" . $dbh->sql_regexp("thetext", - "'[.*.]{3} This bug has been marked as a duplicate" - . " of [[:digit:]]+ [.*.]{3}'") - . ") + WHERE (" + . $dbh->sql_regexp("thetext", + "'[.*.]{3} This bug has been marked as a duplicate" + . " of [[:digit:]]+ [.*.]{3}'") + . ") AND resolution = 'DUPLICATE' - ORDER BY longdescs.bug_when"); - $sth->execute(); - - my (%dupes, $key); - # Because of the way hashes work, this loop removes all but the - # last dupe resolution found for a given bug. - while (my ($dupe, $dupe_of) = $sth->fetchrow_array()) { - $dupes{$dupe} = $dupe_of; - } + ORDER BY longdescs.bug_when" + ); + $sth->execute(); - foreach $key (keys(%dupes)){ - $dupes{$key} =~ /^.*\*\*\* This bug has been marked as a duplicate of (\d+) \*\*\*$/ms; - $dupes{$key} = $1; - $dbh->do("INSERT INTO duplicates VALUES(?, ?)", undef, - $dupes{$key}, $key); - # BugItsADupeOf Dupe - } + my (%dupes, $key); + + # Because of the way hashes work, this loop removes all but the + # last dupe resolution found for a given bug. + while (my ($dupe, $dupe_of) = $sth->fetchrow_array()) { + $dupes{$dupe} = $dupe_of; } + + foreach $key (keys(%dupes)) { + $dupes{$key} + =~ /^.*\*\*\* This bug has been marked as a duplicate of (\d+) \*\*\*$/ms; + $dupes{$key} = $1; + $dbh->do("INSERT INTO duplicates VALUES(?, ?)", undef, $dupes{$key}, $key); + + # BugItsADupeOf Dupe + } + } } sub _recrypt_plaintext_passwords { - my $dbh = Bugzilla->dbh; - # 2001-06-12; myk@mozilla.org; bugs 74032, 77473: - # Recrypt passwords using Perl &crypt instead of the mysql equivalent - # and delete plaintext passwords from the database. - if ($dbh->bz_column_info('profiles', 'password')) { + my $dbh = Bugzilla->dbh; + + # 2001-06-12; myk@mozilla.org; bugs 74032, 77473: + # Recrypt passwords using Perl &crypt instead of the mysql equivalent + # and delete plaintext passwords from the database. + if ($dbh->bz_column_info('profiles', 'password')) { - print <selectrow_array('SELECT COUNT(*) FROM profiles'); - my $sth = $dbh->prepare("SELECT userid, password FROM profiles"); - $sth->execute(); - - my $i = 1; + # Re-crypt everyone's password. + my $total = $dbh->selectrow_array('SELECT COUNT(*) FROM profiles'); + my $sth = $dbh->prepare("SELECT userid, password FROM profiles"); + $sth->execute(); - print "Fixing passwords...\n"; - while (my ($userid, $password) = $sth->fetchrow_array()) { - my $cryptpassword = $dbh->quote(bz_crypt($password)); - $dbh->do("UPDATE profiles " . - "SET cryptpassword = $cryptpassword " . - "WHERE userid = $userid"); - indicate_progress({ total => $total, current => $i, every => 10 }); - } - print "\n"; + my $i = 1; - # Drop the plaintext password field. - $dbh->bz_drop_column('profiles', 'password'); + print "Fixing passwords...\n"; + while (my ($userid, $password) = $sth->fetchrow_array()) { + my $cryptpassword = $dbh->quote(bz_crypt($password)); + $dbh->do("UPDATE profiles " + . "SET cryptpassword = $cryptpassword " + . "WHERE userid = $userid"); + indicate_progress({total => $total, current => $i, every => 10}); } + print "\n"; + + # Drop the plaintext password field. + $dbh->bz_drop_column('profiles', 'password'); + } } sub _update_bugs_activity_to_only_record_changes { - my $dbh = Bugzilla->dbh; - # 2001-07-20 jake@bugzilla.org - Change bugs_activity to only record changes - # http://bugzilla.mozilla.org/show_bug.cgi?id=55161 - if ($dbh->bz_column_info('bugs_activity', 'oldvalue')) { - $dbh->bz_add_column("bugs_activity", "removed", {TYPE => "TINYTEXT"}); - $dbh->bz_add_column("bugs_activity", "added", {TYPE => "TINYTEXT"}); - - # Need to get field id's for the fields that have multiple values - my @multi; - foreach my $f ("cc", "dependson", "blocked", "keywords") { - my $sth = $dbh->prepare("SELECT id " . - "FROM fielddefs " . - "WHERE name = '$f'"); - $sth->execute(); - my ($fid) = $sth->fetchrow_array(); - push (@multi, $fid); + my $dbh = Bugzilla->dbh; + + # 2001-07-20 jake@bugzilla.org - Change bugs_activity to only record changes + # http://bugzilla.mozilla.org/show_bug.cgi?id=55161 + if ($dbh->bz_column_info('bugs_activity', 'oldvalue')) { + $dbh->bz_add_column("bugs_activity", "removed", {TYPE => "TINYTEXT"}); + $dbh->bz_add_column("bugs_activity", "added", {TYPE => "TINYTEXT"}); + + # Need to get field id's for the fields that have multiple values + my @multi; + foreach my $f ("cc", "dependson", "blocked", "keywords") { + my $sth = $dbh->prepare("SELECT id " . "FROM fielddefs " . "WHERE name = '$f'"); + $sth->execute(); + my ($fid) = $sth->fetchrow_array(); + push(@multi, $fid); + } + + # Now we need to process the bugs_activity table and reformat the data + print "Fixing activity log...\n"; + my $total = $dbh->selectrow_array('SELECT COUNT(*) FROM bugs_activity'); + my $sth = $dbh->prepare( + "SELECT bug_id, who, bug_when, fieldid, + oldvalue, newvalue FROM bugs_activity" + ); + $sth->execute; + my $i = 0; + while (my ($bug_id, $who, $bug_when, $fieldid, $oldvalue, $newvalue) + = $sth->fetchrow_array()) + { + $i++; + indicate_progress({total => $total, current => $i, every => 10}); + + # Make sure (old|new)value isn't null (to suppress warnings) + $oldvalue ||= ""; + $newvalue ||= ""; + my ($added, $removed) = ""; + if (grep ($_ eq $fieldid, @multi)) { + $oldvalue =~ s/[\s,]+/ /g; + $newvalue =~ s/[\s,]+/ /g; + my @old = split(" ", $oldvalue); + my @new = split(" ", $newvalue); + my (@add, @remove) = (); + + # Find values that were "added" + foreach my $value (@new) { + if (!grep ($_ eq $value, @old)) { + push(@add, $value); + } } - # Now we need to process the bugs_activity table and reformat the data - print "Fixing activity log...\n"; - my $total = $dbh->selectrow_array('SELECT COUNT(*) FROM bugs_activity'); - my $sth = $dbh->prepare("SELECT bug_id, who, bug_when, fieldid, - oldvalue, newvalue FROM bugs_activity"); - $sth->execute; - my $i = 0; - while (my ($bug_id, $who, $bug_when, $fieldid, $oldvalue, $newvalue) - = $sth->fetchrow_array()) - { - $i++; - indicate_progress({ total => $total, current => $i, every => 10 }); - # Make sure (old|new)value isn't null (to suppress warnings) - $oldvalue ||= ""; - $newvalue ||= ""; - my ($added, $removed) = ""; - if (grep ($_ eq $fieldid, @multi)) { - $oldvalue =~ s/[\s,]+/ /g; - $newvalue =~ s/[\s,]+/ /g; - my @old = split(" ", $oldvalue); - my @new = split(" ", $newvalue); - my (@add, @remove) = (); - # Find values that were "added" - foreach my $value(@new) { - if (! grep ($_ eq $value, @old)) { - push (@add, $value); - } - } - # Find values that were removed - foreach my $value(@old) { - if (! grep ($_ eq $value, @new)) { - push (@remove, $value); - } - } - $added = join (", ", @add); - $removed = join (", ", @remove); - # If we can't determine what changed, put a ? in both fields - unless ($added || $removed) { - $added = "?"; - $removed = "?"; - } - # If the original field (old|new)value was full, then this - # could be incomplete data. - if (length($oldvalue) == 255 || length($newvalue) == 255) { - $added = "? $added"; - $removed = "? $removed"; - } - } else { - $removed = $oldvalue; - $added = $newvalue; - } - $added = $dbh->quote($added); - $removed = $dbh->quote($removed); - $dbh->do("UPDATE bugs_activity + # Find values that were removed + foreach my $value (@old) { + if (!grep ($_ eq $value, @new)) { + push(@remove, $value); + } + } + $added = join(", ", @add); + $removed = join(", ", @remove); + + # If we can't determine what changed, put a ? in both fields + unless ($added || $removed) { + $added = "?"; + $removed = "?"; + } + + # If the original field (old|new)value was full, then this + # could be incomplete data. + if (length($oldvalue) == 255 || length($newvalue) == 255) { + $added = "? $added"; + $removed = "? $removed"; + } + } + else { + $removed = $oldvalue; + $added = $newvalue; + } + $added = $dbh->quote($added); + $removed = $dbh->quote($removed); + $dbh->do( + "UPDATE bugs_activity SET removed = $removed, added = $added WHERE bug_id = $bug_id AND who = $who AND bug_when = '$bug_when' - AND fieldid = $fieldid"); - } - print "\n"; - $dbh->bz_drop_column("bugs_activity", "oldvalue"); - $dbh->bz_drop_column("bugs_activity", "newvalue"); + AND fieldid = $fieldid" + ); } + print "\n"; + $dbh->bz_drop_column("bugs_activity", "oldvalue"); + $dbh->bz_drop_column("bugs_activity", "newvalue"); + } } sub _delete_logincookies_cryptpassword_and_handle_invalid_cookies { - my $dbh = Bugzilla->dbh; - # 2002-02-04 bbaetz@student.usyd.edu.au bug 95732 - # Remove logincookies.cryptpassword, and delete entries which become - # invalid - if ($dbh->bz_column_info("logincookies", "cryptpassword")) { - # We need to delete any cookies which are invalid before dropping the - # column - print "Removing invalid login cookies...\n"; - - # mysql doesn't support DELETE with multi-table queries, so we have - # to iterate - my $sth = $dbh->prepare("SELECT cookie FROM logincookies, profiles " . - "WHERE logincookies.cryptpassword != " . - "profiles.cryptpassword AND " . - "logincookies.userid = profiles.userid"); - $sth->execute(); - while (my ($cookie) = $sth->fetchrow_array()) { - $dbh->do("DELETE FROM logincookies WHERE cookie = $cookie"); - } - - $dbh->bz_drop_column("logincookies", "cryptpassword"); + my $dbh = Bugzilla->dbh; + + # 2002-02-04 bbaetz@student.usyd.edu.au bug 95732 + # Remove logincookies.cryptpassword, and delete entries which become + # invalid + if ($dbh->bz_column_info("logincookies", "cryptpassword")) { + + # We need to delete any cookies which are invalid before dropping the + # column + print "Removing invalid login cookies...\n"; + + # mysql doesn't support DELETE with multi-table queries, so we have + # to iterate + my $sth + = $dbh->prepare("SELECT cookie FROM logincookies, profiles " + . "WHERE logincookies.cryptpassword != " + . "profiles.cryptpassword AND " + . "logincookies.userid = profiles.userid"); + $sth->execute(); + while (my ($cookie) = $sth->fetchrow_array()) { + $dbh->do("DELETE FROM logincookies WHERE cookie = $cookie"); } + + $dbh->bz_drop_column("logincookies", "cryptpassword"); + } } sub _use_ip_instead_of_hostname_in_logincookies { - my $dbh = Bugzilla->dbh; - - # 2002-03-15 bbaetz@student.usyd.edu.au - bug 129466 - # 2002-05-13 preed@sigkill.com - bug 129446 patch backported to the - # BUGZILLA-2_14_1-BRANCH as a security blocker for the 2.14.2 release - # - # Use the ip, not the hostname, in the logincookies table - if ($dbh->bz_column_info("logincookies", "hostname")) { - print "Clearing the logincookies table...\n"; - # We've changed what we match against, so all entries are now invalid - $dbh->do("DELETE FROM logincookies"); - - # Now update the logincookies schema - $dbh->bz_drop_column("logincookies", "hostname"); - $dbh->bz_add_column("logincookies", "ipaddr", - {TYPE => 'varchar(40)'}); - } + my $dbh = Bugzilla->dbh; + + # 2002-03-15 bbaetz@student.usyd.edu.au - bug 129466 + # 2002-05-13 preed@sigkill.com - bug 129446 patch backported to the + # BUGZILLA-2_14_1-BRANCH as a security blocker for the 2.14.2 release + # + # Use the ip, not the hostname, in the logincookies table + if ($dbh->bz_column_info("logincookies", "hostname")) { + print "Clearing the logincookies table...\n"; + + # We've changed what we match against, so all entries are now invalid + $dbh->do("DELETE FROM logincookies"); + + # Now update the logincookies schema + $dbh->bz_drop_column("logincookies", "hostname"); + $dbh->bz_add_column("logincookies", "ipaddr", {TYPE => 'varchar(40)'}); + } } sub _move_quips_into_db { - my $dbh = Bugzilla->dbh; - my $datadir = bz_locations->{'datadir'}; - # 2002-07-15 davef@tetsubo.com - bug 67950 - # Move quips to the db. - if (-e "$datadir/comments") { - print "Populating quips table from $datadir/comments...\n"; - my $comments = new IO::File("$datadir/comments", 'r') - || die "$datadir/comments: $!"; - $comments->untaint; - while (<$comments>) { - chomp; - $dbh->do("INSERT INTO quips (quip) VALUES (?)", undef, $_); - } - - print "\n", install_string('update_quips', { data => $datadir }), "\n"; - $comments->close; - rename("$datadir/comments", "$datadir/comments.bak") - || warn "Failed to rename: $!"; + my $dbh = Bugzilla->dbh; + my $datadir = bz_locations->{'datadir'}; + + # 2002-07-15 davef@tetsubo.com - bug 67950 + # Move quips to the db. + if (-e "$datadir/comments") { + print "Populating quips table from $datadir/comments...\n"; + my $comments = new IO::File("$datadir/comments", 'r') + || die "$datadir/comments: $!"; + $comments->untaint; + while (<$comments>) { + chomp; + $dbh->do("INSERT INTO quips (quip) VALUES (?)", undef, $_); } + + print "\n", install_string('update_quips', {data => $datadir}), "\n"; + $comments->close; + rename("$datadir/comments", "$datadir/comments.bak") + || warn "Failed to rename: $!"; + } } sub _use_ids_for_products_and_components { - my $dbh = Bugzilla->dbh; - # 2002-08-12 jake@bugzilla.org/bbaetz@student.usyd.edu.au - bug 43600 - # Use integer IDs for products and components. - if ($dbh->bz_column_info("products", "product")) { - print "Updating database to use product IDs.\n"; - - # First, we need to remove possible NULL entries - # NULLs may exist, but won't have been used, since all the uses of them - # are in NOT NULL fields in other tables - $dbh->do("DELETE FROM products WHERE product IS NULL"); - $dbh->do("DELETE FROM components WHERE value IS NULL"); - - $dbh->bz_add_column("products", "id", - {TYPE => 'SMALLSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); - $dbh->bz_add_column("components", "product_id", - {TYPE => 'INT2', NOTNULL => 1}, 0); - $dbh->bz_add_column("versions", "product_id", - {TYPE => 'INT2', NOTNULL => 1}, 0); - $dbh->bz_add_column("milestones", "product_id", - {TYPE => 'INT2', NOTNULL => 1}, 0); - $dbh->bz_add_column("bugs", "product_id", - {TYPE => 'INT2', NOTNULL => 1}, 0); - - # The attachstatusdefs table was added in version 2.15, but - # removed again in early 2.17. If it exists now, we still need - # to perform this change with product_id because the code later on - # which converts the attachment statuses to flags depends on it. - # But we need to avoid this if the user is upgrading from 2.14 - # or earlier (because it won't be there to convert). - if ($dbh->bz_table_info("attachstatusdefs")) { - $dbh->bz_add_column("attachstatusdefs", "product_id", - {TYPE => 'INT2', NOTNULL => 1}, 0); - } - - my %products; - my $sth = $dbh->prepare("SELECT id, product FROM products"); - $sth->execute; - while (my ($product_id, $product) = $sth->fetchrow_array()) { - if (exists $products{$product}) { - print "Ignoring duplicate product $product\n"; - $dbh->do("DELETE FROM products WHERE id = $product_id"); - next; - } - $products{$product} = 1; - $dbh->do("UPDATE components SET product_id = $product_id " . - "WHERE program = " . $dbh->quote($product)); - $dbh->do("UPDATE versions SET product_id = $product_id " . - "WHERE program = " . $dbh->quote($product)); - $dbh->do("UPDATE milestones SET product_id = $product_id " . - "WHERE product = " . $dbh->quote($product)); - $dbh->do("UPDATE bugs SET product_id = $product_id " . - "WHERE product = " . $dbh->quote($product)); - $dbh->do("UPDATE attachstatusdefs SET product_id = $product_id " . - "WHERE product = " . $dbh->quote($product)) - if $dbh->bz_table_info("attachstatusdefs"); - } - - print "Updating the database to use component IDs.\n"; + my $dbh = Bugzilla->dbh; + + # 2002-08-12 jake@bugzilla.org/bbaetz@student.usyd.edu.au - bug 43600 + # Use integer IDs for products and components. + if ($dbh->bz_column_info("products", "product")) { + print "Updating database to use product IDs.\n"; + + # First, we need to remove possible NULL entries + # NULLs may exist, but won't have been used, since all the uses of them + # are in NOT NULL fields in other tables + $dbh->do("DELETE FROM products WHERE product IS NULL"); + $dbh->do("DELETE FROM components WHERE value IS NULL"); + + $dbh->bz_add_column("products", "id", + {TYPE => 'SMALLSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); + $dbh->bz_add_column("components", "product_id", {TYPE => 'INT2', NOTNULL => 1}, + 0); + $dbh->bz_add_column("versions", "product_id", {TYPE => 'INT2', NOTNULL => 1}, + 0); + $dbh->bz_add_column("milestones", "product_id", {TYPE => 'INT2', NOTNULL => 1}, + 0); + $dbh->bz_add_column("bugs", "product_id", {TYPE => 'INT2', NOTNULL => 1}, 0); + + # The attachstatusdefs table was added in version 2.15, but + # removed again in early 2.17. If it exists now, we still need + # to perform this change with product_id because the code later on + # which converts the attachment statuses to flags depends on it. + # But we need to avoid this if the user is upgrading from 2.14 + # or earlier (because it won't be there to convert). + if ($dbh->bz_table_info("attachstatusdefs")) { + $dbh->bz_add_column("attachstatusdefs", "product_id", + {TYPE => 'INT2', NOTNULL => 1}, 0); + } - $dbh->bz_add_column("components", "id", - {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); - $dbh->bz_add_column("bugs", "component_id", - {TYPE => 'INT3', NOTNULL => 1}, 0); + my %products; + my $sth = $dbh->prepare("SELECT id, product FROM products"); + $sth->execute; + while (my ($product_id, $product) = $sth->fetchrow_array()) { + if (exists $products{$product}) { + print "Ignoring duplicate product $product\n"; + $dbh->do("DELETE FROM products WHERE id = $product_id"); + next; + } + $products{$product} = 1; + $dbh->do("UPDATE components SET product_id = $product_id " + . "WHERE program = " + . $dbh->quote($product)); + $dbh->do("UPDATE versions SET product_id = $product_id " + . "WHERE program = " + . $dbh->quote($product)); + $dbh->do("UPDATE milestones SET product_id = $product_id " + . "WHERE product = " + . $dbh->quote($product)); + $dbh->do("UPDATE bugs SET product_id = $product_id " + . "WHERE product = " + . $dbh->quote($product)); + $dbh->do("UPDATE attachstatusdefs SET product_id = $product_id " + . "WHERE product = " + . $dbh->quote($product)) + if $dbh->bz_table_info("attachstatusdefs"); + } - my %components; - $sth = $dbh->prepare("SELECT id, value, product_id FROM components"); - $sth->execute; - while (my ($component_id, $component, $product_id) - = $sth->fetchrow_array()) - { - if (exists $components{$component}) { - if (exists $components{$component}{$product_id}) { - print "Ignoring duplicate component $component for", - " product $product_id\n"; - $dbh->do("DELETE FROM components WHERE id = $component_id"); - next; - } - } else { - $components{$component} = {}; - } - $components{$component}{$product_id} = 1; - $dbh->do("UPDATE bugs SET component_id = $component_id " . - "WHERE component = " . $dbh->quote($component) . - " AND product_id = $product_id"); + print "Updating the database to use component IDs.\n"; + + $dbh->bz_add_column("components", "id", + {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); + $dbh->bz_add_column("bugs", "component_id", {TYPE => 'INT3', NOTNULL => 1}, 0); + + my %components; + $sth = $dbh->prepare("SELECT id, value, product_id FROM components"); + $sth->execute; + while (my ($component_id, $component, $product_id) = $sth->fetchrow_array()) { + if (exists $components{$component}) { + if (exists $components{$component}{$product_id}) { + print "Ignoring duplicate component $component for", " product $product_id\n"; + $dbh->do("DELETE FROM components WHERE id = $component_id"); + next; } - print "Fixing Indexes and Uniqueness.\n"; - $dbh->bz_drop_index('milestones', 'milestones_product_idx'); - - $dbh->bz_add_index('milestones', 'milestones_product_id_idx', - {TYPE => 'UNIQUE', FIELDS => [qw(product_id value)]}); - - $dbh->bz_drop_index('bugs', 'bugs_product_idx'); - $dbh->bz_add_index('bugs', 'bugs_product_id_idx', [qw(product_id)]); - $dbh->bz_drop_index('bugs', 'bugs_component_idx'); - $dbh->bz_add_index('bugs', 'bugs_component_id_idx', [qw(component_id)]); - - print "Removing, renaming, and retyping old product and", - " component fields.\n"; - $dbh->bz_drop_column("components", "program"); - $dbh->bz_drop_column("versions", "program"); - $dbh->bz_drop_column("milestones", "product"); - $dbh->bz_drop_column("bugs", "product"); - $dbh->bz_drop_column("bugs", "component"); - $dbh->bz_drop_column("attachstatusdefs", "product") - if $dbh->bz_table_info("attachstatusdefs"); - $dbh->bz_rename_column("products", "product", "name"); - $dbh->bz_alter_column("products", "name", - {TYPE => 'varchar(64)', NOTNULL => 1}); - $dbh->bz_rename_column("components", "value", "name"); - $dbh->bz_alter_column("components", "name", - {TYPE => 'varchar(64)', NOTNULL => 1}); - - print "Adding indexes for products and components tables.\n"; - $dbh->bz_add_index('products', 'products_name_idx', - {TYPE => 'UNIQUE', FIELDS => [qw(name)]}); - $dbh->bz_add_index('components', 'components_product_id_idx', - {TYPE => 'UNIQUE', FIELDS => [qw(product_id name)]}); - $dbh->bz_add_index('components', 'components_name_idx', [qw(name)]); + } + else { + $components{$component} = {}; + } + $components{$component}{$product_id} = 1; + $dbh->do("UPDATE bugs SET component_id = $component_id " + . "WHERE component = " + . $dbh->quote($component) + . " AND product_id = $product_id"); } + print "Fixing Indexes and Uniqueness.\n"; + $dbh->bz_drop_index('milestones', 'milestones_product_idx'); + + $dbh->bz_add_index('milestones', 'milestones_product_id_idx', + {TYPE => 'UNIQUE', FIELDS => [qw(product_id value)]}); + + $dbh->bz_drop_index('bugs', 'bugs_product_idx'); + $dbh->bz_add_index('bugs', 'bugs_product_id_idx', [qw(product_id)]); + $dbh->bz_drop_index('bugs', 'bugs_component_idx'); + $dbh->bz_add_index('bugs', 'bugs_component_id_idx', [qw(component_id)]); + + print "Removing, renaming, and retyping old product and", + " component fields.\n"; + $dbh->bz_drop_column("components", "program"); + $dbh->bz_drop_column("versions", "program"); + $dbh->bz_drop_column("milestones", "product"); + $dbh->bz_drop_column("bugs", "product"); + $dbh->bz_drop_column("bugs", "component"); + $dbh->bz_drop_column("attachstatusdefs", "product") + if $dbh->bz_table_info("attachstatusdefs"); + $dbh->bz_rename_column("products", "product", "name"); + $dbh->bz_alter_column("products", "name", + {TYPE => 'varchar(64)', NOTNULL => 1}); + $dbh->bz_rename_column("components", "value", "name"); + $dbh->bz_alter_column("components", "name", + {TYPE => 'varchar(64)', NOTNULL => 1}); + + print "Adding indexes for products and components tables.\n"; + $dbh->bz_add_index('products', 'products_name_idx', + {TYPE => 'UNIQUE', FIELDS => [qw(name)]}); + $dbh->bz_add_index('components', 'components_product_id_idx', + {TYPE => 'UNIQUE', FIELDS => [qw(product_id name)]}); + $dbh->bz_add_index('components', 'components_name_idx', [qw(name)]); + } } # Helper for the below function. @@ -1519,1211 +1569,1341 @@ sub _use_ids_for_products_and_components { # group names, _list_bits is used to fill in a list of references # to groupset bits for groups that no longer exist. sub _list_bits { - my ($num) = @_; - my $dbh = Bugzilla->dbh; - my @res; - my $curr = 1; - while (1) { - # Convert a big integer to a list of bits - my $sth = $dbh->prepare("SELECT ($num & ~$curr) > 0, + my ($num) = @_; + my $dbh = Bugzilla->dbh; + my @res; + my $curr = 1; + while (1) { + + # Convert a big integer to a list of bits + my $sth = $dbh->prepare( + "SELECT ($num & ~$curr) > 0, ($num & $curr), ($num & ~$curr), - $curr << 1"); - $sth->execute; - my ($more, $thisbit, $remain, $nval) = $sth->fetchrow_array; - push @res,"UNKNOWN<$curr>" if ($thisbit); - $curr = $nval; - $num = $remain; - last if !$more; - } - return @res; + $curr << 1" + ); + $sth->execute; + my ($more, $thisbit, $remain, $nval) = $sth->fetchrow_array; + push @res, "UNKNOWN<$curr>" if ($thisbit); + $curr = $nval; + $num = $remain; + last if !$more; + } + return @res; } sub _convert_groups_system_from_groupset { - my $dbh = Bugzilla->dbh; - # 2002-09-22 - bugreport@peshkin.net - bug 157756 - # - # If the whole groups system is new, but the installation isn't, - # convert all the old groupset groups, etc... - # - # This requires: - # 1) define groups ids in group table - # 2) populate user_group_map with grants from old groupsets - # and blessgroupsets - # 3) populate bug_group_map with data converted from old bug groupsets - # 4) convert activity logs to use group names instead of numbers - # 5) identify the admin from the old all-ones groupset - - # The groups system needs to be converted if groupset exists - if ($dbh->bz_column_info("profiles", "groupset")) { - # Some mysql versions will promote any unique key to primary key - # so all unique keys are removed first and then added back in - $dbh->bz_drop_index('groups', 'groups_bit_idx'); - $dbh->bz_drop_index('groups', 'groups_name_idx'); - my @primary_key = $dbh->primary_key(undef, undef, 'groups'); - if (@primary_key) { - $dbh->do("ALTER TABLE groups DROP PRIMARY KEY"); - } + my $dbh = Bugzilla->dbh; + + # 2002-09-22 - bugreport@peshkin.net - bug 157756 + # + # If the whole groups system is new, but the installation isn't, + # convert all the old groupset groups, etc... + # + # This requires: + # 1) define groups ids in group table + # 2) populate user_group_map with grants from old groupsets + # and blessgroupsets + # 3) populate bug_group_map with data converted from old bug groupsets + # 4) convert activity logs to use group names instead of numbers + # 5) identify the admin from the old all-ones groupset + + # The groups system needs to be converted if groupset exists + if ($dbh->bz_column_info("profiles", "groupset")) { + + # Some mysql versions will promote any unique key to primary key + # so all unique keys are removed first and then added back in + $dbh->bz_drop_index('groups', 'groups_bit_idx'); + $dbh->bz_drop_index('groups', 'groups_name_idx'); + my @primary_key = $dbh->primary_key(undef, undef, 'groups'); + if (@primary_key) { + $dbh->do("ALTER TABLE groups DROP PRIMARY KEY"); + } + + $dbh->bz_add_column('groups', 'id', + {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); + + $dbh->bz_add_index('groups', 'groups_name_idx', + {TYPE => 'UNIQUE', FIELDS => [qw(name)]}); - $dbh->bz_add_column('groups', 'id', - {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); - - $dbh->bz_add_index('groups', 'groups_name_idx', - {TYPE => 'UNIQUE', FIELDS => [qw(name)]}); - - # Convert all existing groupset records to map entries before removing - # groupset fields or removing "bit" from groups. - my $sth = $dbh->prepare("SELECT bit, id FROM groups WHERE bit > 0"); - $sth->execute(); - while (my ($bit, $gid) = $sth->fetchrow_array) { - # Create user_group_map membership grants for old groupsets. - # Get each user with the old groupset bit set - my $sth2 = $dbh->prepare("SELECT userid FROM profiles - WHERE (groupset & $bit) != 0"); - $sth2->execute(); - while (my ($uid) = $sth2->fetchrow_array) { - # Check to see if the user is already a member of the group - # and, if not, insert a new record. - my $query = "SELECT user_id FROM user_group_map + # Convert all existing groupset records to map entries before removing + # groupset fields or removing "bit" from groups. + my $sth = $dbh->prepare("SELECT bit, id FROM groups WHERE bit > 0"); + $sth->execute(); + while (my ($bit, $gid) = $sth->fetchrow_array) { + + # Create user_group_map membership grants for old groupsets. + # Get each user with the old groupset bit set + my $sth2 = $dbh->prepare( + "SELECT userid FROM profiles + WHERE (groupset & $bit) != 0" + ); + $sth2->execute(); + while (my ($uid) = $sth2->fetchrow_array) { + + # Check to see if the user is already a member of the group + # and, if not, insert a new record. + my $query = "SELECT user_id FROM user_group_map WHERE group_id = $gid AND user_id = $uid AND isbless = 0"; - my $sth3 = $dbh->prepare($query); - $sth3->execute(); - if ( !$sth3->fetchrow_array() ) { - $dbh->do("INSERT INTO user_group_map + my $sth3 = $dbh->prepare($query); + $sth3->execute(); + if (!$sth3->fetchrow_array()) { + $dbh->do( + "INSERT INTO user_group_map (user_id, group_id, isbless, grant_type) - VALUES ($uid, $gid, 0, " . GRANT_DIRECT . ")"); - } - } - # Create user can bless group grants for old groupsets, but only - # if we're upgrading from a Bugzilla that had blessing. - if($dbh->bz_column_info('profiles', 'blessgroupset')) { - # Get each user with the old blessgroupset bit set - $sth2 = $dbh->prepare("SELECT userid FROM profiles - WHERE (blessgroupset & $bit) != 0"); - $sth2->execute(); - while (my ($uid) = $sth2->fetchrow_array) { - $dbh->do("INSERT INTO user_group_map + VALUES ($uid, $gid, 0, " . GRANT_DIRECT . ")" + ); + } + } + + # Create user can bless group grants for old groupsets, but only + # if we're upgrading from a Bugzilla that had blessing. + if ($dbh->bz_column_info('profiles', 'blessgroupset')) { + + # Get each user with the old blessgroupset bit set + $sth2 = $dbh->prepare( + "SELECT userid FROM profiles + WHERE (blessgroupset & $bit) != 0" + ); + $sth2->execute(); + while (my ($uid) = $sth2->fetchrow_array) { + $dbh->do( + "INSERT INTO user_group_map (user_id, group_id, isbless, grant_type) - VALUES ($uid, $gid, 1, " . GRANT_DIRECT . ")"); - } - } - # Create bug_group_map records for old groupsets. - # Get each bug with the old group bit set. - $sth2 = $dbh->prepare("SELECT bug_id FROM bugs - WHERE (groupset & $bit) != 0"); - $sth2->execute(); - while (my ($bug_id) = $sth2->fetchrow_array) { - # Insert the bug, group pair into the bug_group_map. - $dbh->do("INSERT INTO bug_group_map (bug_id, group_id) - VALUES ($bug_id, $gid)"); - } + VALUES ($uid, $gid, 1, " . GRANT_DIRECT . ")" + ); } - # Replace old activity log groupset records with lists of names - # of groups. - $sth = $dbh->prepare("SELECT id FROM fielddefs - WHERE name = " . $dbh->quote('bug_group')); - $sth->execute(); - my ($bgfid) = $sth->fetchrow_array; - # Get the field id for the old groupset field - $sth = $dbh->prepare("SELECT id FROM fielddefs - WHERE name = " . $dbh->quote('groupset')); - $sth->execute(); - my ($gsid) = $sth->fetchrow_array; - # Get all bugs_activity records from groupset changes - if ($gsid) { - $sth = $dbh->prepare("SELECT bug_id, bug_when, who, added, removed - FROM bugs_activity WHERE fieldid = $gsid"); - $sth->execute(); - while (my ($bug_id, $bug_when, $who, $added, $removed) = - $sth->fetchrow_array) - { - $added ||= 0; - $removed ||= 0; - # Get names of groups added. - my $sth2 = $dbh->prepare("SELECT name FROM groups - WHERE (bit & $added) != 0 - AND (bit & $removed) = 0"); - $sth2->execute(); - my @logadd; - while (my ($n) = $sth2->fetchrow_array) { - push @logadd, $n; - } - # Get names of groups removed. - $sth2 = $dbh->prepare("SELECT name FROM groups - WHERE (bit & $removed) != 0 - AND (bit & $added) = 0"); - $sth2->execute(); - my @logrem; - while (my ($n) = $sth2->fetchrow_array) { - push @logrem, $n; - } - # Get list of group bits added that correspond to - # missing groups. - $sth2 = $dbh->prepare("SELECT ($added & ~BIT_OR(bit)) - FROM groups"); - $sth2->execute(); - my ($miss) = $sth2->fetchrow_array; - if ($miss) { - push @logadd, _list_bits($miss); - print "\nWARNING - GROUPSET ACTIVITY ON BUG $bug_id", - " CONTAINS DELETED GROUPS\n"; - } - # Get list of group bits deleted that correspond to - # missing groups. - $sth2 = $dbh->prepare("SELECT ($removed & ~BIT_OR(bit)) - FROM groups"); - $sth2->execute(); - ($miss) = $sth2->fetchrow_array; - if ($miss) { - push @logrem, _list_bits($miss); - print "\nWARNING - GROUPSET ACTIVITY ON BUG $bug_id", - " CONTAINS DELETED GROUPS\n"; - } - my $logr = ""; - my $loga = ""; - $logr = join(", ", @logrem) . '?' if @logrem; - $loga = join(", ", @logadd) . '?' if @logadd; - # Replace to old activity record with the converted data. - $dbh->do("UPDATE bugs_activity SET fieldid = $bgfid, added = " . - $dbh->quote($loga) . ", removed = " . - $dbh->quote($logr) . - " WHERE bug_id = $bug_id AND bug_when = " . - $dbh->quote($bug_when) . - " AND who = $who AND fieldid = $gsid"); - } - # Replace groupset changes with group name changes in - # profiles_activity. Get profiles_activity records for groupset. - $sth = $dbh->prepare( - "SELECT userid, profiles_when, who, newvalue, oldvalue " . - "FROM profiles_activity " . - "WHERE fieldid = $gsid"); - $sth->execute(); - while (my ($uid, $uwhen, $uwho, $added, $removed) = - $sth->fetchrow_array) - { - $added ||= 0; - $removed ||= 0; - # Get names of groups added. - my $sth2 = $dbh->prepare("SELECT name FROM groups + } + + # Create bug_group_map records for old groupsets. + # Get each bug with the old group bit set. + $sth2 = $dbh->prepare( + "SELECT bug_id FROM bugs + WHERE (groupset & $bit) != 0" + ); + $sth2->execute(); + while (my ($bug_id) = $sth2->fetchrow_array) { + + # Insert the bug, group pair into the bug_group_map. + $dbh->do( + "INSERT INTO bug_group_map (bug_id, group_id) + VALUES ($bug_id, $gid)" + ); + } + } + + # Replace old activity log groupset records with lists of names + # of groups. + $sth = $dbh->prepare( + "SELECT id FROM fielddefs + WHERE name = " . $dbh->quote('bug_group') + ); + $sth->execute(); + my ($bgfid) = $sth->fetchrow_array; + + # Get the field id for the old groupset field + $sth = $dbh->prepare( + "SELECT id FROM fielddefs + WHERE name = " . $dbh->quote('groupset') + ); + $sth->execute(); + my ($gsid) = $sth->fetchrow_array; + + # Get all bugs_activity records from groupset changes + if ($gsid) { + $sth = $dbh->prepare( + "SELECT bug_id, bug_when, who, added, removed + FROM bugs_activity WHERE fieldid = $gsid" + ); + $sth->execute(); + while (my ($bug_id, $bug_when, $who, $added, $removed) = $sth->fetchrow_array) { + $added ||= 0; + $removed ||= 0; + + # Get names of groups added. + my $sth2 = $dbh->prepare( + "SELECT name FROM groups WHERE (bit & $added) != 0 - AND (bit & $removed) = 0"); - $sth2->execute(); - my @logadd; - while (my ($n) = $sth2->fetchrow_array) { - push @logadd, $n; - } - # Get names of groups removed. - $sth2 = $dbh->prepare("SELECT name FROM groups + AND (bit & $removed) = 0" + ); + $sth2->execute(); + my @logadd; + while (my ($n) = $sth2->fetchrow_array) { + push @logadd, $n; + } + + # Get names of groups removed. + $sth2 = $dbh->prepare( + "SELECT name FROM groups WHERE (bit & $removed) != 0 - AND (bit & $added) = 0"); - $sth2->execute(); - my @logrem; - while (my ($n) = $sth2->fetchrow_array) { - push @logrem, $n; - } - my $ladd = ""; - my $lrem = ""; - $ladd = join(", ", @logadd) . '?' if @logadd; - $lrem = join(", ", @logrem) . '?' if @logrem; - # Replace profiles_activity record for groupset change - # with group list. - $dbh->do("UPDATE profiles_activity " . - "SET fieldid = $bgfid, newvalue = " . - $dbh->quote($ladd) . ", oldvalue = " . - $dbh->quote($lrem) . - " WHERE userid = $uid AND profiles_when = " . - $dbh->quote($uwhen) . - " AND who = $uwho AND fieldid = $gsid"); - } + AND (bit & $added) = 0" + ); + $sth2->execute(); + my @logrem; + while (my ($n) = $sth2->fetchrow_array) { + push @logrem, $n; } - # Identify admin group. - my ($admin_gid) = $dbh->selectrow_array( - "SELECT id FROM groups WHERE name = 'admin'"); - if (!$admin_gid) { - $dbh->do(q{INSERT INTO groups (name, description) - VALUES ('admin', 'Administrators')}); - $admin_gid = $dbh->bz_last_key('groups', 'id'); + # Get list of group bits added that correspond to + # missing groups. + $sth2 = $dbh->prepare( + "SELECT ($added & ~BIT_OR(bit)) + FROM groups" + ); + $sth2->execute(); + my ($miss) = $sth2->fetchrow_array; + if ($miss) { + push @logadd, _list_bits($miss); + print "\nWARNING - GROUPSET ACTIVITY ON BUG $bug_id", + " CONTAINS DELETED GROUPS\n"; } - # Find current admins - my @admins; - # Don't lose admins from DBs where Bug 157704 applies - $sth = $dbh->prepare( - "SELECT userid, (groupset & 65536), login_name " . - "FROM profiles " . - "WHERE (groupset | 65536) = 9223372036854775807"); - $sth->execute(); - while ( my ($userid, $iscomplete, $login_name) - = $sth->fetchrow_array() ) - { - # existing administrators are made members of group "admin" - print "\nWARNING - $login_name IS AN ADMIN IN SPITE OF BUG", - " 157704\n\n" if (!$iscomplete); - push(@admins, $userid) unless grep($_ eq $userid, @admins); + + # Get list of group bits deleted that correspond to + # missing groups. + $sth2 = $dbh->prepare( + "SELECT ($removed & ~BIT_OR(bit)) + FROM groups" + ); + $sth2->execute(); + ($miss) = $sth2->fetchrow_array; + if ($miss) { + push @logrem, _list_bits($miss); + print "\nWARNING - GROUPSET ACTIVITY ON BUG $bug_id", + " CONTAINS DELETED GROUPS\n"; } - # Now make all those users admins directly. They were already - # added to every other group, above, because of their groupset. - foreach my $admin_id (@admins) { - $dbh->do("INSERT INTO user_group_map - (user_id, group_id, isbless, grant_type) - VALUES (?, ?, ?, ?)", - undef, $admin_id, $admin_gid, $_, GRANT_DIRECT) - foreach (0, 1); + my $logr = ""; + my $loga = ""; + $logr = join(", ", @logrem) . '?' if @logrem; + $loga = join(", ", @logadd) . '?' if @logadd; + + # Replace to old activity record with the converted data. + $dbh->do("UPDATE bugs_activity SET fieldid = $bgfid, added = " + . $dbh->quote($loga) + . ", removed = " + . $dbh->quote($logr) + . " WHERE bug_id = $bug_id AND bug_when = " + . $dbh->quote($bug_when) + . " AND who = $who AND fieldid = $gsid"); + } + + # Replace groupset changes with group name changes in + # profiles_activity. Get profiles_activity records for groupset. + $sth + = $dbh->prepare("SELECT userid, profiles_when, who, newvalue, oldvalue " + . "FROM profiles_activity " + . "WHERE fieldid = $gsid"); + $sth->execute(); + while (my ($uid, $uwhen, $uwho, $added, $removed) = $sth->fetchrow_array) { + $added ||= 0; + $removed ||= 0; + + # Get names of groups added. + my $sth2 = $dbh->prepare( + "SELECT name FROM groups + WHERE (bit & $added) != 0 + AND (bit & $removed) = 0" + ); + $sth2->execute(); + my @logadd; + while (my ($n) = $sth2->fetchrow_array) { + push @logadd, $n; } - $dbh->bz_drop_column('profiles','groupset'); - $dbh->bz_drop_column('profiles','blessgroupset'); - $dbh->bz_drop_column('bugs','groupset'); - $dbh->bz_drop_column('groups','bit'); - $dbh->do("DELETE FROM fielddefs WHERE name = " - . $dbh->quote('groupset')); + # Get names of groups removed. + $sth2 = $dbh->prepare( + "SELECT name FROM groups + WHERE (bit & $removed) != 0 + AND (bit & $added) = 0" + ); + $sth2->execute(); + my @logrem; + while (my ($n) = $sth2->fetchrow_array) { + push @logrem, $n; + } + my $ladd = ""; + my $lrem = ""; + $ladd = join(", ", @logadd) . '?' if @logadd; + $lrem = join(", ", @logrem) . '?' if @logrem; + + # Replace profiles_activity record for groupset change + # with group list. + $dbh->do("UPDATE profiles_activity " + . "SET fieldid = $bgfid, newvalue = " + . $dbh->quote($ladd) + . ", oldvalue = " + . $dbh->quote($lrem) + . " WHERE userid = $uid AND profiles_when = " + . $dbh->quote($uwhen) + . " AND who = $uwho AND fieldid = $gsid"); + } + } + + # Identify admin group. + my ($admin_gid) + = $dbh->selectrow_array("SELECT id FROM groups WHERE name = 'admin'"); + if (!$admin_gid) { + $dbh->do( + q{INSERT INTO groups (name, description) + VALUES ('admin', 'Administrators')} + ); + $admin_gid = $dbh->bz_last_key('groups', 'id'); } + + # Find current admins + my @admins; + + # Don't lose admins from DBs where Bug 157704 applies + $sth + = $dbh->prepare("SELECT userid, (groupset & 65536), login_name " + . "FROM profiles " + . "WHERE (groupset | 65536) = 9223372036854775807"); + $sth->execute(); + while (my ($userid, $iscomplete, $login_name) = $sth->fetchrow_array()) { + + # existing administrators are made members of group "admin" + print "\nWARNING - $login_name IS AN ADMIN IN SPITE OF BUG", " 157704\n\n" + if (!$iscomplete); + push(@admins, $userid) unless grep($_ eq $userid, @admins); + } + + # Now make all those users admins directly. They were already + # added to every other group, above, because of their groupset. + foreach my $admin_id (@admins) { + $dbh->do( + "INSERT INTO user_group_map + (user_id, group_id, isbless, grant_type) + VALUES (?, ?, ?, ?)", undef, $admin_id, $admin_gid, $_, + GRANT_DIRECT + ) foreach (0, 1); + } + + $dbh->bz_drop_column('profiles', 'groupset'); + $dbh->bz_drop_column('profiles', 'blessgroupset'); + $dbh->bz_drop_column('bugs', 'groupset'); + $dbh->bz_drop_column('groups', 'bit'); + $dbh->do("DELETE FROM fielddefs WHERE name = " . $dbh->quote('groupset')); + } } sub _convert_attachment_statuses_to_flags { - my $dbh = Bugzilla->dbh; + my $dbh = Bugzilla->dbh; + + # September 2002 myk@mozilla.org bug 98801 + # Convert the attachment statuses tables into flags tables. + if ( $dbh->bz_table_info("attachstatuses") + && $dbh->bz_table_info("attachstatusdefs")) + { + print "Converting attachment statuses to flags...\n"; + + # Get IDs for the old attachment status and new flag fields. + my ($old_field_id) + = $dbh->selectrow_array( + "SELECT id FROM fielddefs WHERE name='attachstatusdefs.name'") + || 0; + my ($new_field_id) + = $dbh->selectrow_array( + "SELECT id FROM fielddefs WHERE name = 'flagtypes.name'"); + + # Convert attachment status definitions to flag types. If more than one + # status has the same name and description, it is merged into a single + # status with multiple inclusion records. - # September 2002 myk@mozilla.org bug 98801 - # Convert the attachment statuses tables into flags tables. - if ($dbh->bz_table_info("attachstatuses") - && $dbh->bz_table_info("attachstatusdefs")) - { - print "Converting attachment statuses to flags...\n"; - - # Get IDs for the old attachment status and new flag fields. - my ($old_field_id) = $dbh->selectrow_array( - "SELECT id FROM fielddefs WHERE name='attachstatusdefs.name'") - || 0; - my ($new_field_id) = $dbh->selectrow_array( - "SELECT id FROM fielddefs WHERE name = 'flagtypes.name'"); - - # Convert attachment status definitions to flag types. If more than one - # status has the same name and description, it is merged into a single - # status with multiple inclusion records. - - my $sth = $dbh->prepare( - "SELECT id, name, description, sortkey, product_id - FROM attachstatusdefs"); - - # status definition IDs indexed by name/description - my $def_ids = {}; - - # merged IDs and the IDs they were merged into. The key is the old ID, - # the value is the new one. This allows us to give statuses the right - # ID when we convert them over to flags. This map includes IDs that - # weren't merged (in this case the old and new IDs are the same), since - # it makes the code simpler. - my $def_id_map = {}; - - $sth->execute(); - while (my ($id, $name, $desc, $sortkey, $prod_id) = - $sth->fetchrow_array()) - { - my $key = $name . $desc; - if (!$def_ids->{$key}) { - $def_ids->{$key} = $id; - my $quoted_name = $dbh->quote($name); - my $quoted_desc = $dbh->quote($desc); - $dbh->do("INSERT INTO flagtypes (id, name, description, + my $sth = $dbh->prepare( + "SELECT id, name, description, sortkey, product_id + FROM attachstatusdefs" + ); + + # status definition IDs indexed by name/description + my $def_ids = {}; + + # merged IDs and the IDs they were merged into. The key is the old ID, + # the value is the new one. This allows us to give statuses the right + # ID when we convert them over to flags. This map includes IDs that + # weren't merged (in this case the old and new IDs are the same), since + # it makes the code simpler. + my $def_id_map = {}; + + $sth->execute(); + while (my ($id, $name, $desc, $sortkey, $prod_id) = $sth->fetchrow_array()) { + my $key = $name . $desc; + if (!$def_ids->{$key}) { + $def_ids->{$key} = $id; + my $quoted_name = $dbh->quote($name); + my $quoted_desc = $dbh->quote($desc); + $dbh->do( + "INSERT INTO flagtypes (id, name, description, sortkey, target_type) VALUES ($id, $quoted_name, $quoted_desc, - $sortkey,'a')"); - } - $def_id_map->{$id} = $def_ids->{$key}; - $dbh->do("INSERT INTO flaginclusions (type_id, product_id) - VALUES ($def_id_map->{$id}, $prod_id)"); - } + $sortkey,'a')" + ); + } + $def_id_map->{$id} = $def_ids->{$key}; + $dbh->do( + "INSERT INTO flaginclusions (type_id, product_id) + VALUES ($def_id_map->{$id}, $prod_id)" + ); + } - # Note: even though we've converted status definitions, we still - # can't drop the table because we need it to convert the statuses - # themselves. - - # Convert attachment statuses to flags. To do this we select - # the statuses from the status table and then, for each one, - # figure out who set it and when they set it from the bugs - # activity table. - my $id = 0; - $sth = $dbh->prepare( - "SELECT attachstatuses.attach_id, attachstatusdefs.id, + # Note: even though we've converted status definitions, we still + # can't drop the table because we need it to convert the statuses + # themselves. + + # Convert attachment statuses to flags. To do this we select + # the statuses from the status table and then, for each one, + # figure out who set it and when they set it from the bugs + # activity table. + my $id = 0; + $sth = $dbh->prepare( + "SELECT attachstatuses.attach_id, attachstatusdefs.id, attachstatusdefs.name, attachments.bug_id FROM attachstatuses, attachstatusdefs, attachments WHERE attachstatuses.statusid = attachstatusdefs.id - AND attachstatuses.attach_id = attachments.attach_id"); + AND attachstatuses.attach_id = attachments.attach_id" + ); - # a query to determine when the attachment status was set and who set it - my $sth2 = $dbh->prepare("SELECT added, who, bug_when + # a query to determine when the attachment status was set and who set it + my $sth2 = $dbh->prepare( + "SELECT added, who, bug_when FROM bugs_activity WHERE bug_id = ? AND attach_id = ? AND fieldid = $old_field_id - ORDER BY bug_when DESC"); - - $sth->execute(); - while (my ($attach_id, $def_id, $status, $bug_id) = - $sth->fetchrow_array()) - { - ++$id; - - # Determine when the attachment status was set and who set it. - # We should always be able to find out this info from the bug - # activity, but we fall back to default values just in case. - $sth2->execute($bug_id, $attach_id); - my ($added, $who, $when); - while (($added, $who, $when) = $sth2->fetchrow_array()) { - last if $added =~ /(^|[, ]+)\Q$status\E([, ]+|$)/; - } - $who = $dbh->quote($who); # "NULL" by default if $who is undefined - $when = $when ? $dbh->quote($when) : "NOW()"; - + ORDER BY bug_when DESC" + ); - $dbh->do("INSERT INTO flags (id, type_id, status, bug_id, + $sth->execute(); + while (my ($attach_id, $def_id, $status, $bug_id) = $sth->fetchrow_array()) { + ++$id; + + # Determine when the attachment status was set and who set it. + # We should always be able to find out this info from the bug + # activity, but we fall back to default values just in case. + $sth2->execute($bug_id, $attach_id); + my ($added, $who, $when); + while (($added, $who, $when) = $sth2->fetchrow_array()) { + last if $added =~ /(^|[, ]+)\Q$status\E([, ]+|$)/; + } + $who = $dbh->quote($who); # "NULL" by default if $who is undefined + $when = $when ? $dbh->quote($when) : "NOW()"; + + + $dbh->do( + "INSERT INTO flags (id, type_id, status, bug_id, attach_id, creation_date, modification_date, requestee_id, setter_id) VALUES ($id, $def_id_map->{$def_id}, '+', $bug_id, - $attach_id, $when, $when, NULL, $who)"); - } + $attach_id, $when, $when, NULL, $who)" + ); + } - # Now that we've converted both tables we can drop them. - $dbh->bz_drop_table("attachstatuses"); - $dbh->bz_drop_table("attachstatusdefs"); + # Now that we've converted both tables we can drop them. + $dbh->bz_drop_table("attachstatuses"); + $dbh->bz_drop_table("attachstatusdefs"); - # Convert activity records for attachment statuses into records - # for flags. - $sth = $dbh->prepare("SELECT attach_id, who, bug_when, added, + # Convert activity records for attachment statuses into records + # for flags. + $sth = $dbh->prepare( + "SELECT attach_id, who, bug_when, added, removed FROM bugs_activity - WHERE fieldid = $old_field_id"); - $sth->execute(); - while (my ($attach_id, $who, $when, $old_added, $old_removed) = - $sth->fetchrow_array()) - { - my @additions = split(/[, ]+/, $old_added); - @additions = map("$_+", @additions); - my $new_added = $dbh->quote(join(", ", @additions)); - - my @removals = split(/[, ]+/, $old_removed); - @removals = map("$_+", @removals); - my $new_removed = $dbh->quote(join(", ", @removals)); - - $old_added = $dbh->quote($old_added); - $old_removed = $dbh->quote($old_removed); - $who = $dbh->quote($who); - $when = $dbh->quote($when); - - $dbh->do("UPDATE bugs_activity SET fieldid = $new_field_id, " . - "added = $new_added, removed = $new_removed " . - "WHERE attach_id = $attach_id AND who = $who " . - "AND bug_when = $when AND fieldid = $old_field_id " . - "AND added = $old_added AND removed = $old_removed"); - } + WHERE fieldid = $old_field_id" + ); + $sth->execute(); + while (my ($attach_id, $who, $when, $old_added, $old_removed) + = $sth->fetchrow_array()) + { + my @additions = split(/[, ]+/, $old_added); + @additions = map("$_+", @additions); + my $new_added = $dbh->quote(join(", ", @additions)); + + my @removals = split(/[, ]+/, $old_removed); + @removals = map("$_+", @removals); + my $new_removed = $dbh->quote(join(", ", @removals)); + + $old_added = $dbh->quote($old_added); + $old_removed = $dbh->quote($old_removed); + $who = $dbh->quote($who); + $when = $dbh->quote($when); + + $dbh->do("UPDATE bugs_activity SET fieldid = $new_field_id, " + . "added = $new_added, removed = $new_removed " + . "WHERE attach_id = $attach_id AND who = $who " + . "AND bug_when = $when AND fieldid = $old_field_id " + . "AND added = $old_added AND removed = $old_removed"); + } - # Remove the attachment status field from the field definitions. - $dbh->do("DELETE FROM fielddefs WHERE name='attachstatusdefs.name'"); + # Remove the attachment status field from the field definitions. + $dbh->do("DELETE FROM fielddefs WHERE name='attachstatusdefs.name'"); - print "done.\n"; - } + print "done.\n"; + } } sub _remove_spaces_and_commas_from_flagtypes { - my $dbh = Bugzilla->dbh; - # Get all names and IDs, to find broken ones and to - # check for collisions when renaming. - my $sth = $dbh->prepare("SELECT name, id FROM flagtypes"); - $sth->execute(); - - my %flagtypes; - my @badflagnames; - # find broken flagtype names, and populate a hash table - # to check for collisions. - while (my ($name, $id) = $sth->fetchrow_array()) { - $flagtypes{$name} = $id; - if ($name =~ /[ ,]/) { - push(@badflagnames, $name); - } + my $dbh = Bugzilla->dbh; + + # Get all names and IDs, to find broken ones and to + # check for collisions when renaming. + my $sth = $dbh->prepare("SELECT name, id FROM flagtypes"); + $sth->execute(); + + my %flagtypes; + my @badflagnames; + + # find broken flagtype names, and populate a hash table + # to check for collisions. + while (my ($name, $id) = $sth->fetchrow_array()) { + $flagtypes{$name} = $id; + if ($name =~ /[ ,]/) { + push(@badflagnames, $name); } - if (@badflagnames) { - print "Removing spaces and commas from flag names...\n"; - my ($flagname, $tryflagname); - my $sth = $dbh->prepare("UPDATE flagtypes SET name = ? WHERE id = ?"); - foreach $flagname (@badflagnames) { - print " Bad flag type name \"$flagname\" ...\n"; - # find a new name for this flagtype. - ($tryflagname = $flagname) =~ tr/ ,/__/; - # avoid collisions with existing flagtype names. - while (defined($flagtypes{$tryflagname})) { - print " ... can't rename as \"$tryflagname\" ...\n"; - $tryflagname .= "'"; - if (length($tryflagname) > 50) { - my $lastchanceflagname = (substr $tryflagname, 0, 47) . '...'; - if (defined($flagtypes{$lastchanceflagname})) { - print " ... last attempt as \"$lastchanceflagname\" still failed.'\n"; - die install_string('update_flags_bad_name', - { flag => $flagname }), "\n"; - } - $tryflagname = $lastchanceflagname; - } - } - $sth->execute($tryflagname, $flagtypes{$flagname}); - print " renamed flag type \"$flagname\" as \"$tryflagname\"\n"; - $flagtypes{$tryflagname} = $flagtypes{$flagname}; - delete $flagtypes{$flagname}; + } + if (@badflagnames) { + print "Removing spaces and commas from flag names...\n"; + my ($flagname, $tryflagname); + my $sth = $dbh->prepare("UPDATE flagtypes SET name = ? WHERE id = ?"); + foreach $flagname (@badflagnames) { + print " Bad flag type name \"$flagname\" ...\n"; + + # find a new name for this flagtype. + ($tryflagname = $flagname) =~ tr/ ,/__/; + + # avoid collisions with existing flagtype names. + while (defined($flagtypes{$tryflagname})) { + print " ... can't rename as \"$tryflagname\" ...\n"; + $tryflagname .= "'"; + if (length($tryflagname) > 50) { + my $lastchanceflagname = (substr $tryflagname, 0, 47) . '...'; + if (defined($flagtypes{$lastchanceflagname})) { + print " ... last attempt as \"$lastchanceflagname\" still failed.'\n"; + die install_string('update_flags_bad_name', {flag => $flagname}), "\n"; + } + $tryflagname = $lastchanceflagname; } - print "... done.\n"; + } + $sth->execute($tryflagname, $flagtypes{$flagname}); + print " renamed flag type \"$flagname\" as \"$tryflagname\"\n"; + $flagtypes{$tryflagname} = $flagtypes{$flagname}; + delete $flagtypes{$flagname}; } + print "... done.\n"; + } } sub _setup_usebuggroups_backward_compatibility { - my $dbh = Bugzilla->dbh; - - # Don't run this on newer Bugzillas. This is a reliable test because - # the longdescs table existed in 2.16 (which had usebuggroups) - # but not in 2.18, and this code happens between 2.16 and 2.18. - return if $dbh->bz_column_info('longdescs', 'already_wrapped'); - - # 2002-11-24 - bugreport@peshkin.net - bug 147275 - # - # If group_control_map is empty, backward-compatibility - # usebuggroups-equivalent records should be created. - my ($maps_exist) = $dbh->selectrow_array( - "SELECT DISTINCT 1 FROM group_control_map"); - if (!$maps_exist) { - print "Converting old usebuggroups controls...\n"; - # Initially populate group_control_map. - # First, get all the existing products and their groups. - my $sth = $dbh->prepare("SELECT groups.id, products.id, groups.name, + my $dbh = Bugzilla->dbh; + + # Don't run this on newer Bugzillas. This is a reliable test because + # the longdescs table existed in 2.16 (which had usebuggroups) + # but not in 2.18, and this code happens between 2.16 and 2.18. + return if $dbh->bz_column_info('longdescs', 'already_wrapped'); + + # 2002-11-24 - bugreport@peshkin.net - bug 147275 + # + # If group_control_map is empty, backward-compatibility + # usebuggroups-equivalent records should be created. + my ($maps_exist) + = $dbh->selectrow_array("SELECT DISTINCT 1 FROM group_control_map"); + if (!$maps_exist) { + print "Converting old usebuggroups controls...\n"; + + # Initially populate group_control_map. + # First, get all the existing products and their groups. + my $sth = $dbh->prepare( + "SELECT groups.id, products.id, groups.name, products.name FROM groups, products - WHERE isbuggroup != 0"); - $sth->execute(); - while (my ($groupid, $productid, $groupname, $productname) - = $sth->fetchrow_array()) - { - if ($groupname eq $productname) { - # Product and group have same name. - $dbh->do("INSERT INTO group_control_map " . - "(group_id, product_id, membercontrol, othercontrol) " . - "VALUES (?, ?, ?, ?)", undef, - ($groupid, $productid, CONTROLMAPDEFAULT, CONTROLMAPNA)); - } else { - # See if this group is a product group at all. - my $sth2 = $dbh->prepare("SELECT id FROM products - WHERE name = " .$dbh->quote($groupname)); - $sth2->execute(); - my ($id) = $sth2->fetchrow_array(); - if (!$id) { - # If there is no product with the same name as this - # group, then it is permitted for all products. - $dbh->do("INSERT INTO group_control_map " . - "(group_id, product_id, membercontrol, othercontrol) " . - "VALUES (?, ?, ?, ?)", undef, - ($groupid, $productid, CONTROLMAPSHOWN, CONTROLMAPNA)); - } - } + WHERE isbuggroup != 0" + ); + $sth->execute(); + while (my ($groupid, $productid, $groupname, $productname) + = $sth->fetchrow_array()) + { + if ($groupname eq $productname) { + + # Product and group have same name. + $dbh->do( + "INSERT INTO group_control_map " + . "(group_id, product_id, membercontrol, othercontrol) " + . "VALUES (?, ?, ?, ?)", + undef, + ($groupid, $productid, CONTROLMAPDEFAULT, CONTROLMAPNA) + ); + } + else { + # See if this group is a product group at all. + my $sth2 = $dbh->prepare( + "SELECT id FROM products + WHERE name = " . $dbh->quote($groupname) + ); + $sth2->execute(); + my ($id) = $sth2->fetchrow_array(); + if (!$id) { + + # If there is no product with the same name as this + # group, then it is permitted for all products. + $dbh->do( + "INSERT INTO group_control_map " + . "(group_id, product_id, membercontrol, othercontrol) " + . "VALUES (?, ?, ?, ?)", + undef, + ($groupid, $productid, CONTROLMAPSHOWN, CONTROLMAPNA) + ); } + } } + } } sub _remove_user_series_map { - my $dbh = Bugzilla->dbh; - # 2004-07-17 GRM - Remove "subscriptions" concept from charting, and add - # group-based security instead. - if ($dbh->bz_table_info("user_series_map")) { - # Oracle doesn't like "date" as a column name, and apparently some DBs - # don't like 'value' either. We use the changes to subscriptions as - # something to hang these renamings off. - $dbh->bz_rename_column('series_data', 'date', 'series_date'); - $dbh->bz_rename_column('series_data', 'value', 'series_value'); - - # series_categories.category_id produces a too-long column name for the - # auto-incrementing sequence (Oracle again). - $dbh->bz_rename_column('series_categories', 'category_id', 'id'); - - $dbh->bz_add_column("series", "public", - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); - - # Migrate public-ness across from user_series_map to new field - my $sth = $dbh->prepare("SELECT series_id from user_series_map " . - "WHERE user_id = 0"); - $sth->execute(); - while (my ($public_series_id) = $sth->fetchrow_array()) { - $dbh->do("UPDATE series SET public = 1 " . - "WHERE series_id = $public_series_id"); - } + my $dbh = Bugzilla->dbh; + + # 2004-07-17 GRM - Remove "subscriptions" concept from charting, and add + # group-based security instead. + if ($dbh->bz_table_info("user_series_map")) { + + # Oracle doesn't like "date" as a column name, and apparently some DBs + # don't like 'value' either. We use the changes to subscriptions as + # something to hang these renamings off. + $dbh->bz_rename_column('series_data', 'date', 'series_date'); + $dbh->bz_rename_column('series_data', 'value', 'series_value'); + + # series_categories.category_id produces a too-long column name for the + # auto-incrementing sequence (Oracle again). + $dbh->bz_rename_column('series_categories', 'category_id', 'id'); + + $dbh->bz_add_column("series", "public", + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); - $dbh->bz_drop_table("user_series_map"); + # Migrate public-ness across from user_series_map to new field + my $sth = $dbh->prepare( + "SELECT series_id from user_series_map " . "WHERE user_id = 0"); + $sth->execute(); + while (my ($public_series_id) = $sth->fetchrow_array()) { + $dbh->do( + "UPDATE series SET public = 1 " . "WHERE series_id = $public_series_id"); } + + $dbh->bz_drop_table("user_series_map"); + } } sub _copy_old_charts_into_database { - my $dbh = Bugzilla->dbh; - my $datadir = bz_locations()->{'datadir'}; - # 2003-06-26 Copy the old charting data into the database, and create the - # queries that will keep it all running. When the old charting system goes - # away, if this code ever runs, it'll just find no files and do nothing. - my $series_exists = $dbh->selectrow_array("SELECT 1 FROM series " . - $dbh->sql_limit(1)); - if (!$series_exists && -d "$datadir/mining" && -e "$datadir/mining/-All-") { - print "Migrating old chart data into database...\n"; - - # We prepare the handle to insert the series data - my $seriesdatasth = $dbh->prepare( - "INSERT INTO series_data (series_id, series_date, series_value) - VALUES (?, ?, ?)"); - - my $deletesth = $dbh->prepare( - "DELETE FROM series_data WHERE series_id = ? AND series_date = ?"); - - my $groupmapsth = $dbh->prepare( - "INSERT INTO category_group_map (category_id, group_id) - VALUES (?, ?)"); - - # Fields in the data file (matches the current collectstats.pl) - my @statuses = - qw(NEW ASSIGNED REOPENED UNCONFIRMED RESOLVED VERIFIED CLOSED); - my @resolutions = - qw(FIXED INVALID WONTFIX LATER REMIND DUPLICATE WORKSFORME MOVED); - my @fields = (@statuses, @resolutions); - - # We have a localization problem here. Where do we get these values? - my $all_name = "-All-"; - my $open_name = "All Open"; - - $dbh->bz_start_transaction(); - my $products = $dbh->selectall_arrayref("SELECT name FROM products"); - - foreach my $product ((map { $_->[0] } @$products), "-All-") { - print "$product:\n"; - # First, create the series - my %queries; - my %seriesids; - - my $query_prod = ""; - if ($product ne "-All-") { - $query_prod = "product=" . html_quote($product) . "&"; - } + my $dbh = Bugzilla->dbh; + my $datadir = bz_locations()->{'datadir'}; + + # 2003-06-26 Copy the old charting data into the database, and create the + # queries that will keep it all running. When the old charting system goes + # away, if this code ever runs, it'll just find no files and do nothing. + my $series_exists + = $dbh->selectrow_array("SELECT 1 FROM series " . $dbh->sql_limit(1)); + if (!$series_exists && -d "$datadir/mining" && -e "$datadir/mining/-All-") { + print "Migrating old chart data into database...\n"; + + # We prepare the handle to insert the series data + my $seriesdatasth = $dbh->prepare( + "INSERT INTO series_data (series_id, series_date, series_value) + VALUES (?, ?, ?)" + ); - # The query for statuses is different to that for resolutions. - $queries{$_} = ($query_prod . "bug_status=$_") foreach (@statuses); - $queries{$_} = ($query_prod . "resolution=$_") - foreach (@resolutions); - - foreach my $field (@fields) { - # Create a Series for each field in this product. - my $series = new Bugzilla::Series(undef, $product, $all_name, - $field, undef, 1, - $queries{$field}, 1); - $series->writeToDatabase(); - $seriesids{$field} = $series->{'series_id'}; - } + my $deletesth = $dbh->prepare( + "DELETE FROM series_data WHERE series_id = ? AND series_date = ?"); - # We also add a new query for "Open", so that migrated products get - # the same set as new products (see editproducts.cgi.) - my @openedstatuses = ("UNCONFIRMED", "NEW", "ASSIGNED", "REOPENED"); - my $query = join("&", map { "bug_status=$_" } @openedstatuses); - my $series = new Bugzilla::Series(undef, $product, $all_name, - $open_name, undef, 1, - $query_prod . $query, 1); - $series->writeToDatabase(); - $seriesids{$open_name} = $series->{'series_id'}; - - # Now, we attempt to read in historical data, if any - # Convert the name in the same way that collectstats.pl does - my $product_file = $product; - $product_file =~ s/\//-/gs; - $product_file = "$datadir/mining/$product_file"; - - # There are many reasons that this might fail (e.g. no stats - # for this product), so we don't worry if it does. - my $in = new IO::File($product_file) or next; - - # The data files should be in a standard format, even for old - # Bugzillas, because of the conversion code further up this file. - my %data; - my $last_date = ""; - - my @lines = <$in>; - while (my $line = shift @lines) { - if ($line =~ /^(\d+\|.*)/) { - my @numbers = split(/\||\r/, $1); - - # Only take the first line for each date; it was possible to - # run collectstats.pl more than once in a day. - next if $numbers[0] eq $last_date; - - for my $i (0 .. $#fields) { - # $numbers[0] is the date - $data{$fields[$i]}{$numbers[0]} = $numbers[$i + 1]; - - # Keep a total of the number of open bugs for this day - if (grep { $_ eq $fields[$i] } @openedstatuses) { - $data{$open_name}{$numbers[0]} += $numbers[$i + 1]; - } - } - - $last_date = $numbers[0]; - } - } + my $groupmapsth = $dbh->prepare( + "INSERT INTO category_group_map (category_id, group_id) + VALUES (?, ?)" + ); - $in->close; - - my $total_items = (scalar(@fields) + 1) - * scalar(keys %{ $data{'NEW'} }); - my $count = 0; - foreach my $field (@fields, $open_name) { - # Insert values into series_data: series_id, date, value - my %fielddata = %{$data{$field}}; - foreach my $date (keys %fielddata) { - # We need to delete in case the text file had duplicate - # entries in it. - $deletesth->execute($seriesids{$field}, $date); - - # We prepared this above - $seriesdatasth->execute($seriesids{$field}, - $date, $fielddata{$date} || 0); - indicate_progress({ total => $total_items, - current => ++$count, every => 100 }); - } - } + # Fields in the data file (matches the current collectstats.pl) + my @statuses = qw(NEW ASSIGNED REOPENED UNCONFIRMED RESOLVED VERIFIED CLOSED); + my @resolutions + = qw(FIXED INVALID WONTFIX LATER REMIND DUPLICATE WORKSFORME MOVED); + my @fields = (@statuses, @resolutions); + + # We have a localization problem here. Where do we get these values? + my $all_name = "-All-"; + my $open_name = "All Open"; - # Create the groupsets for the category - my $category_id = - $dbh->selectrow_array("SELECT id FROM series_categories " . - "WHERE name = " . $dbh->quote($product)); - my $product_id = - $dbh->selectrow_array("SELECT id FROM products " . - "WHERE name = " . $dbh->quote($product)); - - if (defined($category_id) && defined($product_id)) { - - # Get all the mandatory groups for this product - my $group_ids = - $dbh->selectcol_arrayref("SELECT group_id " . - "FROM group_control_map " . - "WHERE product_id = $product_id " . - "AND (membercontrol = " . CONTROLMAPMANDATORY . - " OR othercontrol = " . CONTROLMAPMANDATORY . ")"); - - foreach my $group_id (@$group_ids) { - $groupmapsth->execute($category_id, $group_id); - } + $dbh->bz_start_transaction(); + my $products = $dbh->selectall_arrayref("SELECT name FROM products"); + + foreach my $product ((map { $_->[0] } @$products), "-All-") { + print "$product:\n"; + + # First, create the series + my %queries; + my %seriesids; + + my $query_prod = ""; + if ($product ne "-All-") { + $query_prod = "product=" . html_quote($product) . "&"; + } + + # The query for statuses is different to that for resolutions. + $queries{$_} = ($query_prod . "bug_status=$_") foreach (@statuses); + $queries{$_} = ($query_prod . "resolution=$_") foreach (@resolutions); + + foreach my $field (@fields) { + + # Create a Series for each field in this product. + my $series = new Bugzilla::Series(undef, $product, $all_name, $field, undef, 1, + $queries{$field}, 1); + $series->writeToDatabase(); + $seriesids{$field} = $series->{'series_id'}; + } + + # We also add a new query for "Open", so that migrated products get + # the same set as new products (see editproducts.cgi.) + my @openedstatuses = ("UNCONFIRMED", "NEW", "ASSIGNED", "REOPENED"); + my $query = join("&", map {"bug_status=$_"} @openedstatuses); + my $series + = new Bugzilla::Series(undef, $product, $all_name, $open_name, undef, 1, + $query_prod . $query, 1); + $series->writeToDatabase(); + $seriesids{$open_name} = $series->{'series_id'}; + + # Now, we attempt to read in historical data, if any + # Convert the name in the same way that collectstats.pl does + my $product_file = $product; + $product_file =~ s/\//-/gs; + $product_file = "$datadir/mining/$product_file"; + + # There are many reasons that this might fail (e.g. no stats + # for this product), so we don't worry if it does. + my $in = new IO::File($product_file) or next; + + # The data files should be in a standard format, even for old + # Bugzillas, because of the conversion code further up this file. + my %data; + my $last_date = ""; + + my @lines = <$in>; + while (my $line = shift @lines) { + if ($line =~ /^(\d+\|.*)/) { + my @numbers = split(/\||\r/, $1); + + # Only take the first line for each date; it was possible to + # run collectstats.pl more than once in a day. + next if $numbers[0] eq $last_date; + + for my $i (0 .. $#fields) { + + # $numbers[0] is the date + $data{$fields[$i]}{$numbers[0]} = $numbers[$i + 1]; + + # Keep a total of the number of open bugs for this day + if (grep { $_ eq $fields[$i] } @openedstatuses) { + $data{$open_name}{$numbers[0]} += $numbers[$i + 1]; } + } + + $last_date = $numbers[0]; } + } + + $in->close; + + my $total_items = (scalar(@fields) + 1) * scalar(keys %{$data{'NEW'}}); + my $count = 0; + foreach my $field (@fields, $open_name) { + + # Insert values into series_data: series_id, date, value + my %fielddata = %{$data{$field}}; + foreach my $date (keys %fielddata) { + + # We need to delete in case the text file had duplicate + # entries in it. + $deletesth->execute($seriesids{$field}, $date); - $dbh->bz_commit_transaction(); + # We prepared this above + $seriesdatasth->execute($seriesids{$field}, $date, $fielddata{$date} || 0); + indicate_progress({total => $total_items, current => ++$count, every => 100}); + } + } + + # Create the groupsets for the category + my $category_id = $dbh->selectrow_array( + "SELECT id FROM series_categories " . "WHERE name = " . $dbh->quote($product)); + my $product_id = $dbh->selectrow_array( + "SELECT id FROM products " . "WHERE name = " . $dbh->quote($product)); + + if (defined($category_id) && defined($product_id)) { + + # Get all the mandatory groups for this product + my $group_ids + = $dbh->selectcol_arrayref("SELECT group_id " + . "FROM group_control_map " + . "WHERE product_id = $product_id " + . "AND (membercontrol = " + . CONTROLMAPMANDATORY + . " OR othercontrol = " + . CONTROLMAPMANDATORY + . ")"); + + foreach my $group_id (@$group_ids) { + $groupmapsth->execute($category_id, $group_id); + } + } } + + $dbh->bz_commit_transaction(); + } } sub _add_user_group_map_grant_type { - my $dbh = Bugzilla->dbh; - # 2004-04-12 - Keep regexp-based group permissions up-to-date - Bug 240325 - if ($dbh->bz_column_info("user_group_map", "isderived")) { - $dbh->bz_add_column('user_group_map', 'grant_type', - {TYPE => 'INT1', NOTNULL => 1, DEFAULT => '0'}); - $dbh->do("DELETE FROM user_group_map WHERE isderived != 0"); - $dbh->do("UPDATE user_group_map SET grant_type = " . GRANT_DIRECT); - $dbh->bz_drop_column("user_group_map", "isderived"); - - $dbh->bz_drop_index('user_group_map', 'user_group_map_user_id_idx'); - $dbh->bz_add_index('user_group_map', 'user_group_map_user_id_idx', - {TYPE => 'UNIQUE', - FIELDS => [qw(user_id group_id grant_type isbless)]}); - } + my $dbh = Bugzilla->dbh; + + # 2004-04-12 - Keep regexp-based group permissions up-to-date - Bug 240325 + if ($dbh->bz_column_info("user_group_map", "isderived")) { + $dbh->bz_add_column('user_group_map', 'grant_type', + {TYPE => 'INT1', NOTNULL => 1, DEFAULT => '0'}); + $dbh->do("DELETE FROM user_group_map WHERE isderived != 0"); + $dbh->do("UPDATE user_group_map SET grant_type = " . GRANT_DIRECT); + $dbh->bz_drop_column("user_group_map", "isderived"); + + $dbh->bz_drop_index('user_group_map', 'user_group_map_user_id_idx'); + $dbh->bz_add_index('user_group_map', 'user_group_map_user_id_idx', + {TYPE => 'UNIQUE', FIELDS => [qw(user_id group_id grant_type isbless)]}); + } } sub _add_group_group_map_grant_type { - my $dbh = Bugzilla->dbh; - # 2004-07-16 - Make it possible to have group-group relationships other than - # membership and bless. - if ($dbh->bz_column_info("group_group_map", "isbless")) { - $dbh->bz_add_column('group_group_map', 'grant_type', - {TYPE => 'INT1', NOTNULL => 1, DEFAULT => '0'}); - $dbh->do("UPDATE group_group_map SET grant_type = " . - "IF(isbless, " . GROUP_BLESS . ", " . - GROUP_MEMBERSHIP . ")"); - $dbh->bz_drop_index('group_group_map', 'group_group_map_member_id_idx'); - $dbh->bz_drop_column("group_group_map", "isbless"); - $dbh->bz_add_index('group_group_map', 'group_group_map_member_id_idx', - {TYPE => 'UNIQUE', - FIELDS => [qw(member_id grantor_id grant_type)]}); - } + my $dbh = Bugzilla->dbh; + + # 2004-07-16 - Make it possible to have group-group relationships other than + # membership and bless. + if ($dbh->bz_column_info("group_group_map", "isbless")) { + $dbh->bz_add_column('group_group_map', 'grant_type', + {TYPE => 'INT1', NOTNULL => 1, DEFAULT => '0'}); + $dbh->do("UPDATE group_group_map SET grant_type = " + . "IF(isbless, " + . GROUP_BLESS . ", " + . GROUP_MEMBERSHIP + . ")"); + $dbh->bz_drop_index('group_group_map', 'group_group_map_member_id_idx'); + $dbh->bz_drop_column("group_group_map", "isbless"); + $dbh->bz_add_index( + 'group_group_map', + 'group_group_map_member_id_idx', + {TYPE => 'UNIQUE', FIELDS => [qw(member_id grantor_id grant_type)]} + ); + } } sub _add_longdescs_already_wrapped { - my $dbh = Bugzilla->dbh; - # 2005-01-29 - mkanat@bugzilla.org - if (!$dbh->bz_column_info('longdescs', 'already_wrapped')) { - # Old, pre-wrapped comments should not be auto-wrapped - $dbh->bz_add_column('longdescs', 'already_wrapped', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, 1); - # If an old comment doesn't have a newline in the first 81 characters, - # (or doesn't contain a newline at all) and it contains a space, - # then it's probably a mis-wrapped comment and we should wrap it - # at display-time. - print "Fixing old, mis-wrapped comments...\n"; - $dbh->do(q{UPDATE longdescs SET already_wrapped = 0 + my $dbh = Bugzilla->dbh; + + # 2005-01-29 - mkanat@bugzilla.org + if (!$dbh->bz_column_info('longdescs', 'already_wrapped')) { + + # Old, pre-wrapped comments should not be auto-wrapped + $dbh->bz_add_column('longdescs', 'already_wrapped', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, 1); + + # If an old comment doesn't have a newline in the first 81 characters, + # (or doesn't contain a newline at all) and it contains a space, + # then it's probably a mis-wrapped comment and we should wrap it + # at display-time. + print "Fixing old, mis-wrapped comments...\n"; + $dbh->do( + q{UPDATE longdescs SET already_wrapped = 0 WHERE (} . $dbh->sql_position(q{'\n'}, 'thetext') . q{ > 81 OR } . $dbh->sql_position(q{'\n'}, 'thetext') . q{ = 0) - AND SUBSTRING(thetext FROM 1 FOR 80) LIKE '% %'}); - } + AND SUBSTRING(thetext FROM 1 FOR 80) LIKE '% %'} + ); + } } sub _convert_attachments_filename_from_mediumtext { - my $dbh = Bugzilla->dbh; - # 2002 November, myk@mozilla.org, bug 178841: - # - # Convert the "attachments.filename" column from a ridiculously large - # "mediumtext" to a much more sensible "varchar(100)". Also takes - # the opportunity to remove paths from existing filenames, since they - # shouldn't be there for security. Buggy browsers include them, - # and attachment.cgi now takes them out, but old ones need converting. - my $ref = $dbh->bz_column_info("attachments", "filename"); - if ($ref->{TYPE} ne 'varchar(100)' && $ref->{TYPE} ne 'varchar(255)') { - print "Removing paths from filenames in attachments table..."; - - my $sth = $dbh->prepare("SELECT attach_id, filename FROM attachments " . - "WHERE " . $dbh->sql_position(q{'/'}, 'filename') . " > 0 OR " . - $dbh->sql_position(q{'\\\\'}, 'filename') . " > 0"); - $sth->execute; - - while (my ($attach_id, $filename) = $sth->fetchrow_array) { - $filename =~ s/^.*[\/\\]//; - my $quoted_filename = $dbh->quote($filename); - $dbh->do("UPDATE attachments SET filename = $quoted_filename " . - "WHERE attach_id = $attach_id"); - } + my $dbh = Bugzilla->dbh; + + # 2002 November, myk@mozilla.org, bug 178841: + # + # Convert the "attachments.filename" column from a ridiculously large + # "mediumtext" to a much more sensible "varchar(100)". Also takes + # the opportunity to remove paths from existing filenames, since they + # shouldn't be there for security. Buggy browsers include them, + # and attachment.cgi now takes them out, but old ones need converting. + my $ref = $dbh->bz_column_info("attachments", "filename"); + if ($ref->{TYPE} ne 'varchar(100)' && $ref->{TYPE} ne 'varchar(255)') { + print "Removing paths from filenames in attachments table..."; + + my $sth + = $dbh->prepare("SELECT attach_id, filename FROM attachments " + . "WHERE " + . $dbh->sql_position(q{'/'}, 'filename') + . " > 0 OR " + . $dbh->sql_position(q{'\\\\'}, 'filename') + . " > 0"); + $sth->execute; + + while (my ($attach_id, $filename) = $sth->fetchrow_array) { + $filename =~ s/^.*[\/\\]//; + my $quoted_filename = $dbh->quote($filename); + $dbh->do("UPDATE attachments SET filename = $quoted_filename " + . "WHERE attach_id = $attach_id"); + } - print "Done.\n"; + print "Done.\n"; - $dbh->bz_alter_column("attachments", "filename", - {TYPE => 'varchar(100)', NOTNULL => 1}); - } + $dbh->bz_alter_column("attachments", "filename", + {TYPE => 'varchar(100)', NOTNULL => 1}); + } } sub _rename_votes_count_and_force_group_refresh { - my $dbh = Bugzilla->dbh; - # 2003-04-27 - bugzilla@chimpychompy.org (GavinS) - # - # Bug 180086 (http://bugzilla.mozilla.org/show_bug.cgi?id=180086) - # - # Renaming the 'count' column in the votes table because Sybase doesn't - # like it - return if !$dbh->bz_table_info('votes'); - return if $dbh->bz_column_info('votes', 'count'); - $dbh->bz_rename_column('votes', 'count', 'vote_count'); + my $dbh = Bugzilla->dbh; + + # 2003-04-27 - bugzilla@chimpychompy.org (GavinS) + # + # Bug 180086 (http://bugzilla.mozilla.org/show_bug.cgi?id=180086) + # + # Renaming the 'count' column in the votes table because Sybase doesn't + # like it + return if !$dbh->bz_table_info('votes'); + return if $dbh->bz_column_info('votes', 'count'); + $dbh->bz_rename_column('votes', 'count', 'vote_count'); } sub _fix_group_with_empty_name { - my $dbh = Bugzilla->dbh; - # 2005-01-12 Nick Barnes bug 278010 - # Rename any group which has an empty name. - # Note that there can be at most one such group (because of - # the SQL index on the name column). - my ($emptygroupid) = $dbh->selectrow_array( - "SELECT id FROM groups where name = ''"); - if ($emptygroupid) { - # There is a group with an empty name; find a name to rename it - # as. Must avoid collisions with existing names. Start with - # group_$gid and add _ if necessary. - my $trycount = 0; - my $trygroupname; - my $sth = $dbh->prepare("SELECT 1 FROM groups where name = ?"); - my $name_exists = 1; - - while ($name_exists) { - $trygroupname = "group_$emptygroupid"; - if ($trycount > 0) { - $trygroupname .= "_$trycount"; - } - $name_exists = $dbh->selectrow_array($sth, undef, $trygroupname); - $trycount++; - } - $dbh->do("UPDATE groups SET name = ? WHERE id = ?", - undef, $trygroupname, $emptygroupid); - print "Group $emptygroupid had an empty name; renamed as", - " '$trygroupname'.\n"; + my $dbh = Bugzilla->dbh; + + # 2005-01-12 Nick Barnes bug 278010 + # Rename any group which has an empty name. + # Note that there can be at most one such group (because of + # the SQL index on the name column). + my ($emptygroupid) + = $dbh->selectrow_array("SELECT id FROM groups where name = ''"); + if ($emptygroupid) { + + # There is a group with an empty name; find a name to rename it + # as. Must avoid collisions with existing names. Start with + # group_$gid and add _ if necessary. + my $trycount = 0; + my $trygroupname; + my $sth = $dbh->prepare("SELECT 1 FROM groups where name = ?"); + my $name_exists = 1; + + while ($name_exists) { + $trygroupname = "group_$emptygroupid"; + if ($trycount > 0) { + $trygroupname .= "_$trycount"; + } + $name_exists = $dbh->selectrow_array($sth, undef, $trygroupname); + $trycount++; } + $dbh->do("UPDATE groups SET name = ? WHERE id = ?", + undef, $trygroupname, $emptygroupid); + print "Group $emptygroupid had an empty name; renamed as", + " '$trygroupname'.\n"; + } } # A helper for the emailprefs subs below sub _clone_email_event { - my ($source, $target) = @_; - my $dbh = Bugzilla->dbh; + my ($source, $target) = @_; + my $dbh = Bugzilla->dbh; - $dbh->do("INSERT INTO email_setting (user_id, relationship, event) + $dbh->do( + "INSERT INTO email_setting (user_id, relationship, event) SELECT user_id, relationship, $target FROM email_setting - WHERE event = $source"); + WHERE event = $source" + ); } sub _migrate_email_prefs_to_new_table { - my $dbh = Bugzilla->dbh; - # 2005-03-29 - gerv@gerv.net - bug 73665. - # Migrate email preferences to new email prefs table. - if ($dbh->bz_column_info("profiles", "emailflags")) { - print "Migrating email preferences to new table...\n"; - - # These are the "roles" and "reasons" from the original code, mapped to - # the new terminology of relationships and events. - my %relationships = ("Owner" => REL_ASSIGNEE, - "Reporter" => REL_REPORTER, - "QAcontact" => REL_QA, - "CClist" => REL_CC, - # REL_VOTER was "4" before it was moved to an - # extension. - "Voter" => 4); - - my %events = ("Removeme" => EVT_ADDED_REMOVED, - "Comments" => EVT_COMMENT, - "Attachments" => EVT_ATTACHMENT, - "Status" => EVT_PROJ_MANAGEMENT, - "Resolved" => EVT_OPENED_CLOSED, - "Keywords" => EVT_KEYWORD, - "CC" => EVT_CC, - "Other" => EVT_OTHER, - "Unconfirmed" => EVT_UNCONFIRMED); - - # Request preferences - my %requestprefs = ("FlagRequestee" => EVT_FLAG_REQUESTED, - "FlagRequester" => EVT_REQUESTED_FLAG); - - # We run the below code in a transaction to speed things up. - $dbh->bz_start_transaction(); - - # Select all emailflags flag strings - my $total = $dbh->selectrow_array('SELECT COUNT(*) FROM profiles'); - my $sth = $dbh->prepare("SELECT userid, emailflags FROM profiles"); - $sth->execute(); - my $i = 0; - - while (my ($userid, $flagstring) = $sth->fetchrow_array()) { - $i++; - indicate_progress({ total => $total, current => $i, every => 10 }); - # If the user has never logged in since emailprefs arrived, and the - # temporary code to give them a default string never ran, then - # $flagstring will be null. In this case, they just get all mail. - $flagstring ||= ""; - - # The 255 param is here, because without a third param, split will - # trim any trailing null fields, which causes Perl to eject lots of - # warnings. Any suitably large number would do. - my %emailflags = split(/~/, $flagstring, 255); - - my $sth2 = $dbh->prepare("INSERT into email_setting " . - "(user_id, relationship, event) VALUES (" . - "$userid, ?, ?)"); - foreach my $relationship (keys %relationships) { - foreach my $event (keys %events) { - my $key = "email$relationship$event"; - if (!exists($emailflags{$key}) - || $emailflags{$key} eq 'on') - { - $sth2->execute($relationships{$relationship}, - $events{$event}); - } - } - } - # Note that in the old system, the value of "excludeself" is - # assumed to be off if the preference does not exist in the - # user's list, unlike other preferences whose value is - # assumed to be on if they do not exist. - # - # This preference has changed from global to per-relationship. - if (!exists($emailflags{'ExcludeSelf'}) - || $emailflags{'ExcludeSelf'} ne 'on') - { - foreach my $relationship (keys %relationships) { - $dbh->do("INSERT into email_setting " . - "(user_id, relationship, event) VALUES (" . - $userid . ", " . - $relationships{$relationship}. ", " . - EVT_CHANGED_BY_ME . ")"); - } - } + my $dbh = Bugzilla->dbh; + + # 2005-03-29 - gerv@gerv.net - bug 73665. + # Migrate email preferences to new email prefs table. + if ($dbh->bz_column_info("profiles", "emailflags")) { + print "Migrating email preferences to new table...\n"; + + # These are the "roles" and "reasons" from the original code, mapped to + # the new terminology of relationships and events. + my %relationships = ( + "Owner" => REL_ASSIGNEE, + "Reporter" => REL_REPORTER, + "QAcontact" => REL_QA, + "CClist" => REL_CC, + + # REL_VOTER was "4" before it was moved to an + # extension. + "Voter" => 4 + ); - foreach my $key (keys %requestprefs) { - if (!exists($emailflags{$key}) || $emailflags{$key} eq 'on') { - $dbh->do("INSERT into email_setting " . - "(user_id, relationship, event) VALUES (" . - $userid . ", " . REL_ANY . ", " . - $requestprefs{$key} . ")"); - } - } - } - print "\n"; + my %events = ( + "Removeme" => EVT_ADDED_REMOVED, + "Comments" => EVT_COMMENT, + "Attachments" => EVT_ATTACHMENT, + "Status" => EVT_PROJ_MANAGEMENT, + "Resolved" => EVT_OPENED_CLOSED, + "Keywords" => EVT_KEYWORD, + "CC" => EVT_CC, + "Other" => EVT_OTHER, + "Unconfirmed" => EVT_UNCONFIRMED + ); - # EVT_ATTACHMENT_DATA should initially have identical settings to - # EVT_ATTACHMENT. - _clone_email_event(EVT_ATTACHMENT, EVT_ATTACHMENT_DATA); + # Request preferences + my %requestprefs = ( + "FlagRequestee" => EVT_FLAG_REQUESTED, + "FlagRequester" => EVT_REQUESTED_FLAG + ); - $dbh->bz_commit_transaction(); - $dbh->bz_drop_column("profiles", "emailflags"); + # We run the below code in a transaction to speed things up. + $dbh->bz_start_transaction(); + + # Select all emailflags flag strings + my $total = $dbh->selectrow_array('SELECT COUNT(*) FROM profiles'); + my $sth = $dbh->prepare("SELECT userid, emailflags FROM profiles"); + $sth->execute(); + my $i = 0; + + while (my ($userid, $flagstring) = $sth->fetchrow_array()) { + $i++; + indicate_progress({total => $total, current => $i, every => 10}); + + # If the user has never logged in since emailprefs arrived, and the + # temporary code to give them a default string never ran, then + # $flagstring will be null. In this case, they just get all mail. + $flagstring ||= ""; + + # The 255 param is here, because without a third param, split will + # trim any trailing null fields, which causes Perl to eject lots of + # warnings. Any suitably large number would do. + my %emailflags = split(/~/, $flagstring, 255); + + my $sth2 + = $dbh->prepare("INSERT into email_setting " + . "(user_id, relationship, event) VALUES (" + . "$userid, ?, ?)"); + foreach my $relationship (keys %relationships) { + foreach my $event (keys %events) { + my $key = "email$relationship$event"; + if (!exists($emailflags{$key}) || $emailflags{$key} eq 'on') { + $sth2->execute($relationships{$relationship}, $events{$event}); + } + } + } + + # Note that in the old system, the value of "excludeself" is + # assumed to be off if the preference does not exist in the + # user's list, unlike other preferences whose value is + # assumed to be on if they do not exist. + # + # This preference has changed from global to per-relationship. + if (!exists($emailflags{'ExcludeSelf'}) || $emailflags{'ExcludeSelf'} ne 'on') { + foreach my $relationship (keys %relationships) { + $dbh->do("INSERT into email_setting " + . "(user_id, relationship, event) VALUES (" + . $userid . ", " + . $relationships{$relationship} . ", " + . EVT_CHANGED_BY_ME + . ")"); + } + } + + foreach my $key (keys %requestprefs) { + if (!exists($emailflags{$key}) || $emailflags{$key} eq 'on') { + $dbh->do("INSERT into email_setting " + . "(user_id, relationship, event) VALUES (" + . $userid . ", " + . REL_ANY . ", " + . $requestprefs{$key} + . ")"); + } + } } + print "\n"; + + # EVT_ATTACHMENT_DATA should initially have identical settings to + # EVT_ATTACHMENT. + _clone_email_event(EVT_ATTACHMENT, EVT_ATTACHMENT_DATA); + + $dbh->bz_commit_transaction(); + $dbh->bz_drop_column("profiles", "emailflags"); + } } sub _initialize_new_email_prefs { - my $dbh = Bugzilla->dbh; - # Check for any "new" email settings that wouldn't have been ported over - # during the block above. Since these settings would have otherwise - # fallen under EVT_OTHER, we'll just clone those settings. That way if - # folks have already disabled all of that mail, there won't be any change. - my %events = ( - "Dependency Tree Changes" => EVT_DEPEND_BLOCK, - "Product/Component Changes" => EVT_COMPONENT, - ); - - foreach my $desc (keys %events) { - my $event = $events{$desc}; - my $have_events = $dbh->selectrow_array( - "SELECT 1 FROM email_setting WHERE event = $event " - . $dbh->sql_limit(1)); - - if (!$have_events) { - # No settings in the table yet, so we assume that this is the - # first time it's being set. - print "Initializing \"$desc\" email_setting ...\n"; - _clone_email_event(EVT_OTHER, $event); - } + my $dbh = Bugzilla->dbh; + + # Check for any "new" email settings that wouldn't have been ported over + # during the block above. Since these settings would have otherwise + # fallen under EVT_OTHER, we'll just clone those settings. That way if + # folks have already disabled all of that mail, there won't be any change. + my %events = ( + "Dependency Tree Changes" => EVT_DEPEND_BLOCK, + "Product/Component Changes" => EVT_COMPONENT, + ); + + foreach my $desc (keys %events) { + my $event = $events{$desc}; + my $have_events = $dbh->selectrow_array( + "SELECT 1 FROM email_setting WHERE event = $event " . $dbh->sql_limit(1)); + + if (!$have_events) { + + # No settings in the table yet, so we assume that this is the + # first time it's being set. + print "Initializing \"$desc\" email_setting ...\n"; + _clone_email_event(EVT_OTHER, $event); } + } } sub _change_all_mysql_booleans_to_tinyint { - my $dbh = Bugzilla->dbh; - # 2005-03-27: Standardize all boolean fields to plain "tinyint" - if ( $dbh->isa('Bugzilla::DB::Mysql') ) { - # This is a change to make things consistent with Schema, so we use - # direct-database access methods. - my $quip_info_sth = $dbh->column_info(undef, undef, 'quips', '%'); - my $quips_cols = $quip_info_sth->fetchall_hashref("COLUMN_NAME"); - my $approved_col = $quips_cols->{'approved'}; - if ( $approved_col->{TYPE_NAME} eq 'TINYINT' - and $approved_col->{COLUMN_SIZE} == 1 ) - { - # series.public could have been renamed to series.is_public, - # and so wouldn't need to be fixed manually. - if ($dbh->bz_column_info('series', 'public')) { - $dbh->bz_alter_column_raw('series', 'public', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => '0'}); - } - $dbh->bz_alter_column_raw('bug_status', 'isactive', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => '1'}); - $dbh->bz_alter_column_raw('rep_platform', 'isactive', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => '1'}); - $dbh->bz_alter_column_raw('resolution', 'isactive', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => '1'}); - $dbh->bz_alter_column_raw('op_sys', 'isactive', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => '1'}); - $dbh->bz_alter_column_raw('bug_severity', 'isactive', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => '1'}); - $dbh->bz_alter_column_raw('priority', 'isactive', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => '1'}); - $dbh->bz_alter_column_raw('quips', 'approved', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => '1'}); - } - } + my $dbh = Bugzilla->dbh; + + # 2005-03-27: Standardize all boolean fields to plain "tinyint" + if ($dbh->isa('Bugzilla::DB::Mysql')) { + + # This is a change to make things consistent with Schema, so we use + # direct-database access methods. + my $quip_info_sth = $dbh->column_info(undef, undef, 'quips', '%'); + my $quips_cols = $quip_info_sth->fetchall_hashref("COLUMN_NAME"); + my $approved_col = $quips_cols->{'approved'}; + if ( $approved_col->{TYPE_NAME} eq 'TINYINT' + and $approved_col->{COLUMN_SIZE} == 1) + { + # series.public could have been renamed to series.is_public, + # and so wouldn't need to be fixed manually. + if ($dbh->bz_column_info('series', 'public')) { + $dbh->bz_alter_column_raw('series', 'public', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => '0'}); + } + $dbh->bz_alter_column_raw('bug_status', 'isactive', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => '1'}); + $dbh->bz_alter_column_raw('rep_platform', 'isactive', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => '1'}); + $dbh->bz_alter_column_raw('resolution', 'isactive', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => '1'}); + $dbh->bz_alter_column_raw('op_sys', 'isactive', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => '1'}); + $dbh->bz_alter_column_raw('bug_severity', 'isactive', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => '1'}); + $dbh->bz_alter_column_raw('priority', 'isactive', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => '1'}); + $dbh->bz_alter_column_raw('quips', 'approved', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => '1'}); + } + } } # A helper for the below function. sub _de_dup_version { - my ($product_id, $version) = @_; - my $dbh = Bugzilla->dbh; - print "Fixing duplicate version $version in product_id $product_id...\n"; - $dbh->do('DELETE FROM versions WHERE product_id = ? AND value = ?', - undef, $product_id, $version); - $dbh->do('INSERT INTO versions (product_id, value) VALUES (?,?)', - undef, $product_id, $version); + my ($product_id, $version) = @_; + my $dbh = Bugzilla->dbh; + print "Fixing duplicate version $version in product_id $product_id...\n"; + $dbh->do('DELETE FROM versions WHERE product_id = ? AND value = ?', + undef, $product_id, $version); + $dbh->do('INSERT INTO versions (product_id, value) VALUES (?,?)', + undef, $product_id, $version); } sub _add_versions_product_id_index { - my $dbh = Bugzilla->dbh; - if (!$dbh->bz_index_info('versions', 'versions_product_id_idx')) { - my $dup_versions = $dbh->selectall_arrayref( - 'SELECT product_id, value FROM versions - GROUP BY product_id, value HAVING COUNT(value) > 1', {Slice=>{}}); - foreach my $dup_version (@$dup_versions) { - _de_dup_version($dup_version->{product_id}, $dup_version->{value}); - } - - $dbh->bz_add_index('versions', 'versions_product_id_idx', - {TYPE => 'UNIQUE', FIELDS => [qw(product_id value)]}); + my $dbh = Bugzilla->dbh; + if (!$dbh->bz_index_info('versions', 'versions_product_id_idx')) { + my $dup_versions = $dbh->selectall_arrayref( + 'SELECT product_id, value FROM versions + GROUP BY product_id, value HAVING COUNT(value) > 1', {Slice => {}} + ); + foreach my $dup_version (@$dup_versions) { + _de_dup_version($dup_version->{product_id}, $dup_version->{value}); } + + $dbh->bz_add_index('versions', 'versions_product_id_idx', + {TYPE => 'UNIQUE', FIELDS => [qw(product_id value)]}); + } } sub _fix_whine_queries_title_and_op_sys_value { - my $dbh = Bugzilla->dbh; - if (!exists $dbh->bz_column_info('whine_queries', 'title')->{DEFAULT}) { - # The below change actually has nothing to do with the whine_queries - # change, it just has to be contained within a schema change so that - # it doesn't run every time we run checksetup. - - # Old Bugzillas have "other" as an OS choice, new ones have "Other" - # (capital O). - print "Setting any 'other' op_sys to 'Other'...\n"; - $dbh->do('UPDATE op_sys SET value = ? WHERE value = ?', - undef, "Other", "other"); - $dbh->do('UPDATE bugs SET op_sys = ? WHERE op_sys = ?', - undef, "Other", "other"); - if (Bugzilla->params->{'defaultopsys'} eq 'other') { - # We can't actually fix the param here, because WriteParams() will - # make $datadir/params.json unwriteable to the webservergroup. - # It's too much of an ugly hack to copy the permission-fixing code - # down to here. (It would create more potential future bugs than - # it would solve problems.) - print "WARNING: Your 'defaultopsys' param is set to 'other', but" - . " Bugzilla now\n" - . " uses 'Other' (capital O).\n"; - } - - # Add a DEFAULT to whine_queries stuff so that editwhines.cgi - # works on PostgreSQL. - $dbh->bz_alter_column('whine_queries', 'title', {TYPE => 'varchar(128)', - NOTNULL => 1, DEFAULT => "''"}); + my $dbh = Bugzilla->dbh; + if (!exists $dbh->bz_column_info('whine_queries', 'title')->{DEFAULT}) { + + # The below change actually has nothing to do with the whine_queries + # change, it just has to be contained within a schema change so that + # it doesn't run every time we run checksetup. + + # Old Bugzillas have "other" as an OS choice, new ones have "Other" + # (capital O). + print "Setting any 'other' op_sys to 'Other'...\n"; + $dbh->do('UPDATE op_sys SET value = ? WHERE value = ?', undef, "Other", + "other"); + $dbh->do('UPDATE bugs SET op_sys = ? WHERE op_sys = ?', undef, "Other", + "other"); + if (Bugzilla->params->{'defaultopsys'} eq 'other') { + + # We can't actually fix the param here, because WriteParams() will + # make $datadir/params.json unwriteable to the webservergroup. + # It's too much of an ugly hack to copy the permission-fixing code + # down to here. (It would create more potential future bugs than + # it would solve problems.) + print "WARNING: Your 'defaultopsys' param is set to 'other', but" + . " Bugzilla now\n" + . " uses 'Other' (capital O).\n"; } + + # Add a DEFAULT to whine_queries stuff so that editwhines.cgi + # works on PostgreSQL. + $dbh->bz_alter_column('whine_queries', 'title', + {TYPE => 'varchar(128)', NOTNULL => 1, DEFAULT => "''"}); + } } sub _fix_attachments_submitter_id_idx { - my $dbh = Bugzilla->dbh; - # 2005-06-29 bugreport@peshkin.net, bug 299156 - if ($dbh->bz_index_info('attachments', 'attachments_submitter_id_idx') - && (scalar(@{$dbh->bz_index_info('attachments', - 'attachments_submitter_id_idx' - )->{FIELDS}}) < 2)) - { - $dbh->bz_drop_index('attachments', 'attachments_submitter_id_idx'); - } - $dbh->bz_add_index('attachments', 'attachments_submitter_id_idx', - [qw(submitter_id bug_id)]); + my $dbh = Bugzilla->dbh; + + # 2005-06-29 bugreport@peshkin.net, bug 299156 + if ( + $dbh->bz_index_info('attachments', 'attachments_submitter_id_idx') + && ( + scalar(@{ + $dbh->bz_index_info('attachments', 'attachments_submitter_id_idx')->{FIELDS} + }) < 2 + ) + ) + { + $dbh->bz_drop_index('attachments', 'attachments_submitter_id_idx'); + } + $dbh->bz_add_index('attachments', 'attachments_submitter_id_idx', + [qw(submitter_id bug_id)]); } sub _copy_attachments_thedata_to_attach_data { - my $dbh = Bugzilla->dbh; - # 2005-08-25 - bugreport@peshkin.net - Bug 305333 - if ($dbh->bz_column_info("attachments", "thedata")) { - print "Migrating attachment data to its own table...\n"; - print "(This may take a very long time)\n"; - $dbh->do("INSERT INTO attach_data (id, thedata) - SELECT attach_id, thedata FROM attachments"); - $dbh->bz_drop_column("attachments", "thedata"); - } + my $dbh = Bugzilla->dbh; + + # 2005-08-25 - bugreport@peshkin.net - Bug 305333 + if ($dbh->bz_column_info("attachments", "thedata")) { + print "Migrating attachment data to its own table...\n"; + print "(This may take a very long time)\n"; + $dbh->do( + "INSERT INTO attach_data (id, thedata) + SELECT attach_id, thedata FROM attachments" + ); + $dbh->bz_drop_column("attachments", "thedata"); + } } sub _fix_broken_all_closed_series { - my $dbh = Bugzilla->dbh; - - # 2005-11-26 - wurblzap@gmail.com - Bug 300473 - # Repair broken automatically generated series queries for non-open bugs. - my $broken_series_indicator = - 'field0-0-0=resolution&type0-0-0=notequals&value0-0-0=---'; - my $broken_nonopen_series = - $dbh->selectall_arrayref("SELECT series_id, query FROM series - WHERE query LIKE '$broken_series_indicator%'"); - if (@$broken_nonopen_series) { - print 'Repairing broken series...'; - my $sth_nuke = - $dbh->prepare('DELETE FROM series_data WHERE series_id = ?'); - # This statement is used to repair a series by replacing the broken - # query with the correct one. - my $sth_repair = - $dbh->prepare('UPDATE series SET query = ? WHERE series_id = ?'); - # The corresponding series for open bugs look like one of these two - # variations (bug 225687 changed the order of bug states). - # This depends on the set of bug states representing open bugs not - # to have changed since series creation. - my $open_bugs_query_base_old = - join("&", map { "bug_status=" . url_quote($_) } - ('UNCONFIRMED', 'NEW', 'ASSIGNED', 'REOPENED')); - my $open_bugs_query_base_new = - join("&", map { "bug_status=" . url_quote($_) } - ('NEW', 'REOPENED', 'ASSIGNED', 'UNCONFIRMED')); - my $sth_openbugs_series = - $dbh->prepare("SELECT series_id FROM series WHERE query IN (?, ?)"); - # Statement to find the series which has collected the most data. - my $sth_data_collected = - $dbh->prepare('SELECT count(*) FROM series_data - WHERE series_id = ?'); - # Statement to select a broken non-open bugs count data entry. - my $sth_select_broken_nonopen_data = - $dbh->prepare('SELECT series_date, series_value FROM series_data' . - ' WHERE series_id = ?'); - # Statement to select an open bugs count data entry. - my $sth_select_open_data = - $dbh->prepare('SELECT series_value FROM series_data' . - ' WHERE series_id = ? AND series_date = ?'); - # Statement to fix a broken non-open bugs count data entry. - my $sth_fix_broken_nonopen_data = - $dbh->prepare('UPDATE series_data SET series_value = ?' . - ' WHERE series_id = ? AND series_date = ?'); - # Statement to delete an unfixable broken non-open bugs count data - # entry. - my $sth_delete_broken_nonopen_data = - $dbh->prepare('DELETE FROM series_data' . - ' WHERE series_id = ? AND series_date = ?'); - foreach (@$broken_nonopen_series) { - my ($broken_series_id, $nonopen_bugs_query) = @$_; - - # Determine the product-and-component part of the query. - if ($nonopen_bugs_query =~ /^$broken_series_indicator(.*)$/) { - my $prodcomp = $1; - - # If there is more than one series for the corresponding - # open-bugs series, we pick the one with the most data, - # which should be the one which was generated on creation. - # It's a pity we can't do subselects. - $sth_openbugs_series->execute( - $open_bugs_query_base_old . $prodcomp, - $open_bugs_query_base_new . $prodcomp); - - my ($found_open_series_id, $datacount) = (undef, -1); - foreach my $open_ser_id ($sth_openbugs_series->fetchrow_array) { - $sth_data_collected->execute($open_ser_id); - my ($this_datacount) = $sth_data_collected->fetchrow_array; - if ($this_datacount > $datacount) { - $datacount = $this_datacount; - $found_open_series_id = $open_ser_id; - } - } - - if ($found_open_series_id) { - # Move along corrupted series data and correct it. The - # corruption consists of it being the number of all bugs - # instead of the number of non-open bugs, so we calculate - # the correct count by subtracting the number of open bugs. - # If there is no corresponding open-bugs count for some - # reason (shouldn't happen), we drop the data entry. - print " $broken_series_id..."; - $sth_select_broken_nonopen_data->execute($broken_series_id); - while (my $rowref = - $sth_select_broken_nonopen_data->fetchrow_arrayref) - { - my ($date, $broken_value) = @$rowref; - my ($openbugs_value) = - $dbh->selectrow_array($sth_select_open_data, undef, - $found_open_series_id, $date); - if (defined($openbugs_value)) { - $sth_fix_broken_nonopen_data->execute - ($broken_value - $openbugs_value, - $broken_series_id, $date); - } - else { - print <dbh; + + # 2005-11-26 - wurblzap@gmail.com - Bug 300473 + # Repair broken automatically generated series queries for non-open bugs. + my $broken_series_indicator + = 'field0-0-0=resolution&type0-0-0=notequals&value0-0-0=---'; + my $broken_nonopen_series = $dbh->selectall_arrayref( + "SELECT series_id, query FROM series + WHERE query LIKE '$broken_series_indicator%'" + ); + if (@$broken_nonopen_series) { + print 'Repairing broken series...'; + my $sth_nuke = $dbh->prepare('DELETE FROM series_data WHERE series_id = ?'); + + # This statement is used to repair a series by replacing the broken + # query with the correct one. + my $sth_repair + = $dbh->prepare('UPDATE series SET query = ? WHERE series_id = ?'); + + # The corresponding series for open bugs look like one of these two + # variations (bug 225687 changed the order of bug states). + # This depends on the set of bug states representing open bugs not + # to have changed since series creation. + my $open_bugs_query_base_old = join("&", + map { "bug_status=" . url_quote($_) } + ('UNCONFIRMED', 'NEW', 'ASSIGNED', 'REOPENED')); + my $open_bugs_query_base_new = join("&", + map { "bug_status=" . url_quote($_) } + ('NEW', 'REOPENED', 'ASSIGNED', 'UNCONFIRMED')); + my $sth_openbugs_series + = $dbh->prepare("SELECT series_id FROM series WHERE query IN (?, ?)"); + + # Statement to find the series which has collected the most data. + my $sth_data_collected = $dbh->prepare( + 'SELECT count(*) FROM series_data + WHERE series_id = ?' + ); + + # Statement to select a broken non-open bugs count data entry. + my $sth_select_broken_nonopen_data = $dbh->prepare( + 'SELECT series_date, series_value FROM series_data' . ' WHERE series_id = ?'); + + # Statement to select an open bugs count data entry. + my $sth_select_open_data = $dbh->prepare('SELECT series_value FROM series_data' + . ' WHERE series_id = ? AND series_date = ?'); + + # Statement to fix a broken non-open bugs count data entry. + my $sth_fix_broken_nonopen_data + = $dbh->prepare('UPDATE series_data SET series_value = ?' + . ' WHERE series_id = ? AND series_date = ?'); + + # Statement to delete an unfixable broken non-open bugs count data + # entry. + my $sth_delete_broken_nonopen_data = $dbh->prepare( + 'DELETE FROM series_data' . ' WHERE series_id = ? AND series_date = ?'); + foreach (@$broken_nonopen_series) { + my ($broken_series_id, $nonopen_bugs_query) = @$_; + + # Determine the product-and-component part of the query. + if ($nonopen_bugs_query =~ /^$broken_series_indicator(.*)$/) { + my $prodcomp = $1; + + # If there is more than one series for the corresponding + # open-bugs series, we pick the one with the most data, + # which should be the one which was generated on creation. + # It's a pity we can't do subselects. + $sth_openbugs_series->execute($open_bugs_query_base_old . $prodcomp, + $open_bugs_query_base_new . $prodcomp); + + my ($found_open_series_id, $datacount) = (undef, -1); + foreach my $open_ser_id ($sth_openbugs_series->fetchrow_array) { + $sth_data_collected->execute($open_ser_id); + my ($this_datacount) = $sth_data_collected->fetchrow_array; + if ($this_datacount > $datacount) { + $datacount = $this_datacount; + $found_open_series_id = $open_ser_id; + } + } + + if ($found_open_series_id) { + + # Move along corrupted series data and correct it. The + # corruption consists of it being the number of all bugs + # instead of the number of non-open bugs, so we calculate + # the correct count by subtracting the number of open bugs. + # If there is no corresponding open-bugs count for some + # reason (shouldn't happen), we drop the data entry. + print " $broken_series_id..."; + $sth_select_broken_nonopen_data->execute($broken_series_id); + while (my $rowref = $sth_select_broken_nonopen_data->fetchrow_arrayref) { + my ($date, $broken_value) = @$rowref; + my ($openbugs_value) + = $dbh->selectrow_array($sth_select_open_data, undef, $found_open_series_id, + $date); + if (defined($openbugs_value)) { + $sth_fix_broken_nonopen_data->execute($broken_value - $openbugs_value, + $broken_series_id, $date); + } + else { + print <execute - ($broken_series_id, $date); - } - } - - # Fix the broken query so that it collects correct data - # in the future. - $nonopen_bugs_query =~ - s/^$broken_series_indicator/field0-0-0=resolution&type0-0-0=regexp&value0-0-0=./; - $sth_repair->execute($nonopen_bugs_query, - $broken_series_id); - } - else { - print <execute($broken_series_id, $date); + } + } + + # Fix the broken query so that it collects correct data + # in the future. + $nonopen_bugs_query + =~ s/^$broken_series_indicator/field0-0-0=resolution&type0-0-0=regexp&value0-0-0=./; + $sth_repair->execute($nonopen_bugs_query, $broken_series_id); + } + else { + print <dbh; + my $dbh = Bugzilla->dbh; - my $regex_groups_exist = $dbh->selectrow_array( - "SELECT 1 FROM groups WHERE userregexp = '' " . $dbh->sql_limit(1)); - return if !$regex_groups_exist; + my $regex_groups_exist = $dbh->selectrow_array( + "SELECT 1 FROM groups WHERE userregexp = '' " . $dbh->sql_limit(1)); + return if !$regex_groups_exist; - my $regex_derivations = $dbh->selectrow_array( - 'SELECT 1 FROM user_group_map WHERE grant_type = ' . GRANT_REGEXP - . ' ' . $dbh->sql_limit(1)); - return if $regex_derivations; + my $regex_derivations + = $dbh->selectrow_array('SELECT 1 FROM user_group_map WHERE grant_type = ' + . GRANT_REGEXP . ' ' + . $dbh->sql_limit(1)); + return if $regex_derivations; - print "Deriving regex group memberships...\n"; + print "Deriving regex group memberships...\n"; - # Re-evaluate all regexps, to keep them up-to-date. - my $sth = $dbh->prepare( - "SELECT profiles.userid, profiles.login_name, groups.id, + # Re-evaluate all regexps, to keep them up-to-date. + my $sth = $dbh->prepare( + "SELECT profiles.userid, profiles.login_name, groups.id, groups.userregexp, user_group_map.group_id FROM (profiles CROSS JOIN groups) LEFT JOIN user_group_map ON user_group_map.user_id = profiles.userid AND user_group_map.group_id = groups.id AND user_group_map.grant_type = ? - WHERE userregexp != '' OR user_group_map.group_id IS NOT NULL"); + WHERE userregexp != '' OR user_group_map.group_id IS NOT NULL" + ); - my $sth_add = $dbh->prepare( - "INSERT INTO user_group_map (user_id, group_id, isbless, grant_type) - VALUES (?, ?, 0, " . GRANT_REGEXP . ")"); + my $sth_add = $dbh->prepare( + "INSERT INTO user_group_map (user_id, group_id, isbless, grant_type) + VALUES (?, ?, 0, " . GRANT_REGEXP . ")" + ); - my $sth_del = $dbh->prepare( - "DELETE FROM user_group_map + my $sth_del = $dbh->prepare( + "DELETE FROM user_group_map WHERE user_id = ? AND group_id = ? AND isbless = 0 - AND grant_type = " . GRANT_REGEXP); + AND grant_type = " . GRANT_REGEXP + ); - $sth->execute(GRANT_REGEXP); - while (my ($uid, $login, $gid, $rexp, $present) = - $sth->fetchrow_array()) - { - if ($login =~ m/$rexp/i) { - $sth_add->execute($uid, $gid) unless $present; - } else { - $sth_del->execute($uid, $gid) if $present; - } + $sth->execute(GRANT_REGEXP); + while (my ($uid, $login, $gid, $rexp, $present) = $sth->fetchrow_array()) { + if ($login =~ m/$rexp/i) { + $sth_add->execute($uid, $gid) unless $present; } + else { + $sth_del->execute($uid, $gid) if $present; + } + } } sub _clean_control_characters_from_short_desc { - my $dbh = Bugzilla->dbh; - - # Fixup for Bug 101380 - # "Newlines, nulls, leading/trailing spaces are getting into summaries" - - my $controlchar_bugs = - $dbh->selectall_arrayref("SELECT short_desc, bug_id FROM bugs WHERE " . - $dbh->sql_regexp('short_desc', "'[[:cntrl:]]'")); - if (scalar(@$controlchar_bugs)) { - my $msg = 'Cleaning control characters from bug summaries...'; - my $found = 0; - foreach (@$controlchar_bugs) { - my ($short_desc, $bug_id) = @$_; - my $clean_short_desc = clean_text($short_desc); - if ($clean_short_desc ne $short_desc) { - print $msg if !$found; - $found = 1; - print " $bug_id..."; - $dbh->do("UPDATE bugs SET short_desc = ? WHERE bug_id = ?", - undef, $clean_short_desc, $bug_id); - } - } - print " done.\n" if $found; + my $dbh = Bugzilla->dbh; + + # Fixup for Bug 101380 + # "Newlines, nulls, leading/trailing spaces are getting into summaries" + + my $controlchar_bugs + = $dbh->selectall_arrayref("SELECT short_desc, bug_id FROM bugs WHERE " + . $dbh->sql_regexp('short_desc', "'[[:cntrl:]]'")); + if (scalar(@$controlchar_bugs)) { + my $msg = 'Cleaning control characters from bug summaries...'; + my $found = 0; + foreach (@$controlchar_bugs) { + my ($short_desc, $bug_id) = @$_; + my $clean_short_desc = clean_text($short_desc); + if ($clean_short_desc ne $short_desc) { + print $msg if !$found; + $found = 1; + print " $bug_id..."; + $dbh->do("UPDATE bugs SET short_desc = ? WHERE bug_id = ?", + undef, $clean_short_desc, $bug_id); + } } + print " done.\n" if $found; + } } sub _stop_storing_inactive_flags { - my $dbh = Bugzilla->dbh; - # 2006-03-02 LpSolit@gmail.com - Bug 322285 - # Do not store inactive flags in the DB anymore. - if ($dbh->bz_column_info('flags', 'id')->{'TYPE'} eq 'INT3') { - # We first have to remove all existing inactive flags. - if ($dbh->bz_column_info('flags', 'is_active')) { - $dbh->do('DELETE FROM flags WHERE is_active = 0'); - } + my $dbh = Bugzilla->dbh; - # Now we convert the id column to the auto_increment format. - $dbh->bz_alter_column('flags', 'id', - {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); + # 2006-03-02 LpSolit@gmail.com - Bug 322285 + # Do not store inactive flags in the DB anymore. + if ($dbh->bz_column_info('flags', 'id')->{'TYPE'} eq 'INT3') { - # And finally, we remove the is_active column. - $dbh->bz_drop_column('flags', 'is_active'); + # We first have to remove all existing inactive flags. + if ($dbh->bz_column_info('flags', 'is_active')) { + $dbh->do('DELETE FROM flags WHERE is_active = 0'); } + + # Now we convert the id column to the auto_increment format. + $dbh->bz_alter_column('flags', 'id', + {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); + + # And finally, we remove the is_active column. + $dbh->bz_drop_column('flags', 'is_active'); + } } sub _change_short_desc_from_mediumtext_to_varchar { - my $dbh = Bugzilla->dbh; - # short_desc should not be a mediumtext, fix anything longer than 255 chars. - if($dbh->bz_column_info('bugs', 'short_desc')->{TYPE} eq 'MEDIUMTEXT') { - # Move extremely long summaries into a comment ("from" the Reporter), - # and then truncate the summary. - my $long_summary_bugs = $dbh->selectall_arrayref( - 'SELECT bug_id, short_desc, reporter - FROM bugs WHERE CHAR_LENGTH(short_desc) > 255'); - - if (@$long_summary_bugs) { - print "\n", install_string('update_summary_truncated'); - my $comment_sth = $dbh->prepare( - 'INSERT INTO longdescs (bug_id, who, thetext, bug_when) - VALUES (?, ?, ?, NOW())'); - my $desc_sth = $dbh->prepare('UPDATE bugs SET short_desc = ? - WHERE bug_id = ?'); - my @affected_bugs; - foreach my $bug (@$long_summary_bugs) { - my ($bug_id, $summary, $reporter_id) = @$bug; - my $summary_comment = - install_string('update_summary_truncate_comment', - { summary => $summary }); - $comment_sth->execute($bug_id, $reporter_id, $summary_comment); - my $short_summary = substr($summary, 0, 252) . "..."; - $desc_sth->execute($short_summary, $bug_id); - push(@affected_bugs, $bug_id); - } - print join(', ', @affected_bugs) . "\n\n"; - } + my $dbh = Bugzilla->dbh; + + # short_desc should not be a mediumtext, fix anything longer than 255 chars. + if ($dbh->bz_column_info('bugs', 'short_desc')->{TYPE} eq 'MEDIUMTEXT') { - $dbh->bz_alter_column('bugs', 'short_desc', {TYPE => 'varchar(255)', - NOTNULL => 1}); + # Move extremely long summaries into a comment ("from" the Reporter), + # and then truncate the summary. + my $long_summary_bugs = $dbh->selectall_arrayref( + 'SELECT bug_id, short_desc, reporter + FROM bugs WHERE CHAR_LENGTH(short_desc) > 255' + ); + + if (@$long_summary_bugs) { + print "\n", install_string('update_summary_truncated'); + my $comment_sth = $dbh->prepare( + 'INSERT INTO longdescs (bug_id, who, thetext, bug_when) + VALUES (?, ?, ?, NOW())' + ); + my $desc_sth = $dbh->prepare( + 'UPDATE bugs SET short_desc = ? + WHERE bug_id = ?' + ); + my @affected_bugs; + foreach my $bug (@$long_summary_bugs) { + my ($bug_id, $summary, $reporter_id) = @$bug; + my $summary_comment + = install_string('update_summary_truncate_comment', {summary => $summary}); + $comment_sth->execute($bug_id, $reporter_id, $summary_comment); + my $short_summary = substr($summary, 0, 252) . "..."; + $desc_sth->execute($short_summary, $bug_id); + push(@affected_bugs, $bug_id); + } + print join(', ', @affected_bugs) . "\n\n"; } + + $dbh->bz_alter_column('bugs', 'short_desc', + {TYPE => 'varchar(255)', NOTNULL => 1}); + } } sub _move_namedqueries_linkinfooter_to_its_own_table { - my $dbh = Bugzilla->dbh; - if ($dbh->bz_column_info("namedqueries", "linkinfooter")) { - # Move link-in-footer information into a table of its own. - my $sth_read = $dbh->prepare('SELECT id, userid + my $dbh = Bugzilla->dbh; + if ($dbh->bz_column_info("namedqueries", "linkinfooter")) { + + # Move link-in-footer information into a table of its own. + my $sth_read = $dbh->prepare( + 'SELECT id, userid FROM namedqueries - WHERE linkinfooter = 1'); - my $sth_write = $dbh->prepare('INSERT INTO namedqueries_link_in_footer - (namedquery_id, user_id) VALUES (?, ?)'); - $sth_read->execute(); - while (my ($id, $userid) = $sth_read->fetchrow_array()) { - $sth_write->execute($id, $userid); - } - $dbh->bz_drop_column("namedqueries", "linkinfooter"); + WHERE linkinfooter = 1' + ); + my $sth_write = $dbh->prepare( + 'INSERT INTO namedqueries_link_in_footer + (namedquery_id, user_id) VALUES (?, ?)' + ); + $sth_read->execute(); + while (my ($id, $userid) = $sth_read->fetchrow_array()) { + $sth_write->execute($id, $userid); } + $dbh->bz_drop_column("namedqueries", "linkinfooter"); + } } sub _add_classifications_sortkey { - my $dbh = Bugzilla->dbh; - # 2006-07-07 olav@bkor.dhs.org - Bug 277377 - # Add a sortkey to the classifications - if (!$dbh->bz_column_info('classifications', 'sortkey')) { - $dbh->bz_add_column('classifications', 'sortkey', - {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0}); - - my $class_ids = $dbh->selectcol_arrayref( - 'SELECT id FROM classifications ORDER BY name'); - my $sth = $dbh->prepare('UPDATE classifications SET sortkey = ? ' . - 'WHERE id = ?'); - my $sortkey = 0; - foreach my $class_id (@$class_ids) { - $sth->execute($sortkey, $class_id); - $sortkey += 100; - } + my $dbh = Bugzilla->dbh; + + # 2006-07-07 olav@bkor.dhs.org - Bug 277377 + # Add a sortkey to the classifications + if (!$dbh->bz_column_info('classifications', 'sortkey')) { + $dbh->bz_add_column('classifications', 'sortkey', + {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0}); + + my $class_ids + = $dbh->selectcol_arrayref('SELECT id FROM classifications ORDER BY name'); + my $sth + = $dbh->prepare('UPDATE classifications SET sortkey = ? ' . 'WHERE id = ?'); + my $sortkey = 0; + foreach my $class_id (@$class_ids) { + $sth->execute($sortkey, $class_id); + $sortkey += 100; } + } } sub _move_data_nomail_into_db { - my $dbh = Bugzilla->dbh; - my $datadir = bz_locations()->{'datadir'}; - # 2006-07-14 karl@kornel.name - Bug 100953 - # If a nomail file exists, move its contents into the DB - $dbh->bz_add_column('profiles', 'disable_mail', - { TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE' }); - if (-e "$datadir/nomail") { - # We have a data/nomail file, read it in and delete it - my %nomail; - print "Found a data/nomail file. Moving nomail entries into DB...\n"; - my $nomail_file = new IO::File("$datadir/nomail", 'r'); - while (<$nomail_file>) { - $nomail{trim($_)} = 1; - } - $nomail_file->close; + my $dbh = Bugzilla->dbh; + my $datadir = bz_locations()->{'datadir'}; + + # 2006-07-14 karl@kornel.name - Bug 100953 + # If a nomail file exists, move its contents into the DB + $dbh->bz_add_column('profiles', 'disable_mail', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); + if (-e "$datadir/nomail") { + + # We have a data/nomail file, read it in and delete it + my %nomail; + print "Found a data/nomail file. Moving nomail entries into DB...\n"; + my $nomail_file = new IO::File("$datadir/nomail", 'r'); + while (<$nomail_file>) { + $nomail{trim($_)} = 1; + } + $nomail_file->close; - # Go through each entry read. If a user exists, set disable_mail. - my $query = $dbh->prepare('UPDATE profiles + # Go through each entry read. If a user exists, set disable_mail. + my $query = $dbh->prepare( + 'UPDATE profiles SET disable_mail = 1 - WHERE userid = ?'); - foreach my $user_to_check (keys %nomail) { - my $uid = $dbh->selectrow_array( - 'SELECT userid FROM profiles WHERE login_name = ?', - undef, $user_to_check); - next if !$uid; - print "\tDisabling email for user $user_to_check\n"; - $query->execute($uid); - delete $nomail{$user_to_check}; - } - - # If there are any nomail entries remaining, move them to nomail.bad - # and say something to the user. - if (scalar(keys %nomail)) { - print "\n", install_string('update_nomail_bad', - { data => $datadir }), "\n"; - my $nomail_bad = new IO::File("$datadir/nomail.bad", '>>'); - foreach my $unknown_user (keys %nomail) { - print "\t$unknown_user\n"; - print $nomail_bad "$unknown_user\n"; - delete $nomail{$unknown_user}; - } - $nomail_bad->close; - print "\n"; - } + WHERE userid = ?' + ); + foreach my $user_to_check (keys %nomail) { + my $uid + = $dbh->selectrow_array('SELECT userid FROM profiles WHERE login_name = ?', + undef, $user_to_check); + next if !$uid; + print "\tDisabling email for user $user_to_check\n"; + $query->execute($uid); + delete $nomail{$user_to_check}; + } - # Now that we don't need it, get rid of the nomail file. - unlink "$datadir/nomail"; + # If there are any nomail entries remaining, move them to nomail.bad + # and say something to the user. + if (scalar(keys %nomail)) { + print "\n", install_string('update_nomail_bad', {data => $datadir}), "\n"; + my $nomail_bad = new IO::File("$datadir/nomail.bad", '>>'); + foreach my $unknown_user (keys %nomail) { + print "\t$unknown_user\n"; + print $nomail_bad "$unknown_user\n"; + delete $nomail{$unknown_user}; + } + $nomail_bad->close; + print "\n"; } + + # Now that we don't need it, get rid of the nomail file. + unlink "$datadir/nomail"; + } } sub _update_longdescs_who_index { - my $dbh = Bugzilla->dbh; - # When doing a search on who posted a comment, longdescs is joined - # against the bugs table. So we need an index on both of these, - # not just on "who". - my $who_index = $dbh->bz_index_info('longdescs', 'longdescs_who_idx'); - if (!$who_index || scalar @{$who_index->{FIELDS}} == 1) { - # If the index doesn't exist, this will harmlessly do nothing. - $dbh->bz_drop_index('longdescs', 'longdescs_who_idx'); - $dbh->bz_add_index('longdescs', 'longdescs_who_idx', [qw(who bug_id)]); - } + my $dbh = Bugzilla->dbh; + + # When doing a search on who posted a comment, longdescs is joined + # against the bugs table. So we need an index on both of these, + # not just on "who". + my $who_index = $dbh->bz_index_info('longdescs', 'longdescs_who_idx'); + if (!$who_index || scalar @{$who_index->{FIELDS}} == 1) { + + # If the index doesn't exist, this will harmlessly do nothing. + $dbh->bz_drop_index('longdescs', 'longdescs_who_idx'); + $dbh->bz_add_index('longdescs', 'longdescs_who_idx', [qw(who bug_id)]); + } } sub _fix_uppercase_custom_field_names { - # Before the final release of 3.0, custom fields could be - # created with mixed-case names. - my $dbh = Bugzilla->dbh; - my $fields = $dbh->selectall_arrayref( - 'SELECT name, type FROM fielddefs WHERE custom = 1'); - foreach my $row (@$fields) { - my ($name, $type) = @$row; - if ($name ne lc($name)) { - $dbh->bz_rename_column('bugs', $name, lc($name)); - $dbh->bz_rename_table($name, lc($name)) - if $type == FIELD_TYPE_SINGLE_SELECT; - $dbh->do('UPDATE fielddefs SET name = ? WHERE name = ?', - undef, lc($name), $name); - } + + # Before the final release of 3.0, custom fields could be + # created with mixed-case names. + my $dbh = Bugzilla->dbh; + my $fields = $dbh->selectall_arrayref( + 'SELECT name, type FROM fielddefs WHERE custom = 1'); + foreach my $row (@$fields) { + my ($name, $type) = @$row; + if ($name ne lc($name)) { + $dbh->bz_rename_column('bugs', $name, lc($name)); + $dbh->bz_rename_table($name, lc($name)) if $type == FIELD_TYPE_SINGLE_SELECT; + $dbh->do('UPDATE fielddefs SET name = ? WHERE name = ?', + undef, lc($name), $name); } + } } sub _fix_uppercase_index_names { - # We forgot to fix indexes in the above code. - my $dbh = Bugzilla->dbh; - my $fields = $dbh->selectcol_arrayref( - 'SELECT name FROM fielddefs WHERE type = ? AND custom = 1', - undef, FIELD_TYPE_SINGLE_SELECT); - foreach my $field (@$fields) { - my $indexes = $dbh->bz_table_indexes($field); - foreach my $name (keys %$indexes) { - next if $name eq lc($name); - my $index = $indexes->{$name}; - # Lowercase the name and everything in the definition. - my $new_name = lc($name); - my @new_fields = map {lc($_)} @{$index->{FIELDS}}; - my $new_def = {FIELDS => \@new_fields, TYPE => $index->{TYPE}}; - $new_def = \@new_fields if !$index->{TYPE}; - $dbh->bz_drop_index($field, $name); - $dbh->bz_add_index($field, $new_name, $new_def); - } + + # We forgot to fix indexes in the above code. + my $dbh = Bugzilla->dbh; + my $fields + = $dbh->selectcol_arrayref( + 'SELECT name FROM fielddefs WHERE type = ? AND custom = 1', + undef, FIELD_TYPE_SINGLE_SELECT); + foreach my $field (@$fields) { + my $indexes = $dbh->bz_table_indexes($field); + foreach my $name (keys %$indexes) { + next if $name eq lc($name); + my $index = $indexes->{$name}; + + # Lowercase the name and everything in the definition. + my $new_name = lc($name); + my @new_fields = map { lc($_) } @{$index->{FIELDS}}; + my $new_def = {FIELDS => \@new_fields, TYPE => $index->{TYPE}}; + $new_def = \@new_fields if !$index->{TYPE}; + $dbh->bz_drop_index($field, $name); + $dbh->bz_add_index($field, $new_name, $new_def); } + } } sub _initialize_workflow_for_upgrade { - my $old_params = shift; - my $dbh = Bugzilla->dbh; - - $dbh->bz_add_column('bug_status', 'is_open', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); - - # Till now, bug statuses were not customizable. Nevertheless, local - # changes are possible and so we will try to respect these changes. - # This means: get the status of bugs having a resolution different from '' - # and mark these statuses as 'closed', even if some of these statuses are - # expected to be open statuses. Bug statuses we have no information about - # are left as 'open'. - # - # We append the default list of closed statuses *unless* we detect at least - # one closed state in the DB (i.e. with is_open = 0). This would mean that - # the DB has already been updated at least once and maybe the admin decided - # that e.g. 'RESOLVED' is now an open state, in which case we don't want to - # override this attribute. At least one bug status has to be a closed state - # anyway (due to the 'duplicate_or_move_bug_status' parameter) so it's safe - # to use this criteria. - my $num_closed_states = $dbh->selectrow_array('SELECT COUNT(*) FROM bug_status - WHERE is_open = 0'); - - if (!$num_closed_states) { - my @closed_statuses = - @{$dbh->selectcol_arrayref('SELECT DISTINCT bug_status FROM bugs - WHERE resolution != ?', undef, '')}; - @closed_statuses = - map {$dbh->quote($_)} (@closed_statuses, qw(RESOLVED VERIFIED CLOSED)); - - print "Marking closed bug statuses as such...\n"; - $dbh->do('UPDATE bug_status SET is_open = 0 WHERE value IN (' . - join(', ', @closed_statuses) . ')'); - } - - # We only populate the workflow here if we're upgrading from a version - # before 4.0 (which is where init_workflow was added). This was the - # first schema change done for 4.0, so we check this. - return if $dbh->bz_column_info('bugs_activity', 'comment_id'); - - # Populate the status_workflow table. We do nothing if the table already - # has entries. If all bug status transitions have been deleted, the - # workflow will be restored to its default schema. - my $count = $dbh->selectrow_array('SELECT COUNT(*) FROM status_workflow'); - - if (!$count) { - # Make sure the variables below are defined as - # status_workflow.require_comment cannot be NULL. - my $create = $old_params->{'commentoncreate'} || 0; - my $confirm = $old_params->{'commentonconfirm'} || 0; - my $accept = $old_params->{'commentonaccept'} || 0; - my $resolve = $old_params->{'commentonresolve'} || 0; - my $verify = $old_params->{'commentonverify'} || 0; - my $close = $old_params->{'commentonclose'} || 0; - my $reopen = $old_params->{'commentonreopen'} || 0; - # This was till recently the only way to get back to NEW for - # confirmed bugs, so we use this parameter here. - my $reassign = $old_params->{'commentonreassign'} || 0; - - # This is the default workflow for upgrading installations. - my @workflow = ([undef, 'UNCONFIRMED', $create], - [undef, 'NEW', $create], - [undef, 'ASSIGNED', $create], - ['UNCONFIRMED', 'NEW', $confirm], - ['UNCONFIRMED', 'ASSIGNED', $accept], - ['UNCONFIRMED', 'RESOLVED', $resolve], - ['NEW', 'ASSIGNED', $accept], - ['NEW', 'RESOLVED', $resolve], - ['ASSIGNED', 'NEW', $reassign], - ['ASSIGNED', 'RESOLVED', $resolve], - ['REOPENED', 'NEW', $reassign], - ['REOPENED', 'ASSIGNED', $accept], - ['REOPENED', 'RESOLVED', $resolve], - ['RESOLVED', 'UNCONFIRMED', $reopen], - ['RESOLVED', 'REOPENED', $reopen], - ['RESOLVED', 'VERIFIED', $verify], - ['RESOLVED', 'CLOSED', $close], - ['VERIFIED', 'UNCONFIRMED', $reopen], - ['VERIFIED', 'REOPENED', $reopen], - ['VERIFIED', 'CLOSED', $close], - ['CLOSED', 'UNCONFIRMED', $reopen], - ['CLOSED', 'REOPENED', $reopen]); - - print "Now filling the 'status_workflow' table with valid bug status transitions...\n"; - my $sth_select = $dbh->prepare('SELECT id FROM bug_status WHERE value = ?'); - my $sth = $dbh->prepare('INSERT INTO status_workflow (old_status, new_status, - require_comment) VALUES (?, ?, ?)'); - - foreach my $transition (@workflow) { - my ($from, $to); - # If it's an initial state, there is no "old" value. - $from = $dbh->selectrow_array($sth_select, undef, $transition->[0]) - if $transition->[0]; - $to = $dbh->selectrow_array($sth_select, undef, $transition->[1]); - # If one of the bug statuses doesn't exist, the transition is invalid. - next if (($transition->[0] && !$from) || !$to); - - $sth->execute($from, $to, $transition->[2] ? 1 : 0); - } - } + my $old_params = shift; + my $dbh = Bugzilla->dbh; + + $dbh->bz_add_column('bug_status', 'is_open', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); + + # Till now, bug statuses were not customizable. Nevertheless, local + # changes are possible and so we will try to respect these changes. + # This means: get the status of bugs having a resolution different from '' + # and mark these statuses as 'closed', even if some of these statuses are + # expected to be open statuses. Bug statuses we have no information about + # are left as 'open'. + # + # We append the default list of closed statuses *unless* we detect at least + # one closed state in the DB (i.e. with is_open = 0). This would mean that + # the DB has already been updated at least once and maybe the admin decided + # that e.g. 'RESOLVED' is now an open state, in which case we don't want to + # override this attribute. At least one bug status has to be a closed state + # anyway (due to the 'duplicate_or_move_bug_status' parameter) so it's safe + # to use this criteria. + my $num_closed_states = $dbh->selectrow_array( + 'SELECT COUNT(*) FROM bug_status + WHERE is_open = 0' + ); + + if (!$num_closed_states) { + my @closed_statuses = @{ + $dbh->selectcol_arrayref( + 'SELECT DISTINCT bug_status FROM bugs + WHERE resolution != ?', undef, '' + ) + }; + @closed_statuses + = map { $dbh->quote($_) } (@closed_statuses, qw(RESOLVED VERIFIED CLOSED)); + + print "Marking closed bug statuses as such...\n"; + $dbh->do('UPDATE bug_status SET is_open = 0 WHERE value IN (' + . join(', ', @closed_statuses) + . ')'); + } + + # We only populate the workflow here if we're upgrading from a version + # before 4.0 (which is where init_workflow was added). This was the + # first schema change done for 4.0, so we check this. + return if $dbh->bz_column_info('bugs_activity', 'comment_id'); + + # Populate the status_workflow table. We do nothing if the table already + # has entries. If all bug status transitions have been deleted, the + # workflow will be restored to its default schema. + my $count = $dbh->selectrow_array('SELECT COUNT(*) FROM status_workflow'); + + if (!$count) { + + # Make sure the variables below are defined as + # status_workflow.require_comment cannot be NULL. + my $create = $old_params->{'commentoncreate'} || 0; + my $confirm = $old_params->{'commentonconfirm'} || 0; + my $accept = $old_params->{'commentonaccept'} || 0; + my $resolve = $old_params->{'commentonresolve'} || 0; + my $verify = $old_params->{'commentonverify'} || 0; + my $close = $old_params->{'commentonclose'} || 0; + my $reopen = $old_params->{'commentonreopen'} || 0; + + # This was till recently the only way to get back to NEW for + # confirmed bugs, so we use this parameter here. + my $reassign = $old_params->{'commentonreassign'} || 0; + + # This is the default workflow for upgrading installations. + my @workflow = ( + [undef, 'UNCONFIRMED', $create], + [undef, 'NEW', $create], + [undef, 'ASSIGNED', $create], + ['UNCONFIRMED', 'NEW', $confirm], + ['UNCONFIRMED', 'ASSIGNED', $accept], + ['UNCONFIRMED', 'RESOLVED', $resolve], + ['NEW', 'ASSIGNED', $accept], + ['NEW', 'RESOLVED', $resolve], + ['ASSIGNED', 'NEW', $reassign], + ['ASSIGNED', 'RESOLVED', $resolve], + ['REOPENED', 'NEW', $reassign], + ['REOPENED', 'ASSIGNED', $accept], + ['REOPENED', 'RESOLVED', $resolve], + ['RESOLVED', 'UNCONFIRMED', $reopen], + ['RESOLVED', 'REOPENED', $reopen], + ['RESOLVED', 'VERIFIED', $verify], + ['RESOLVED', 'CLOSED', $close], + ['VERIFIED', 'UNCONFIRMED', $reopen], + ['VERIFIED', 'REOPENED', $reopen], + ['VERIFIED', 'CLOSED', $close], + ['CLOSED', 'UNCONFIRMED', $reopen], + ['CLOSED', 'REOPENED', $reopen] + ); - # Make sure the bug status used by the 'duplicate_or_move_bug_status' - # parameter has all the required transitions set. - my $dup_status = Bugzilla->params->{'duplicate_or_move_bug_status'}; - my $status_id = $dbh->selectrow_array( - 'SELECT id FROM bug_status WHERE value = ?', undef, $dup_status); - # There's a minor chance that this status isn't in the DB. - $status_id || return; + print + "Now filling the 'status_workflow' table with valid bug status transitions...\n"; + my $sth_select = $dbh->prepare('SELECT id FROM bug_status WHERE value = ?'); + my $sth = $dbh->prepare( + 'INSERT INTO status_workflow (old_status, new_status, + require_comment) VALUES (?, ?, ?)' + ); - my $missing_statuses = $dbh->selectcol_arrayref( - 'SELECT id FROM bug_status - LEFT JOIN status_workflow ON old_status = id - AND new_status = ? - WHERE old_status IS NULL', undef, $status_id); + foreach my $transition (@workflow) { + my ($from, $to); + + # If it's an initial state, there is no "old" value. + $from = $dbh->selectrow_array($sth_select, undef, $transition->[0]) + if $transition->[0]; + $to = $dbh->selectrow_array($sth_select, undef, $transition->[1]); - my $sth = $dbh->prepare('INSERT INTO status_workflow - (old_status, new_status) VALUES (?, ?)'); + # If one of the bug statuses doesn't exist, the transition is invalid. + next if (($transition->[0] && !$from) || !$to); - foreach my $old_status_id (@$missing_statuses) { - next if ($old_status_id == $status_id); - $sth->execute($old_status_id, $status_id); + $sth->execute($from, $to, $transition->[2] ? 1 : 0); } + } + + # Make sure the bug status used by the 'duplicate_or_move_bug_status' + # parameter has all the required transitions set. + my $dup_status = Bugzilla->params->{'duplicate_or_move_bug_status'}; + my $status_id + = $dbh->selectrow_array('SELECT id FROM bug_status WHERE value = ?', + undef, $dup_status); + + # There's a minor chance that this status isn't in the DB. + $status_id || return; + + my $missing_statuses = $dbh->selectcol_arrayref( + 'SELECT id FROM bug_status + LEFT JOIN status_workflow ON old_status = id + AND new_status = ? + WHERE old_status IS NULL', undef, $status_id + ); + + my $sth = $dbh->prepare( + 'INSERT INTO status_workflow + (old_status, new_status) VALUES (?, ?)' + ); + + foreach my $old_status_id (@$missing_statuses) { + next if ($old_status_id == $status_id); + $sth->execute($old_status_id, $status_id); + } } sub _make_lang_setting_dynamic { - my $dbh = Bugzilla->dbh; - my $count = $dbh->selectrow_array(q{SELECT 1 FROM setting + my $dbh = Bugzilla->dbh; + my $count = $dbh->selectrow_array( + q{SELECT 1 FROM setting WHERE name = 'lang' - AND subclass IS NULL}); - if ($count) { - $dbh->do(q{UPDATE setting SET subclass = 'Lang' WHERE name = 'lang'}); - $dbh->do(q{DELETE FROM setting_value WHERE name = 'lang'}); - } + AND subclass IS NULL} + ); + if ($count) { + $dbh->do(q{UPDATE setting SET subclass = 'Lang' WHERE name = 'lang'}); + $dbh->do(q{DELETE FROM setting_value WHERE name = 'lang'}); + } } sub _fix_attachment_modification_date { - my $dbh = Bugzilla->dbh; - if (!$dbh->bz_column_info('attachments', 'modification_time')) { - # Allow NULL values till the modification time has been set. - $dbh->bz_add_column('attachments', 'modification_time', {TYPE => 'DATETIME'}); + my $dbh = Bugzilla->dbh; + if (!$dbh->bz_column_info('attachments', 'modification_time')) { - print "Setting the modification time for attachments...\n"; - $dbh->do('UPDATE attachments SET modification_time = creation_ts'); + # Allow NULL values till the modification time has been set. + $dbh->bz_add_column('attachments', 'modification_time', {TYPE => 'DATETIME'}); - # Now force values to be always defined. - $dbh->bz_alter_column('attachments', 'modification_time', - {TYPE => 'DATETIME', NOTNULL => 1}); + print "Setting the modification time for attachments...\n"; + $dbh->do('UPDATE attachments SET modification_time = creation_ts'); - # Update the modification time for attachments which have been modified. - my $attachments = - $dbh->selectall_arrayref('SELECT attach_id, MAX(bug_when) FROM bugs_activity - WHERE attach_id IS NOT NULL ' . - $dbh->sql_group_by('attach_id')); + # Now force values to be always defined. + $dbh->bz_alter_column('attachments', 'modification_time', + {TYPE => 'DATETIME', NOTNULL => 1}); - my $sth = $dbh->prepare('UPDATE attachments SET modification_time = ? - WHERE attach_id = ?'); - $sth->execute($_->[1], $_->[0]) foreach (@$attachments); - } - # We add this here to be sure to have the index being added, due to the original - # patch omitting it. - $dbh->bz_add_index('attachments', 'attachments_modification_time_idx', - [qw(modification_time)]); + # Update the modification time for attachments which have been modified. + my $attachments = $dbh->selectall_arrayref( + 'SELECT attach_id, MAX(bug_when) FROM bugs_activity + WHERE attach_id IS NOT NULL ' + . $dbh->sql_group_by('attach_id') + ); + + my $sth = $dbh->prepare( + 'UPDATE attachments SET modification_time = ? + WHERE attach_id = ?' + ); + $sth->execute($_->[1], $_->[0]) foreach (@$attachments); + } + + # We add this here to be sure to have the index being added, due to the original + # patch omitting it. + $dbh->bz_add_index('attachments', 'attachments_modification_time_idx', + [qw(modification_time)]); } sub _change_text_types { - my $dbh = Bugzilla->dbh; - return if - $dbh->bz_column_info('namedqueries', 'query')->{TYPE} eq 'LONGTEXT'; - _check_content_length('attachments', 'mimetype', 255, 'attach_id'); - _check_content_length('fielddefs', 'description', 255, 'id'); - _check_content_length('attachments', 'description', 255, 'attach_id'); - - $dbh->bz_alter_column('bugs', 'bug_file_loc', - { TYPE => 'MEDIUMTEXT'}); - $dbh->bz_alter_column('longdescs', 'thetext', - { TYPE => 'LONGTEXT', NOTNULL => 1 }); - $dbh->bz_alter_column('attachments', 'description', - { TYPE => 'TINYTEXT', NOTNULL => 1 }); - $dbh->bz_alter_column('attachments', 'mimetype', - { TYPE => 'TINYTEXT', NOTNULL => 1 }); - # This also changes NULL to NOT NULL. - $dbh->bz_alter_column('flagtypes', 'description', - { TYPE => 'MEDIUMTEXT', NOTNULL => 1 }, ''); - $dbh->bz_alter_column('fielddefs', 'description', - { TYPE => 'TINYTEXT', NOTNULL => 1 }); - $dbh->bz_alter_column('groups', 'description', - { TYPE => 'MEDIUMTEXT', NOTNULL => 1 }); - $dbh->bz_alter_column('namedqueries', 'query', - { TYPE => 'LONGTEXT', NOTNULL => 1 }); - -} + my $dbh = Bugzilla->dbh; + return if $dbh->bz_column_info('namedqueries', 'query')->{TYPE} eq 'LONGTEXT'; + _check_content_length('attachments', 'mimetype', 255, 'attach_id'); + _check_content_length('fielddefs', 'description', 255, 'id'); + _check_content_length('attachments', 'description', 255, 'attach_id'); + + $dbh->bz_alter_column('bugs', 'bug_file_loc', {TYPE => 'MEDIUMTEXT'}); + $dbh->bz_alter_column('longdescs', 'thetext', + {TYPE => 'LONGTEXT', NOTNULL => 1}); + $dbh->bz_alter_column('attachments', 'description', + {TYPE => 'TINYTEXT', NOTNULL => 1}); + $dbh->bz_alter_column('attachments', 'mimetype', + {TYPE => 'TINYTEXT', NOTNULL => 1}); + + # This also changes NULL to NOT NULL. + $dbh->bz_alter_column('flagtypes', 'description', + {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, ''); + $dbh->bz_alter_column('fielddefs', 'description', + {TYPE => 'TINYTEXT', NOTNULL => 1}); + $dbh->bz_alter_column('groups', 'description', + {TYPE => 'MEDIUMTEXT', NOTNULL => 1}); + $dbh->bz_alter_column('namedqueries', 'query', + {TYPE => 'LONGTEXT', NOTNULL => 1}); + +} sub _check_content_length { - my ($table_name, $field_name, $max_length, $id_field) = @_; - my $dbh = Bugzilla->dbh; - my %contents = @{ $dbh->selectcol_arrayref( - "SELECT $id_field, $field_name FROM $table_name - WHERE CHAR_LENGTH($field_name) > ?", {Columns=>[1,2]}, $max_length) }; - - if (scalar keys %contents) { - my $error = install_string('install_data_too_long', - { column => $field_name, - id_column => $id_field, - table => $table_name, - max_length => $max_length }); - foreach my $id (keys %contents) { - my $string = $contents{$id}; - # Don't dump the whole string--it could be 16MB. - if (length($string) > 80) { - $string = substr($string, 0, 30) . "..." - . substr($string, -30) . "\n"; - } - $error .= "$id: $string\n"; - } - die $error; + my ($table_name, $field_name, $max_length, $id_field) = @_; + my $dbh = Bugzilla->dbh; + my %contents = @{ + $dbh->selectcol_arrayref( + "SELECT $id_field, $field_name FROM $table_name + WHERE CHAR_LENGTH($field_name) > ?", {Columns => [1, 2]}, $max_length + ) + }; + + if (scalar keys %contents) { + my $error = install_string( + 'install_data_too_long', + { + column => $field_name, + id_column => $id_field, + table => $table_name, + max_length => $max_length + } + ); + foreach my $id (keys %contents) { + my $string = $contents{$id}; + + # Don't dump the whole string--it could be 16MB. + if (length($string) > 80) { + $string = substr($string, 0, 30) . "..." . substr($string, -30) . "\n"; + } + $error .= "$id: $string\n"; } + die $error; + } } sub _add_foreign_keys_to_multiselects { - my $dbh = Bugzilla->dbh; + my $dbh = Bugzilla->dbh; - my $names = $dbh->selectcol_arrayref( - 'SELECT name + my $names = $dbh->selectcol_arrayref( + 'SELECT name FROM fielddefs - WHERE type = ' . FIELD_TYPE_MULTI_SELECT); + WHERE type = ' . FIELD_TYPE_MULTI_SELECT + ); - foreach my $name (@$names) { - $dbh->bz_add_fk("bug_$name", "bug_id", - {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'}); - - $dbh->bz_add_fk("bug_$name", "value", - {TABLE => $name, COLUMN => 'value', DELETE => 'RESTRICT'}); - } + foreach my $name (@$names) { + $dbh->bz_add_fk("bug_$name", "bug_id", + {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'}); + + $dbh->bz_add_fk("bug_$name", "value", + {TABLE => $name, COLUMN => 'value', DELETE => 'RESTRICT'}); + } } # This subroutine is used in multiple places (for times when we update @@ -3252,693 +3490,738 @@ sub _add_foreign_keys_to_multiselects { # it to update bugs_fulltext for those bug_ids instead of populating the # whole table. sub _populate_bugs_fulltext { - my $bug_ids = shift; - my $dbh = Bugzilla->dbh; - my $fulltext = $dbh->selectrow_array('SELECT 1 FROM bugs_fulltext ' - . $dbh->sql_limit(1)); - # We only populate the table if it's empty or if we've been given a - # set of bug ids. - if ($bug_ids or !$fulltext) { - $bug_ids ||= $dbh->selectcol_arrayref('SELECT bug_id FROM bugs'); - # If there are no bugs in the bugs table, there's nothing to populate. - return if !@$bug_ids; - my $num_bugs = scalar @$bug_ids; - - my $command = "INSERT"; - my $where = ""; - if ($fulltext) { - print "Updating bugs_fulltext for $num_bugs bugs...\n"; - $where = "WHERE " . $dbh->sql_in('bugs.bug_id', $bug_ids); - # It turns out that doing a REPLACE INTO is up to 10x faster - # than any other possible method of updating the table, in MySQL, - # which matters a LOT for large installations. - if ($dbh->isa('Bugzilla::DB::Mysql')) { - $command = "REPLACE"; - } - else { - $dbh->do("DELETE FROM bugs_fulltext WHERE " - . $dbh->sql_in('bug_id', $bug_ids)); - } - } - else { - print "Populating bugs_fulltext with $num_bugs entries..."; - print " (this can take a long time.)\n"; - } - my $newline = $dbh->quote("\n"); - $dbh->do( - qq{$command INTO bugs_fulltext (bug_id, short_desc, comments, + my $bug_ids = shift; + my $dbh = Bugzilla->dbh; + my $fulltext + = $dbh->selectrow_array('SELECT 1 FROM bugs_fulltext ' . $dbh->sql_limit(1)); + + # We only populate the table if it's empty or if we've been given a + # set of bug ids. + if ($bug_ids or !$fulltext) { + $bug_ids ||= $dbh->selectcol_arrayref('SELECT bug_id FROM bugs'); + + # If there are no bugs in the bugs table, there's nothing to populate. + return if !@$bug_ids; + my $num_bugs = scalar @$bug_ids; + + my $command = "INSERT"; + my $where = ""; + if ($fulltext) { + print "Updating bugs_fulltext for $num_bugs bugs...\n"; + $where = "WHERE " . $dbh->sql_in('bugs.bug_id', $bug_ids); + + # It turns out that doing a REPLACE INTO is up to 10x faster + # than any other possible method of updating the table, in MySQL, + # which matters a LOT for large installations. + if ($dbh->isa('Bugzilla::DB::Mysql')) { + $command = "REPLACE"; + } + else { + $dbh->do("DELETE FROM bugs_fulltext WHERE " . $dbh->sql_in('bug_id', $bug_ids)); + } + } + else { + print "Populating bugs_fulltext with $num_bugs entries..."; + print " (this can take a long time.)\n"; + } + my $newline = $dbh->quote("\n"); + $dbh->do( + qq{$command INTO bugs_fulltext (bug_id, short_desc, comments, comments_noprivate) SELECT bugs.bug_id, bugs.short_desc, } - . $dbh->sql_group_concat('longdescs.thetext', $newline, 0) - . ', ' . $dbh->sql_group_concat('nopriv.thetext', $newline, 0) . - qq{ FROM bugs + . $dbh->sql_group_concat('longdescs.thetext', $newline, 0) . ', ' + . $dbh->sql_group_concat('nopriv.thetext', $newline, 0) + . qq{ FROM bugs LEFT JOIN longdescs ON bugs.bug_id = longdescs.bug_id LEFT JOIN longdescs AS nopriv ON longdescs.comment_id = nopriv.comment_id AND nopriv.isprivate = 0 $where } - . $dbh->sql_group_by('bugs.bug_id', 'bugs.short_desc')); - } + . $dbh->sql_group_by('bugs.bug_id', 'bugs.short_desc') + ); + } } sub _fix_illegal_flag_modification_dates { - my $dbh = Bugzilla->dbh; + my $dbh = Bugzilla->dbh; + + my $rows = $dbh->do( + 'UPDATE flags SET modification_date = creation_date + WHERE modification_date < creation_date' + ); - my $rows = $dbh->do('UPDATE flags SET modification_date = creation_date - WHERE modification_date < creation_date'); - # If no rows are affected, $dbh->do returns 0E0 instead of 0. - print "$rows flags had an illegal modification date. Fixed!\n" if ($rows =~ /^\d+$/); + # If no rows are affected, $dbh->do returns 0E0 instead of 0. + print "$rows flags had an illegal modification date. Fixed!\n" + if ($rows =~ /^\d+$/); } sub _add_visiblity_value_to_value_tables { - my $dbh = Bugzilla->dbh; - my @standard_fields = - qw(bug_status resolution priority bug_severity op_sys rep_platform); - my $custom_fields = $dbh->selectcol_arrayref( - 'SELECT name FROM fielddefs WHERE custom = 1 AND type IN(?,?)', - undef, FIELD_TYPE_SINGLE_SELECT, FIELD_TYPE_MULTI_SELECT); - foreach my $field (@standard_fields, @$custom_fields) { - $dbh->bz_add_column($field, 'visibility_value_id', {TYPE => 'INT2'}); - $dbh->bz_add_index($field, "${field}_visibility_value_id_idx", - ['visibility_value_id']); - } + my $dbh = Bugzilla->dbh; + my @standard_fields + = qw(bug_status resolution priority bug_severity op_sys rep_platform); + my $custom_fields + = $dbh->selectcol_arrayref( + 'SELECT name FROM fielddefs WHERE custom = 1 AND type IN(?,?)', + undef, FIELD_TYPE_SINGLE_SELECT, FIELD_TYPE_MULTI_SELECT); + foreach my $field (@standard_fields, @$custom_fields) { + $dbh->bz_add_column($field, 'visibility_value_id', {TYPE => 'INT2'}); + $dbh->bz_add_index($field, "${field}_visibility_value_id_idx", + ['visibility_value_id']); + } } sub _add_extern_id_index { - my $dbh = Bugzilla->dbh; - if (!$dbh->bz_index_info('profiles', 'profiles_extern_id_idx')) { - # Some Bugzillas have a multiple empty strings in extern_id, - # which need to be converted to NULLs before we add the index. - $dbh->do("UPDATE profiles SET extern_id = NULL WHERE extern_id = ''"); - $dbh->bz_add_index('profiles', 'profiles_extern_id_idx', - {TYPE => 'UNIQUE', FIELDS => [qw(extern_id)]}); - } + my $dbh = Bugzilla->dbh; + if (!$dbh->bz_index_info('profiles', 'profiles_extern_id_idx')) { + + # Some Bugzillas have a multiple empty strings in extern_id, + # which need to be converted to NULLs before we add the index. + $dbh->do("UPDATE profiles SET extern_id = NULL WHERE extern_id = ''"); + $dbh->bz_add_index('profiles', 'profiles_extern_id_idx', + {TYPE => 'UNIQUE', FIELDS => [qw(extern_id)]}); + } } sub _convert_disallownew_to_isactive { - my $dbh = Bugzilla->dbh; - if ($dbh->bz_column_info('products', 'disallownew')){ - $dbh->bz_add_column('products', 'isactive', - { TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); - - # isactive is the boolean reverse of disallownew. - $dbh->do('UPDATE products SET isactive = 0 WHERE disallownew = 1'); - $dbh->do('UPDATE products SET isactive = 1 WHERE disallownew = 0'); - - $dbh->bz_drop_column('products','disallownew'); - } + my $dbh = Bugzilla->dbh; + if ($dbh->bz_column_info('products', 'disallownew')) { + $dbh->bz_add_column('products', 'isactive', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); + + # isactive is the boolean reverse of disallownew. + $dbh->do('UPDATE products SET isactive = 0 WHERE disallownew = 1'); + $dbh->do('UPDATE products SET isactive = 1 WHERE disallownew = 0'); + + $dbh->bz_drop_column('products', 'disallownew'); + } } sub _fix_logincookies_ipaddr { - my $dbh = Bugzilla->dbh; - return if !$dbh->bz_column_info('logincookies', 'ipaddr')->{NOTNULL}; + my $dbh = Bugzilla->dbh; + return if !$dbh->bz_column_info('logincookies', 'ipaddr')->{NOTNULL}; - $dbh->bz_alter_column('logincookies', 'ipaddr', {TYPE => 'varchar(40)'}); - $dbh->do('UPDATE logincookies SET ipaddr = NULL WHERE ipaddr = ?', - undef, '0.0.0.0'); + $dbh->bz_alter_column('logincookies', 'ipaddr', {TYPE => 'varchar(40)'}); + $dbh->do('UPDATE logincookies SET ipaddr = NULL WHERE ipaddr = ?', + undef, '0.0.0.0'); } sub _fix_invalid_custom_field_names { - my $fields = Bugzilla->fields({ custom => 1 }); + my $fields = Bugzilla->fields({custom => 1}); - foreach my $field (@$fields) { - next if $field->name =~ /^[a-zA-Z0-9_]+$/; - # The field name is illegal and can break the DB. Kill the field! - $field->set_obsolete(1); - print install_string('update_cf_invalid_name', - { field => $field->name }), "\n"; - eval { $field->remove_from_db(); }; - warn $@ if $@; - } + foreach my $field (@$fields) { + next if $field->name =~ /^[a-zA-Z0-9_]+$/; + + # The field name is illegal and can break the DB. Kill the field! + $field->set_obsolete(1); + print install_string('update_cf_invalid_name', {field => $field->name}), "\n"; + eval { $field->remove_from_db(); }; + warn $@ if $@; + } } sub _set_attachment_comment_type { - my ($type, $string) = @_; - my $dbh = Bugzilla->dbh; - # We check if there are any comments of this type already, first, - # because this is faster than a full LIKE search on the comments, - # and currently this will run every time we run checksetup. - my $test = $dbh->selectrow_array( - "SELECT 1 FROM longdescs WHERE type = $type " . $dbh->sql_limit(1)); - return [] if $test; - my %comments = @{ $dbh->selectcol_arrayref( - "SELECT comment_id, thetext FROM longdescs - WHERE thetext LIKE '$string%'", - {Columns=>[1,2]}) }; - my @comment_ids = keys %comments; - return [] if !scalar @comment_ids; - my $what = "update"; + my ($type, $string) = @_; + my $dbh = Bugzilla->dbh; + + # We check if there are any comments of this type already, first, + # because this is faster than a full LIKE search on the comments, + # and currently this will run every time we run checksetup. + my $test = $dbh->selectrow_array( + "SELECT 1 FROM longdescs WHERE type = $type " . $dbh->sql_limit(1)); + return [] if $test; + my %comments = @{ + $dbh->selectcol_arrayref( + "SELECT comment_id, thetext FROM longdescs + WHERE thetext LIKE '$string%'", {Columns => [1, 2]} + ) + }; + my @comment_ids = keys %comments; + return [] if !scalar @comment_ids; + my $what = "update"; + if ($type == CMT_ATTACHMENT_CREATED) { + $what = "creation"; + } + print "Setting the type field on attachment $what comments...\n"; + my $sth = $dbh->prepare( + 'UPDATE longdescs SET thetext = ?, type = ?, extra_data = ? + WHERE comment_id = ?' + ); + my $count = 0; + my $total = scalar @comment_ids; + foreach my $id (@comment_ids) { + $count++; + my $text = $comments{$id}; + next if $text !~ /^\Q$string\E(\d+)/; + my $attachment_id = $1; + my @lines = split("\n", $text); if ($type == CMT_ATTACHMENT_CREATED) { - $what = "creation"; + + # Now we have to remove the text up until we find a line that's + # just a single newline, because the old "Created an attachment" + # text included the attachment description underneath it, and in + # Bugzillas before 2.20, that could be wrapped into multiple lines, + # in the database. + while (1) { + my $line = shift @lines; + last if (!defined $line or trim($line) eq ''); + } } - print "Setting the type field on attachment $what comments...\n"; - my $sth = $dbh->prepare( - 'UPDATE longdescs SET thetext = ?, type = ?, extra_data = ? - WHERE comment_id = ?'); - my $count = 0; - my $total = scalar @comment_ids; - foreach my $id (@comment_ids) { - $count++; - my $text = $comments{$id}; - next if $text !~ /^\Q$string\E(\d+)/; - my $attachment_id = $1; - my @lines = split("\n", $text); - if ($type == CMT_ATTACHMENT_CREATED) { - # Now we have to remove the text up until we find a line that's - # just a single newline, because the old "Created an attachment" - # text included the attachment description underneath it, and in - # Bugzillas before 2.20, that could be wrapped into multiple lines, - # in the database. - while (1) { - my $line = shift @lines; - last if (!defined $line or trim($line) eq ''); - } - } - else { - # However, the "From update of attachment" line is always just - # one line--the first line of the comment. - shift @lines; - } - $text = join("\n", @lines); - $sth->execute($text, $type, $attachment_id, $id); - indicate_progress({ total => $total, current => $count, - every => 25 }); + else { + # However, the "From update of attachment" line is always just + # one line--the first line of the comment. + shift @lines; } - return \@comment_ids; + $text = join("\n", @lines); + $sth->execute($text, $type, $attachment_id, $id); + indicate_progress({total => $total, current => $count, every => 25}); + } + return \@comment_ids; } sub _set_attachment_comment_types { - my $dbh = Bugzilla->dbh; - $dbh->bz_start_transaction(); - my $created_ids = _set_attachment_comment_type( - CMT_ATTACHMENT_CREATED, 'Created an attachment (id='); - my $updated_ids = _set_attachment_comment_type( - CMT_ATTACHMENT_UPDATED, '(From update of attachment '); - $dbh->bz_commit_transaction(); - return unless (@$created_ids or @$updated_ids); - - my @comment_ids = (@$created_ids, @$updated_ids); - - my $bug_ids = $dbh->selectcol_arrayref( - 'SELECT DISTINCT bug_id FROM longdescs WHERE ' - . $dbh->sql_in('comment_id', \@comment_ids)); - _populate_bugs_fulltext($bug_ids); + my $dbh = Bugzilla->dbh; + $dbh->bz_start_transaction(); + my $created_ids = _set_attachment_comment_type(CMT_ATTACHMENT_CREATED, + 'Created an attachment (id='); + my $updated_ids = _set_attachment_comment_type(CMT_ATTACHMENT_UPDATED, + '(From update of attachment '); + $dbh->bz_commit_transaction(); + return unless (@$created_ids or @$updated_ids); + + my @comment_ids = (@$created_ids, @$updated_ids); + + my $bug_ids + = $dbh->selectcol_arrayref('SELECT DISTINCT bug_id FROM longdescs WHERE ' + . $dbh->sql_in('comment_id', \@comment_ids)); + _populate_bugs_fulltext($bug_ids); } sub _add_allows_unconfirmed_to_product_table { - my $dbh = Bugzilla->dbh; - if (!$dbh->bz_column_info('products', 'allows_unconfirmed')) { - $dbh->bz_add_column('products', 'allows_unconfirmed', - { TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE' }); - if ($dbh->bz_column_info('products', 'votestoconfirm')) { - $dbh->do('UPDATE products SET allows_unconfirmed = 1 - WHERE votestoconfirm > 0'); - } + my $dbh = Bugzilla->dbh; + if (!$dbh->bz_column_info('products', 'allows_unconfirmed')) { + $dbh->bz_add_column('products', 'allows_unconfirmed', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); + if ($dbh->bz_column_info('products', 'votestoconfirm')) { + $dbh->do( + 'UPDATE products SET allows_unconfirmed = 1 + WHERE votestoconfirm > 0' + ); } + } } sub _convert_flagtypes_fks_to_set_null { - my $dbh = Bugzilla->dbh; - foreach my $column (qw(request_group_id grant_group_id)) { - my $fk = $dbh->bz_fk_info('flagtypes', $column); - if ($fk and !defined $fk->{DELETE}) { - $fk->{DELETE} = 'SET NULL'; - $dbh->bz_alter_fk('flagtypes', $column, $fk); - } + my $dbh = Bugzilla->dbh; + foreach my $column (qw(request_group_id grant_group_id)) { + my $fk = $dbh->bz_fk_info('flagtypes', $column); + if ($fk and !defined $fk->{DELETE}) { + $fk->{DELETE} = 'SET NULL'; + $dbh->bz_alter_fk('flagtypes', $column, $fk); } + } } sub _fix_decimal_types { - my $dbh = Bugzilla->dbh; - my $type = {TYPE => 'decimal(7,2)', NOTNULL => 1, DEFAULT => '0'}; - $dbh->bz_alter_column('bugs', 'estimated_time', $type); - $dbh->bz_alter_column('bugs', 'remaining_time', $type); - $dbh->bz_alter_column('longdescs', 'work_time', $type); + my $dbh = Bugzilla->dbh; + my $type = {TYPE => 'decimal(7,2)', NOTNULL => 1, DEFAULT => '0'}; + $dbh->bz_alter_column('bugs', 'estimated_time', $type); + $dbh->bz_alter_column('bugs', 'remaining_time', $type); + $dbh->bz_alter_column('longdescs', 'work_time', $type); } sub _fix_series_creator_fk { - my $dbh = Bugzilla->dbh; - my $fk = $dbh->bz_fk_info('series', 'creator'); - if ($fk and $fk->{DELETE} eq 'SET NULL') { - $fk->{DELETE} = 'CASCADE'; - $dbh->bz_alter_fk('series', 'creator', $fk); - } + my $dbh = Bugzilla->dbh; + my $fk = $dbh->bz_fk_info('series', 'creator'); + if ($fk and $fk->{DELETE} eq 'SET NULL') { + $fk->{DELETE} = 'CASCADE'; + $dbh->bz_alter_fk('series', 'creator', $fk); + } } sub _remove_attachment_isurl { - my $dbh = Bugzilla->dbh; + my $dbh = Bugzilla->dbh; - if ($dbh->bz_column_info('attachments', 'isurl')) { - # Now all attachments must have a filename. - $dbh->do('UPDATE attachments SET filename = ? WHERE isurl = 1', - undef, 'url.txt'); - $dbh->bz_drop_column('attachments', 'isurl'); - $dbh->do("DELETE FROM fielddefs WHERE name='attachments.isurl'"); - } + if ($dbh->bz_column_info('attachments', 'isurl')) { + + # Now all attachments must have a filename. + $dbh->do('UPDATE attachments SET filename = ? WHERE isurl = 1', + undef, 'url.txt'); + $dbh->bz_drop_column('attachments', 'isurl'); + $dbh->do("DELETE FROM fielddefs WHERE name='attachments.isurl'"); + } } sub _add_isactive_to_product_fields { - my $dbh = Bugzilla->dbh; - - # If we add the isactive column all values should start off as active - if (!$dbh->bz_column_info('components', 'isactive')) { - $dbh->bz_add_column('components', 'isactive', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); - } - - if (!$dbh->bz_column_info('versions', 'isactive')) { - $dbh->bz_add_column('versions', 'isactive', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); - } - - if (!$dbh->bz_column_info('milestones', 'isactive')) { - $dbh->bz_add_column('milestones', 'isactive', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); - } + my $dbh = Bugzilla->dbh; + + # If we add the isactive column all values should start off as active + if (!$dbh->bz_column_info('components', 'isactive')) { + $dbh->bz_add_column('components', 'isactive', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); + } + + if (!$dbh->bz_column_info('versions', 'isactive')) { + $dbh->bz_add_column('versions', 'isactive', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); + } + + if (!$dbh->bz_column_info('milestones', 'isactive')) { + $dbh->bz_add_column('milestones', 'isactive', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); + } } sub _migrate_field_visibility_value { - my $dbh = Bugzilla->dbh; + my $dbh = Bugzilla->dbh; - if ($dbh->bz_column_info('fielddefs', 'visibility_value_id')) { - print "Populating new field_visibility table...\n"; + if ($dbh->bz_column_info('fielddefs', 'visibility_value_id')) { + print "Populating new field_visibility table...\n"; - $dbh->bz_start_transaction(); - - my %results = - @{ $dbh->selectcol_arrayref( - "SELECT id, visibility_value_id FROM fielddefs - WHERE visibility_value_id IS NOT NULL", - { Columns => [1,2] }) }; + $dbh->bz_start_transaction(); - my $insert_sth = - $dbh->prepare("INSERT INTO field_visibility (field_id, value_id) - VALUES (?, ?)"); + my %results = @{ + $dbh->selectcol_arrayref( + "SELECT id, visibility_value_id FROM fielddefs + WHERE visibility_value_id IS NOT NULL", {Columns => [1, 2]} + ) + }; - foreach my $id (keys %results) { - $insert_sth->execute($id, $results{$id}); - } + my $insert_sth = $dbh->prepare( + "INSERT INTO field_visibility (field_id, value_id) + VALUES (?, ?)" + ); - $dbh->bz_commit_transaction(); - $dbh->bz_drop_column('fielddefs', 'visibility_value_id'); + foreach my $id (keys %results) { + $insert_sth->execute($id, $results{$id}); } + + $dbh->bz_commit_transaction(); + $dbh->bz_drop_column('fielddefs', 'visibility_value_id'); + } } sub _fix_series_indexes { - my $dbh = Bugzilla->dbh; - return if $dbh->bz_index_info('series', 'series_category_idx'); + my $dbh = Bugzilla->dbh; + return if $dbh->bz_index_info('series', 'series_category_idx'); - $dbh->bz_drop_index('series', 'series_creator_idx'); + $dbh->bz_drop_index('series', 'series_creator_idx'); - # Fix duplicated names under the same category/subcategory before - # adding the more restrictive index. - my $duplicated_series = $dbh->selectall_arrayref( - 'SELECT s1.series_id, s1.category, s1.subcategory, s1.name + # Fix duplicated names under the same category/subcategory before + # adding the more restrictive index. + my $duplicated_series = $dbh->selectall_arrayref( + 'SELECT s1.series_id, s1.category, s1.subcategory, s1.name FROM series AS s1 INNER JOIN series AS s2 ON s1.category = s2.category AND s1.subcategory = s2.subcategory AND s1.name = s2.name - WHERE s1.series_id != s2.series_id'); - my $sth_series_update = $dbh->prepare('UPDATE series SET name = ? WHERE series_id = ?'); - my $sth_series_query = $dbh->prepare('SELECT 1 FROM series WHERE name = ? - AND category = ? AND subcategory = ?'); - - my %renamed_series; - foreach my $series (@$duplicated_series) { - my ($series_id, $category, $subcategory, $name) = @$series; - # Leave the first series alone, then rename duplicated ones. - if ($renamed_series{"${category}_${subcategory}_${name}"}++) { - print "Renaming series ${category}/${subcategory}/${name}...\n"; - my $c = 0; - my $exists = 1; - while ($exists) { - $sth_series_query->execute($name . ++$c, $category, $subcategory); - $exists = $sth_series_query->fetchrow_array; - } - $sth_series_update->execute($name . $c, $series_id); - } + WHERE s1.series_id != s2.series_id' + ); + my $sth_series_update + = $dbh->prepare('UPDATE series SET name = ? WHERE series_id = ?'); + my $sth_series_query = $dbh->prepare( + 'SELECT 1 FROM series WHERE name = ? + AND category = ? AND subcategory = ?' + ); + + my %renamed_series; + foreach my $series (@$duplicated_series) { + my ($series_id, $category, $subcategory, $name) = @$series; + + # Leave the first series alone, then rename duplicated ones. + if ($renamed_series{"${category}_${subcategory}_${name}"}++) { + print "Renaming series ${category}/${subcategory}/${name}...\n"; + my $c = 0; + my $exists = 1; + while ($exists) { + $sth_series_query->execute($name . ++$c, $category, $subcategory); + $exists = $sth_series_query->fetchrow_array; + } + $sth_series_update->execute($name . $c, $series_id); } + } - $dbh->bz_add_index('series', 'series_creator_idx', ['creator']); - $dbh->bz_add_index('series', 'series_category_idx', - {FIELDS => [qw(category subcategory name)], TYPE => 'UNIQUE'}); + $dbh->bz_add_index('series', 'series_creator_idx', ['creator']); + $dbh->bz_add_index('series', 'series_category_idx', + {FIELDS => [qw(category subcategory name)], TYPE => 'UNIQUE'}); } sub _migrate_user_tags { - my $dbh = Bugzilla->dbh; - return unless $dbh->bz_column_info('namedqueries', 'query_type'); + my $dbh = Bugzilla->dbh; + return unless $dbh->bz_column_info('namedqueries', 'query_type'); - my $tags = $dbh->selectall_arrayref('SELECT id, userid, name, query + my $tags = $dbh->selectall_arrayref( + 'SELECT id, userid, name, query FROM namedqueries - WHERE query_type != 0'); - - my $sth_tags = $dbh->prepare( - 'INSERT INTO tag (user_id, name) VALUES (?, ?)'); - my $sth_tag_id = $dbh->prepare( - 'SELECT id FROM tag WHERE user_id = ? AND name = ?'); - my $sth_bug_tag = $dbh->prepare('INSERT INTO bug_tag (bug_id, tag_id) - VALUES (?, ?)'); - my $sth_nq = $dbh->prepare('UPDATE namedqueries SET query = ? - WHERE id = ?'); - - if (scalar @$tags) { - print install_string('update_queries_to_tags'), "\n"; + WHERE query_type != 0' + ); + + my $sth_tags = $dbh->prepare('INSERT INTO tag (user_id, name) VALUES (?, ?)'); + my $sth_tag_id + = $dbh->prepare('SELECT id FROM tag WHERE user_id = ? AND name = ?'); + my $sth_bug_tag = $dbh->prepare( + 'INSERT INTO bug_tag (bug_id, tag_id) + VALUES (?, ?)' + ); + my $sth_nq = $dbh->prepare( + 'UPDATE namedqueries SET query = ? + WHERE id = ?' + ); + + if (scalar @$tags) { + print install_string('update_queries_to_tags'), "\n"; + } + + my $total = scalar(@$tags); + my $current = 0; + + $dbh->bz_start_transaction(); + foreach my $tag (@$tags) { + my ($query_id, $user_id, $name, $query) = @$tag; + + # Tags are all lowercase. + my $tag_name = lc($name); + + $sth_tags->execute($user_id, $tag_name); + + my $tag_id = $dbh->selectrow_array($sth_tag_id, undef, $user_id, $tag_name); + + indicate_progress({current => ++$current, total => $total, every => 25}); + + my $uri = URI->new("buglist.cgi?$query", 'http'); + my $bug_id_list = $uri->query_param_delete('bug_id'); + if (!$bug_id_list) { + warn "No bug_id param for tag $name from user $user_id: $query"; + next; } + my @bug_ids = split(/[\s,]+/, $bug_id_list); - my $total = scalar(@$tags); - my $current = 0; - - $dbh->bz_start_transaction(); - foreach my $tag (@$tags) { - my ($query_id, $user_id, $name, $query) = @$tag; - # Tags are all lowercase. - my $tag_name = lc($name); - - $sth_tags->execute($user_id, $tag_name); - - my $tag_id = $dbh->selectrow_array($sth_tag_id, - undef, $user_id, $tag_name); + # Make sure that things like "001" get converted to "1" + @bug_ids = map { int($_) } @bug_ids; - indicate_progress({ current => ++$current, total => $total, - every => 25 }); + # And remove duplicates + @bug_ids = uniq @bug_ids; + foreach my $bug_id (@bug_ids) { - my $uri = URI->new("buglist.cgi?$query", 'http'); - my $bug_id_list = $uri->query_param_delete('bug_id'); - if (!$bug_id_list) { - warn "No bug_id param for tag $name from user $user_id: $query"; - next; - } - my @bug_ids = split(/[\s,]+/, $bug_id_list); - # Make sure that things like "001" get converted to "1" - @bug_ids = map { int($_) } @bug_ids; - # And remove duplicates - @bug_ids = uniq @bug_ids; - foreach my $bug_id (@bug_ids) { - # If "int" above failed this might be undef. We also - # don't want to accept bug 0. - next if !$bug_id; - $sth_bug_tag->execute($bug_id, $tag_id); - } - - # Existing tags may be used in whines, or shared with - # other users. So we convert them rather than delete them. - $uri->query_param('tag', $tag_name); - $sth_nq->execute($uri->query, $query_id); + # If "int" above failed this might be undef. We also + # don't want to accept bug 0. + next if !$bug_id; + $sth_bug_tag->execute($bug_id, $tag_id); } - $dbh->bz_commit_transaction(); + # Existing tags may be used in whines, or shared with + # other users. So we convert them rather than delete them. + $uri->query_param('tag', $tag_name); + $sth_nq->execute($uri->query, $query_id); + } + + $dbh->bz_commit_transaction(); - $dbh->bz_drop_column('namedqueries', 'query_type'); + $dbh->bz_drop_column('namedqueries', 'query_type'); } sub _populate_bug_see_also_class { - my $dbh = Bugzilla->dbh; + my $dbh = Bugzilla->dbh; - if ($dbh->bz_column_info('bug_see_also', 'class')) { - # The length was incorrectly set to 64 instead of 255. - $dbh->bz_alter_column('bug_see_also', 'class', - {TYPE => 'varchar(255)', NOTNULL => 1, DEFAULT => "''"}); - return; - } + if ($dbh->bz_column_info('bug_see_also', 'class')) { - $dbh->bz_add_column('bug_see_also', 'class', - {TYPE => 'varchar(255)', NOTNULL => 1, DEFAULT => "''"}, ''); + # The length was incorrectly set to 64 instead of 255. + $dbh->bz_alter_column('bug_see_also', 'class', + {TYPE => 'varchar(255)', NOTNULL => 1, DEFAULT => "''"}); + return; + } - my $result = $dbh->selectall_arrayref( - "SELECT id, value FROM bug_see_also"); + $dbh->bz_add_column('bug_see_also', 'class', + {TYPE => 'varchar(255)', NOTNULL => 1, DEFAULT => "''"}, ''); - my $update_sth = - $dbh->prepare("UPDATE bug_see_also SET class = ? WHERE id = ?"); - - $dbh->bz_start_transaction(); - foreach my $see_also (@$result) { - my ($id, $value) = @$see_also; - my $class = Bugzilla::BugUrl->class_for($value); - $update_sth->execute($class, $id); - } - $dbh->bz_commit_transaction(); + my $result = $dbh->selectall_arrayref("SELECT id, value FROM bug_see_also"); + + my $update_sth + = $dbh->prepare("UPDATE bug_see_also SET class = ? WHERE id = ?"); + + $dbh->bz_start_transaction(); + foreach my $see_also (@$result) { + my ($id, $value) = @$see_also; + my $class = Bugzilla::BugUrl->class_for($value); + $update_sth->execute($class, $id); + } + $dbh->bz_commit_transaction(); } sub _migrate_disabledtext_boolean { - my $dbh = Bugzilla->dbh; - if (!$dbh->bz_column_info('profiles', 'is_enabled')) { - $dbh->bz_add_column("profiles", 'is_enabled', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); - $dbh->do("UPDATE profiles SET is_enabled = 0 - WHERE disabledtext != ''"); - } + my $dbh = Bugzilla->dbh; + if (!$dbh->bz_column_info('profiles', 'is_enabled')) { + $dbh->bz_add_column("profiles", 'is_enabled', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); + $dbh->do( + "UPDATE profiles SET is_enabled = 0 + WHERE disabledtext != ''" + ); + } } sub _rename_tags_to_tag { - my $dbh = Bugzilla->dbh; - if ($dbh->bz_table_info('tags')) { - # If we get here, it's because the schema created "tag" as an empty - # table while "tags" still exists. We get rid of the empty - # tag table so we can do the rename over the top of it. - $dbh->bz_drop_table('tag'); - $dbh->bz_drop_index('tags', 'tags_user_id_idx'); - $dbh->bz_rename_table('tags','tag'); - $dbh->bz_add_index('tag', 'tag_user_id_idx', - {FIELDS => [qw(user_id name)], TYPE => 'UNIQUE'}); - } - if (my $bug_tag_fk = $dbh->bz_fk_info('bug_tag', 'tag_id')) { - # bz_rename_table() didn't handle FKs correctly. - if ($bug_tag_fk->{TABLE} eq 'tags') { - $bug_tag_fk->{TABLE} = 'tag'; - $dbh->bz_alter_fk('bug_tag', 'tag_id', $bug_tag_fk); - } + my $dbh = Bugzilla->dbh; + if ($dbh->bz_table_info('tags')) { + + # If we get here, it's because the schema created "tag" as an empty + # table while "tags" still exists. We get rid of the empty + # tag table so we can do the rename over the top of it. + $dbh->bz_drop_table('tag'); + $dbh->bz_drop_index('tags', 'tags_user_id_idx'); + $dbh->bz_rename_table('tags', 'tag'); + $dbh->bz_add_index('tag', 'tag_user_id_idx', + {FIELDS => [qw(user_id name)], TYPE => 'UNIQUE'}); + } + if (my $bug_tag_fk = $dbh->bz_fk_info('bug_tag', 'tag_id')) { + + # bz_rename_table() didn't handle FKs correctly. + if ($bug_tag_fk->{TABLE} eq 'tags') { + $bug_tag_fk->{TABLE} = 'tag'; + $dbh->bz_alter_fk('bug_tag', 'tag_id', $bug_tag_fk); } + } } sub _on_delete_set_null_for_audit_log_userid { - my $dbh = Bugzilla->dbh; - my $fk = $dbh->bz_fk_info('audit_log', 'user_id'); - if ($fk and !defined $fk->{DELETE}) { - $fk->{DELETE} = 'SET NULL'; - $dbh->bz_alter_fk('audit_log', 'user_id', $fk); - } + my $dbh = Bugzilla->dbh; + my $fk = $dbh->bz_fk_info('audit_log', 'user_id'); + if ($fk and !defined $fk->{DELETE}) { + $fk->{DELETE} = 'SET NULL'; + $dbh->bz_alter_fk('audit_log', 'user_id', $fk); + } } sub _fix_notnull_defaults { - my $dbh = Bugzilla->dbh; + my $dbh = Bugzilla->dbh; - $dbh->bz_alter_column('bugs', 'bug_file_loc', - {TYPE => 'MEDIUMTEXT', NOTNULL => 1, - DEFAULT => "''"}, ''); + $dbh->bz_alter_column('bugs', 'bug_file_loc', + {TYPE => 'MEDIUMTEXT', NOTNULL => 1, DEFAULT => "''"}, ''); - my $custom_fields = Bugzilla::Field->match({ - custom => 1, type => [ FIELD_TYPE_FREETEXT, FIELD_TYPE_TEXTAREA ] + my $custom_fields + = Bugzilla::Field->match({ + custom => 1, type => [FIELD_TYPE_FREETEXT, FIELD_TYPE_TEXTAREA] }); - foreach my $field (@$custom_fields) { - if ($field->type == FIELD_TYPE_FREETEXT) { - $dbh->bz_alter_column('bugs', $field->name, - {TYPE => 'varchar(255)', NOTNULL => 1, - DEFAULT => "''"}, ''); - } - if ($field->type == FIELD_TYPE_TEXTAREA) { - $dbh->bz_alter_column('bugs', $field->name, - {TYPE => 'MEDIUMTEXT', NOTNULL => 1, - DEFAULT => "''"}, ''); - } + foreach my $field (@$custom_fields) { + if ($field->type == FIELD_TYPE_FREETEXT) { + $dbh->bz_alter_column('bugs', $field->name, + {TYPE => 'varchar(255)', NOTNULL => 1, DEFAULT => "''"}, ''); } + if ($field->type == FIELD_TYPE_TEXTAREA) { + $dbh->bz_alter_column('bugs', $field->name, + {TYPE => 'MEDIUMTEXT', NOTNULL => 1, DEFAULT => "''"}, ''); + } + } } sub _fix_longdescs_primary_key { - my $dbh = Bugzilla->dbh; - if ($dbh->bz_column_info('longdescs', 'comment_id')->{TYPE} ne 'INTSERIAL') { - $dbh->bz_drop_related_fks('longdescs', 'comment_id'); - $dbh->bz_alter_column('bugs_activity', 'comment_id', {TYPE => 'INT4'}); - $dbh->bz_alter_column('longdescs', 'comment_id', - {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); - } + my $dbh = Bugzilla->dbh; + if ($dbh->bz_column_info('longdescs', 'comment_id')->{TYPE} ne 'INTSERIAL') { + $dbh->bz_drop_related_fks('longdescs', 'comment_id'); + $dbh->bz_alter_column('bugs_activity', 'comment_id', {TYPE => 'INT4'}); + $dbh->bz_alter_column('longdescs', 'comment_id', + {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); + } } sub _fix_longdescs_indexes { - my $dbh = Bugzilla->dbh; - my $bug_id_idx = $dbh->bz_index_info('longdescs', 'longdescs_bug_id_idx'); - if ($bug_id_idx && scalar @{$bug_id_idx->{'FIELDS'}} < 2) { - $dbh->bz_drop_index('longdescs', 'longdescs_bug_id_idx'); - $dbh->bz_add_index('longdescs', 'longdescs_bug_id_idx', [qw(bug_id work_time)]); - } + my $dbh = Bugzilla->dbh; + my $bug_id_idx = $dbh->bz_index_info('longdescs', 'longdescs_bug_id_idx'); + if ($bug_id_idx && scalar @{$bug_id_idx->{'FIELDS'}} < 2) { + $dbh->bz_drop_index('longdescs', 'longdescs_bug_id_idx'); + $dbh->bz_add_index('longdescs', 'longdescs_bug_id_idx', [qw(bug_id work_time)]); + } } sub _fix_dependencies_dupes { - my $dbh = Bugzilla->dbh; - my $blocked_idx = $dbh->bz_index_info('dependencies', 'dependencies_blocked_idx'); - if ($blocked_idx && scalar @{$blocked_idx->{'FIELDS'}} < 2) { - # Remove duplicated entries - my $dupes = $dbh->selectall_arrayref(" + my $dbh = Bugzilla->dbh; + my $blocked_idx + = $dbh->bz_index_info('dependencies', 'dependencies_blocked_idx'); + if ($blocked_idx && scalar @{$blocked_idx->{'FIELDS'}} < 2) { + + # Remove duplicated entries + my $dupes = $dbh->selectall_arrayref(" SELECT blocked, dependson, COUNT(*) AS count - FROM dependencies " . - $dbh->sql_group_by('blocked, dependson') . " - HAVING COUNT(*) > 1", - { Slice => {} }); - print "Removing duplicated entries from the 'dependencies' table...\n" if @$dupes; - foreach my $dupe (@$dupes) { - $dbh->do("DELETE FROM dependencies - WHERE blocked = ? AND dependson = ?", - undef, $dupe->{blocked}, $dupe->{dependson}); - $dbh->do("INSERT INTO dependencies (blocked, dependson) VALUES (?, ?)", - undef, $dupe->{blocked}, $dupe->{dependson}); - } - $dbh->bz_drop_index('dependencies', 'dependencies_blocked_idx'); - $dbh->bz_add_index('dependencies', 'dependencies_blocked_idx', - { FIELDS => [qw(blocked dependson)], TYPE => 'UNIQUE' }); - } + FROM dependencies " . $dbh->sql_group_by('blocked, dependson') . " + HAVING COUNT(*) > 1", {Slice => {}}); + print "Removing duplicated entries from the 'dependencies' table...\n" + if @$dupes; + foreach my $dupe (@$dupes) { + $dbh->do( + "DELETE FROM dependencies + WHERE blocked = ? AND dependson = ?", undef, $dupe->{blocked}, + $dupe->{dependson} + ); + $dbh->do("INSERT INTO dependencies (blocked, dependson) VALUES (?, ?)", + undef, $dupe->{blocked}, $dupe->{dependson}); + } + $dbh->bz_drop_index('dependencies', 'dependencies_blocked_idx'); + $dbh->bz_add_index('dependencies', 'dependencies_blocked_idx', + {FIELDS => [qw(blocked dependson)], TYPE => 'UNIQUE'}); + } } sub _shorten_long_quips { - my $dbh = Bugzilla->dbh; - my $quips = $dbh->selectall_arrayref("SELECT quipid, quip FROM quips - WHERE CHAR_LENGTH(quip) > 512"); - - if (@$quips) { - print "Shortening quips longer than 512 characters:"; - - my $query = $dbh->prepare("UPDATE quips SET quip = ? WHERE quipid = ?"); - - foreach my $quip (@$quips) { - my ($quipid, $quip_str) = @$quip; - $quip_str = substr($quip_str, 0, 509) . "..."; - print " $quipid"; - $query->execute($quip_str, $quipid); - } - print "\n"; + my $dbh = Bugzilla->dbh; + my $quips = $dbh->selectall_arrayref( + "SELECT quipid, quip FROM quips + WHERE CHAR_LENGTH(quip) > 512" + ); + + if (@$quips) { + print "Shortening quips longer than 512 characters:"; + + my $query = $dbh->prepare("UPDATE quips SET quip = ? WHERE quipid = ?"); + + foreach my $quip (@$quips) { + my ($quipid, $quip_str) = @$quip; + $quip_str = substr($quip_str, 0, 509) . "..."; + print " $quipid"; + $query->execute($quip_str, $quipid); } - $dbh->bz_alter_column('quips', 'quip', { TYPE => 'varchar(512)', NOTNULL => 1}); + print "\n"; + } + $dbh->bz_alter_column('quips', 'quip', {TYPE => 'varchar(512)', NOTNULL => 1}); } sub _add_password_salt_separator { - my $dbh = Bugzilla->dbh; + my $dbh = Bugzilla->dbh; - $dbh->bz_start_transaction(); + $dbh->bz_start_transaction(); - my $profiles = $dbh->selectall_arrayref("SELECT userid, cryptpassword FROM profiles WHERE (" - . $dbh->sql_regexp("cryptpassword", "'^[^,]+{'") . ")"); + my $profiles + = $dbh->selectall_arrayref( + "SELECT userid, cryptpassword FROM profiles WHERE (" + . $dbh->sql_regexp("cryptpassword", "'^[^,]+{'") + . ")"); - if (@$profiles) { - say "Adding salt separator to password hashes..."; + if (@$profiles) { + say "Adding salt separator to password hashes..."; - my $query = $dbh->prepare("UPDATE profiles SET cryptpassword = ? WHERE userid = ?"); - my %algo_sizes; + my $query + = $dbh->prepare("UPDATE profiles SET cryptpassword = ? WHERE userid = ?"); + my %algo_sizes; - foreach my $profile (@$profiles) { - my ($userid, $hash) = @$profile; - my ($algorithm) = $hash =~ /{([^}]+)}$/; + foreach my $profile (@$profiles) { + my ($userid, $hash) = @$profile; + my ($algorithm) = $hash =~ /{([^}]+)}$/; - $algo_sizes{$algorithm} ||= length(Digest->new($algorithm)->b64digest); + $algo_sizes{$algorithm} ||= length(Digest->new($algorithm)->b64digest); - # Calculate the salt length by taking the stored hash and - # subtracting the combined lengths of the hash size, the - # algorithm name, and 2 for the {} surrounding the name. - my $not_salt_len = $algo_sizes{$algorithm} + length($algorithm) + 2; - my $salt_len = length($hash) - $not_salt_len; + # Calculate the salt length by taking the stored hash and + # subtracting the combined lengths of the hash size, the + # algorithm name, and 2 for the {} surrounding the name. + my $not_salt_len = $algo_sizes{$algorithm} + length($algorithm) + 2; + my $salt_len = length($hash) - $not_salt_len; - substr($hash, $salt_len, 0, ','); - $query->execute($hash, $userid); - } + substr($hash, $salt_len, 0, ','); + $query->execute($hash, $userid); } - $dbh->bz_commit_transaction(); + } + $dbh->bz_commit_transaction(); } sub _fix_flagclusions_indexes { - my $dbh = Bugzilla->dbh; - foreach my $table ('flaginclusions', 'flagexclusions') { - my $index = $table . '_type_id_idx'; - my $idx_info = $dbh->bz_index_info($table, $index); - if ($idx_info && $idx_info->{'TYPE'} ne 'UNIQUE') { - # Remove duplicated entries - my $dupes = $dbh->selectall_arrayref(" + my $dbh = Bugzilla->dbh; + foreach my $table ('flaginclusions', 'flagexclusions') { + my $index = $table . '_type_id_idx'; + my $idx_info = $dbh->bz_index_info($table, $index); + if ($idx_info && $idx_info->{'TYPE'} ne 'UNIQUE') { + + # Remove duplicated entries + my $dupes = $dbh->selectall_arrayref(" SELECT type_id, product_id, component_id, COUNT(*) AS count - FROM $table " . - $dbh->sql_group_by('type_id, product_id, component_id') . " - HAVING COUNT(*) > 1", - { Slice => {} }); - say "Removing duplicated entries from the '$table' table..." if @$dupes; - foreach my $dupe (@$dupes) { - $dbh->do("DELETE FROM $table + FROM $table " + . $dbh->sql_group_by('type_id, product_id, component_id') . " + HAVING COUNT(*) > 1", {Slice => {}}); + say "Removing duplicated entries from the '$table' table..." if @$dupes; + foreach my $dupe (@$dupes) { + $dbh->do( + "DELETE FROM $table WHERE type_id = ? AND product_id = ? AND component_id = ?", - undef, $dupe->{type_id}, $dupe->{product_id}, $dupe->{component_id}); - $dbh->do("INSERT INTO $table (type_id, product_id, component_id) VALUES (?, ?, ?)", - undef, $dupe->{type_id}, $dupe->{product_id}, $dupe->{component_id}); - } - $dbh->bz_drop_index($table, $index); - $dbh->bz_add_index($table, $index, - { FIELDS => [qw(type_id product_id component_id)], - TYPE => 'UNIQUE' }); - } + undef, $dupe->{type_id}, $dupe->{product_id}, $dupe->{component_id} + ); + $dbh->do( + "INSERT INTO $table (type_id, product_id, component_id) VALUES (?, ?, ?)", + undef, $dupe->{type_id}, $dupe->{product_id}, $dupe->{component_id}); + } + $dbh->bz_drop_index($table, $index); + $dbh->bz_add_index($table, $index, + {FIELDS => [qw(type_id product_id component_id)], TYPE => 'UNIQUE'}); } + } } sub _fix_components_primary_key { - my $dbh = Bugzilla->dbh; - if ($dbh->bz_column_info('components', 'id')->{TYPE} ne 'MEDIUMSERIAL') { - $dbh->bz_drop_related_fks('components', 'id'); - $dbh->bz_alter_column("components", "id", - {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); - $dbh->bz_alter_column("flaginclusions", "component_id", - {TYPE => 'INT3'}); - $dbh->bz_alter_column("flagexclusions", "component_id", - {TYPE => 'INT3'}); - $dbh->bz_alter_column("bugs", "component_id", - {TYPE => 'INT3', NOTNULL => 1}); - $dbh->bz_alter_column("component_cc", "component_id", - {TYPE => 'INT3', NOTNULL => 1}); - } + my $dbh = Bugzilla->dbh; + if ($dbh->bz_column_info('components', 'id')->{TYPE} ne 'MEDIUMSERIAL') { + $dbh->bz_drop_related_fks('components', 'id'); + $dbh->bz_alter_column("components", "id", + {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); + $dbh->bz_alter_column("flaginclusions", "component_id", {TYPE => 'INT3'}); + $dbh->bz_alter_column("flagexclusions", "component_id", {TYPE => 'INT3'}); + $dbh->bz_alter_column("bugs", "component_id", {TYPE => 'INT3', NOTNULL => 1}); + $dbh->bz_alter_column("component_cc", "component_id", + {TYPE => 'INT3', NOTNULL => 1}); + } } sub _fix_user_api_keys_indexes { - my $dbh = Bugzilla->dbh; - - if ($dbh->bz_index_info('user_api_keys', 'user_api_keys_key')) { - $dbh->bz_drop_index('user_api_keys', 'user_api_keys_key'); - $dbh->bz_add_index('user_api_keys', 'user_api_keys_api_key_idx', - { FIELDS => ['api_key'], TYPE => 'UNIQUE' }); - } - if ($dbh->bz_index_info('user_api_keys', 'user_api_keys_user_id')) { - $dbh->bz_drop_index('user_api_keys', 'user_api_keys_user_id'); - $dbh->bz_add_index('user_api_keys', 'user_api_keys_user_id_idx', ['user_id']); - } + my $dbh = Bugzilla->dbh; + + if ($dbh->bz_index_info('user_api_keys', 'user_api_keys_key')) { + $dbh->bz_drop_index('user_api_keys', 'user_api_keys_key'); + $dbh->bz_add_index('user_api_keys', 'user_api_keys_api_key_idx', + {FIELDS => ['api_key'], TYPE => 'UNIQUE'}); + } + if ($dbh->bz_index_info('user_api_keys', 'user_api_keys_user_id')) { + $dbh->bz_drop_index('user_api_keys', 'user_api_keys_user_id'); + $dbh->bz_add_index('user_api_keys', 'user_api_keys_user_id_idx', ['user_id']); + } } sub _update_alias { - my $dbh = Bugzilla->dbh; - return unless $dbh->bz_column_info('bugs', 'alias'); + my $dbh = Bugzilla->dbh; + return unless $dbh->bz_column_info('bugs', 'alias'); - # We need to move the aliases from the bugs table to the bugs_aliases table - $dbh->do(q{ + # We need to move the aliases from the bugs table to the bugs_aliases table + $dbh->do( + q{ INSERT INTO bugs_aliases (bug_id, alias) SELECT bug_id, alias FROM bugs WHERE alias IS NOT NULL - }); + } + ); - $dbh->bz_drop_column('bugs', 'alias'); + $dbh->bz_drop_column('bugs', 'alias'); } sub _sanitize_audit_log_table { - my $dbh = Bugzilla->dbh; - - # Replace hashed passwords by a generic comment. - my $class = 'Bugzilla::User'; - my $field = 'cryptpassword'; - - my $hashed_passwd = - $dbh->selectcol_arrayref('SELECT added FROM audit_log WHERE class = ? AND field = ? - AND ' . $dbh->sql_not_ilike('hashed_with_', 'added'), - undef, ($class, $field)); - if (@$hashed_passwd) { - say "Sanitizing hashed passwords stored in the 'audit_log' table..."; - my $sth = $dbh->prepare('UPDATE audit_log SET added = ? - WHERE class = ? AND field = ? AND added = ?'); - - foreach my $passwd (@$hashed_passwd) { - my (undef, $sanitized_passwd) = - Bugzilla::Object::_sanitize_audit_log($class, $field, [undef, $passwd]); - $sth->execute($sanitized_passwd, $class, $field, $passwd); - } + my $dbh = Bugzilla->dbh; + + # Replace hashed passwords by a generic comment. + my $class = 'Bugzilla::User'; + my $field = 'cryptpassword'; + + my $hashed_passwd = $dbh->selectcol_arrayref( + 'SELECT added FROM audit_log WHERE class = ? AND field = ? + AND ' + . $dbh->sql_not_ilike('hashed_with_', 'added'), undef, ($class, $field) + ); + if (@$hashed_passwd) { + say "Sanitizing hashed passwords stored in the 'audit_log' table..."; + my $sth = $dbh->prepare( + 'UPDATE audit_log SET added = ? + WHERE class = ? AND field = ? AND added = ?' + ); + + foreach my $passwd (@$hashed_passwd) { + my (undef, $sanitized_passwd) + = Bugzilla::Object::_sanitize_audit_log($class, $field, [undef, $passwd]); + $sth->execute($sanitized_passwd, $class, $field, $passwd); } + } } 1; diff --git a/Bugzilla/Install/Filesystem.pm b/Bugzilla/Install/Filesystem.pm index d30ae18dc..e309dc942 100644 --- a/Bugzilla/Install/Filesystem.pm +++ b/Bugzilla/Install/Filesystem.pm @@ -36,11 +36,11 @@ use POSIX (); use parent qw(Exporter); our @EXPORT = qw( - update_filesystem - create_htaccess - fix_all_file_permissions - fix_dir_permissions - fix_file_permissions + update_filesystem + create_htaccess + fix_all_file_permissions + fix_dir_permissions + fix_file_permissions ); use constant HT_DEFAULT_DENY => <localconfig->{'use_suexec'} }; -sub _group { Bugzilla->localconfig->{'webservergroup'} }; +sub _suexec { Bugzilla->localconfig->{'use_suexec'} } +sub _group { Bugzilla->localconfig->{'webservergroup'} } # Writeable by the owner only. use constant OWNER_WRITE => 0600; + # Executable by the owner only. use constant OWNER_EXECUTE => 0700; + # A directory which is only writeable by the owner. use constant DIR_OWNER_WRITE => 0700; # A cgi script that the webserver can execute. -sub WS_EXECUTE { _group() ? 0750 : 0755 }; +sub WS_EXECUTE { _group() ? 0750 : 0755 } + # A file that is read by cgi scripts, but is not ever read # directly by the webserver. -sub CGI_READ { _group() ? 0640 : 0644 }; +sub CGI_READ { _group() ? 0640 : 0644 } + # A file that is written to by cgi scripts, but is not ever # read or written directly by the webserver. -sub CGI_WRITE { _group() ? 0660 : 0666 }; +sub CGI_WRITE { _group() ? 0660 : 0666 } + # A file that is served directly by the web server. -sub WS_SERVE { (_group() and !_suexec()) ? 0640 : 0644 }; +sub WS_SERVE { (_group() and !_suexec()) ? 0640 : 0644 } # A directory whose contents can be read or served by the # webserver (so even directories containing cgi scripts # would have this permission). -sub DIR_WS_SERVE { (_group() and !_suexec()) ? 0750 : 0755 }; +sub DIR_WS_SERVE { (_group() and !_suexec()) ? 0750 : 0755 } + # A directory that is read by cgi scripts, but is never accessed # directly by the webserver -sub DIR_CGI_READ { _group() ? 0750 : 0755 }; +sub DIR_CGI_READ { _group() ? 0750 : 0755 } + # A directory that is written to by cgi scripts, but where the # scripts never needs to overwrite files created by other # users. -sub DIR_CGI_WRITE { _group() ? 0770 : 01777 }; +sub DIR_CGI_WRITE { _group() ? 0770 : 01777 } + # A directory that is written to by cgi scripts, where the # scripts need to overwrite files created by other users. -sub DIR_CGI_OVERWRITE { _group() ? 0770 : 0777 }; +sub DIR_CGI_OVERWRITE { _group() ? 0770 : 0777 } -# This can be combined (using "|") with other permissions for +# This can be combined (using "|") with other permissions for # directories that, in addition to their normal permissions (such # as DIR_CGI_WRITE) also have content served directly from them # (or their subdirectories) to the user, via the webserver. -sub DIR_ALSO_WS_SERVE { _suexec() ? 0001 : 0 }; +sub DIR_ALSO_WS_SERVE { _suexec() ? 0001 : 0 } # This looks like a constant because it effectively is, but # it has to call other subroutines and read the current filesystem, # so it's defined as a sub. This is not exported, so it doesn't have -# a perldoc. However, look at the various hashes defined inside this +# a perldoc. However, look at the various hashes defined inside this # function to understand what it returns. (There are comments throughout.) # # The rationale for the file permissions is that there is a group the @@ -117,196 +125,175 @@ sub DIR_ALSO_WS_SERVE { _suexec() ? 0001 : 0 }; # by this group. Otherwise someone may find it possible to change the cgis # when exploiting some security flaw somewhere (not necessarily in Bugzilla!) sub FILESYSTEM { - my $datadir = bz_locations()->{'datadir'}; - my $attachdir = bz_locations()->{'attachdir'}; - my $extensionsdir = bz_locations()->{'extensionsdir'}; - my $webdotdir = bz_locations()->{'webdotdir'}; - my $templatedir = bz_locations()->{'templatedir'}; - my $libdir = bz_locations()->{'libpath'}; - my $extlib = bz_locations()->{'ext_libpath'}; - my $skinsdir = bz_locations()->{'skinsdir'}; - my $localconfig = bz_locations()->{'localconfig'}; - my $template_cache = bz_locations()->{'template_cache'}; - my $graphsdir = bz_locations()->{'graphsdir'}; - my $assetsdir = bz_locations()->{'assetsdir'}; - - # We want to set the permissions the same for all localconfig files - # across all PROJECTs, so we do something special with $localconfig, - # lower down in the permissions section. - if ($ENV{PROJECT}) { - $localconfig =~ s/\.\Q$ENV{PROJECT}\E$//; - } - - # Note: When being processed by checksetup, these have their permissions - # set in this order: %all_dirs, %recurse_dirs, %all_files. - # - # Each is processed in alphabetical order of keys, so shorter keys - # will have their permissions set before longer keys (thus setting - # the permissions on parent directories before setting permissions - # on their children). - - # --- FILE PERMISSIONS (Non-created files) --- # - my %files = ( - '*' => { perms => OWNER_WRITE }, - # Some .pl files are WS_EXECUTE because we want - # users to be able to cron them or otherwise run - # them as a secure user, like the webserver owner. - '*.cgi' => { perms => WS_EXECUTE }, - 'whineatnews.pl' => { perms => WS_EXECUTE }, - 'collectstats.pl' => { perms => WS_EXECUTE }, - 'importxml.pl' => { perms => WS_EXECUTE }, - 'testserver.pl' => { perms => WS_EXECUTE }, - 'whine.pl' => { perms => WS_EXECUTE }, - 'email_in.pl' => { perms => WS_EXECUTE }, - 'sanitycheck.pl' => { perms => WS_EXECUTE }, - 'checksetup.pl' => { perms => OWNER_EXECUTE }, - 'runtests.pl' => { perms => OWNER_EXECUTE }, - 'jobqueue.pl' => { perms => OWNER_EXECUTE }, - 'migrate.pl' => { perms => OWNER_EXECUTE }, - 'install-module.pl' => { perms => OWNER_EXECUTE }, - 'clean-bug-user-last-visit.pl' => { perms => WS_EXECUTE }, - - 'Bugzilla.pm' => { perms => CGI_READ }, - "$localconfig*" => { perms => CGI_READ }, - 'bugzilla.dtd' => { perms => WS_SERVE }, - 'mod_perl.pl' => { perms => WS_SERVE }, - 'robots.txt' => { perms => WS_SERVE }, - '.htaccess' => { perms => WS_SERVE }, - - 'contrib/README' => { perms => OWNER_WRITE }, - 'contrib/*/README' => { perms => OWNER_WRITE }, - 'contrib/Bugzilla.pm' => { perms => OWNER_WRITE }, - 'docs/bugzilla.ent' => { perms => OWNER_WRITE }, - 'docs/makedocs.pl' => { perms => OWNER_EXECUTE }, - 'docs/style.css' => { perms => WS_SERVE }, - 'docs/*/rel_notes.txt' => { perms => WS_SERVE }, - 'docs/*/README.docs' => { perms => OWNER_WRITE }, - "$datadir/params.json" => { perms => CGI_WRITE }, - "$datadir/old-params.txt" => { perms => OWNER_WRITE }, - "$extensionsdir/create.pl" => { perms => OWNER_EXECUTE }, - "$extensionsdir/*/*.pl" => { perms => WS_EXECUTE }, - ); - - # Directories that we want to set the perms on, but not - # recurse through. These are directories we didn't create - # in checkesetup.pl. - my %non_recurse_dirs = ( - '.' => DIR_WS_SERVE, - docs => DIR_WS_SERVE, - ); - - # This sets the permissions for each item inside each of these - # directories, including the directory itself. - # 'CVS' directories are special, though, and are never readable by - # the webserver. - my %recurse_dirs = ( - # Writeable directories - $template_cache => { files => CGI_READ, - dirs => DIR_CGI_OVERWRITE }, - $attachdir => { files => CGI_WRITE, - dirs => DIR_CGI_WRITE }, - $webdotdir => { files => WS_SERVE, - dirs => DIR_CGI_WRITE | DIR_ALSO_WS_SERVE }, - $graphsdir => { files => WS_SERVE, - dirs => DIR_CGI_WRITE | DIR_ALSO_WS_SERVE }, - "$datadir/db" => { files => CGI_WRITE, - dirs => DIR_CGI_WRITE }, - $assetsdir => { files => WS_SERVE, - dirs => DIR_CGI_OVERWRITE | DIR_ALSO_WS_SERVE }, - - # Readable directories - "$datadir/mining" => { files => CGI_READ, - dirs => DIR_CGI_READ }, - "$libdir/Bugzilla" => { files => CGI_READ, - dirs => DIR_CGI_READ }, - $extlib => { files => CGI_READ, - dirs => DIR_CGI_READ }, - $templatedir => { files => CGI_READ, - dirs => DIR_CGI_READ }, - # Directories in the extensions/ dir are WS_SERVE so that - # the web/ directories can be served by the web server. - # But, for extra security, we deny direct webserver access to - # the lib/ and template/ directories of extensions. - $extensionsdir => { files => CGI_READ, - dirs => DIR_WS_SERVE }, - "$extensionsdir/*/lib" => { files => CGI_READ, - dirs => DIR_CGI_READ }, - "$extensionsdir/*/template" => { files => CGI_READ, - dirs => DIR_CGI_READ }, - - # Content served directly by the webserver - images => { files => WS_SERVE, - dirs => DIR_WS_SERVE }, - js => { files => WS_SERVE, - dirs => DIR_WS_SERVE }, - $skinsdir => { files => WS_SERVE, - dirs => DIR_WS_SERVE }, - 'docs/*/html' => { files => WS_SERVE, - dirs => DIR_WS_SERVE }, - 'docs/*/pdf' => { files => WS_SERVE, - dirs => DIR_WS_SERVE }, - 'docs/*/txt' => { files => WS_SERVE, - dirs => DIR_WS_SERVE }, - 'docs/*/images' => { files => WS_SERVE, - dirs => DIR_WS_SERVE }, - "$extensionsdir/*/web" => { files => WS_SERVE, - dirs => DIR_WS_SERVE }, - - # Directories only for the owner, not for the webserver. - '.bzr' => { files => OWNER_WRITE, - dirs => DIR_OWNER_WRITE }, - t => { files => OWNER_WRITE, - dirs => DIR_OWNER_WRITE }, - xt => { files => OWNER_WRITE, - dirs => DIR_OWNER_WRITE }, - 'docs/lib' => { files => OWNER_WRITE, - dirs => DIR_OWNER_WRITE }, - 'docs/*/xml' => { files => OWNER_WRITE, - dirs => DIR_OWNER_WRITE }, - 'contrib' => { files => OWNER_EXECUTE, - dirs => DIR_OWNER_WRITE, }, - ); - - # --- FILES TO CREATE --- # - - # The name of each directory that we should actually *create*, - # pointing at its default permissions. - my %create_dirs = ( - # This is DIR_ALSO_WS_SERVE because it contains $webdotdir and - # $assetsdir. - $datadir => DIR_CGI_OVERWRITE | DIR_ALSO_WS_SERVE, - # Directories that are read-only for cgi scripts - "$datadir/mining" => DIR_CGI_READ, - "$datadir/extensions" => DIR_CGI_READ, - $extensionsdir => DIR_CGI_READ, - # Directories that cgi scripts can write to. - "$datadir/db" => DIR_CGI_WRITE, - $attachdir => DIR_CGI_WRITE, - $graphsdir => DIR_CGI_WRITE | DIR_ALSO_WS_SERVE, - $webdotdir => DIR_CGI_WRITE | DIR_ALSO_WS_SERVE, - $assetsdir => DIR_CGI_WRITE | DIR_ALSO_WS_SERVE, - # Directories that contain content served directly by the web server. - "$skinsdir/custom" => DIR_WS_SERVE, - "$skinsdir/contrib" => DIR_WS_SERVE, - ); - - # The name of each file, pointing at its default permissions and - # default contents. - my %create_files = ( - "$datadir/extensions/additional" => { perms => CGI_READ, - contents => '' }, - # We create this file so that it always has the right owner - # and permissions. Otherwise, the webserver creates it as - # owned by itself, which can cause problems if jobqueue.pl - # or something else is not running as the webserver or root. - "$datadir/mailer.testfile" => { perms => CGI_WRITE, - contents => '' }, - ); - - # Because checksetup controls the creation of index.html separately - # from all other files, it gets its very own hash. - my %index_html = ( - 'index.html' => { perms => WS_SERVE, contents => <{'datadir'}; + my $attachdir = bz_locations()->{'attachdir'}; + my $extensionsdir = bz_locations()->{'extensionsdir'}; + my $webdotdir = bz_locations()->{'webdotdir'}; + my $templatedir = bz_locations()->{'templatedir'}; + my $libdir = bz_locations()->{'libpath'}; + my $extlib = bz_locations()->{'ext_libpath'}; + my $skinsdir = bz_locations()->{'skinsdir'}; + my $localconfig = bz_locations()->{'localconfig'}; + my $template_cache = bz_locations()->{'template_cache'}; + my $graphsdir = bz_locations()->{'graphsdir'}; + my $assetsdir = bz_locations()->{'assetsdir'}; + + # We want to set the permissions the same for all localconfig files + # across all PROJECTs, so we do something special with $localconfig, + # lower down in the permissions section. + if ($ENV{PROJECT}) { + $localconfig =~ s/\.\Q$ENV{PROJECT}\E$//; + } + + # Note: When being processed by checksetup, these have their permissions + # set in this order: %all_dirs, %recurse_dirs, %all_files. + # + # Each is processed in alphabetical order of keys, so shorter keys + # will have their permissions set before longer keys (thus setting + # the permissions on parent directories before setting permissions + # on their children). + + # --- FILE PERMISSIONS (Non-created files) --- # + my %files = ( + '*' => {perms => OWNER_WRITE}, + + # Some .pl files are WS_EXECUTE because we want + # users to be able to cron them or otherwise run + # them as a secure user, like the webserver owner. + '*.cgi' => {perms => WS_EXECUTE}, + 'whineatnews.pl' => {perms => WS_EXECUTE}, + 'collectstats.pl' => {perms => WS_EXECUTE}, + 'importxml.pl' => {perms => WS_EXECUTE}, + 'testserver.pl' => {perms => WS_EXECUTE}, + 'whine.pl' => {perms => WS_EXECUTE}, + 'email_in.pl' => {perms => WS_EXECUTE}, + 'sanitycheck.pl' => {perms => WS_EXECUTE}, + 'checksetup.pl' => {perms => OWNER_EXECUTE}, + 'runtests.pl' => {perms => OWNER_EXECUTE}, + 'jobqueue.pl' => {perms => OWNER_EXECUTE}, + 'migrate.pl' => {perms => OWNER_EXECUTE}, + 'install-module.pl' => {perms => OWNER_EXECUTE}, + 'clean-bug-user-last-visit.pl' => {perms => WS_EXECUTE}, + + 'Bugzilla.pm' => {perms => CGI_READ}, + "$localconfig*" => {perms => CGI_READ}, + 'bugzilla.dtd' => {perms => WS_SERVE}, + 'mod_perl.pl' => {perms => WS_SERVE}, + 'robots.txt' => {perms => WS_SERVE}, + '.htaccess' => {perms => WS_SERVE}, + + 'contrib/README' => {perms => OWNER_WRITE}, + 'contrib/*/README' => {perms => OWNER_WRITE}, + 'contrib/Bugzilla.pm' => {perms => OWNER_WRITE}, + 'docs/bugzilla.ent' => {perms => OWNER_WRITE}, + 'docs/makedocs.pl' => {perms => OWNER_EXECUTE}, + 'docs/style.css' => {perms => WS_SERVE}, + 'docs/*/rel_notes.txt' => {perms => WS_SERVE}, + 'docs/*/README.docs' => {perms => OWNER_WRITE}, + "$datadir/params.json" => {perms => CGI_WRITE}, + "$datadir/old-params.txt" => {perms => OWNER_WRITE}, + "$extensionsdir/create.pl" => {perms => OWNER_EXECUTE}, + "$extensionsdir/*/*.pl" => {perms => WS_EXECUTE}, + ); + + # Directories that we want to set the perms on, but not + # recurse through. These are directories we didn't create + # in checkesetup.pl. + my %non_recurse_dirs = ('.' => DIR_WS_SERVE, docs => DIR_WS_SERVE,); + + # This sets the permissions for each item inside each of these + # directories, including the directory itself. + # 'CVS' directories are special, though, and are never readable by + # the webserver. + my %recurse_dirs = ( + + # Writeable directories + $template_cache => {files => CGI_READ, dirs => DIR_CGI_OVERWRITE}, + $attachdir => {files => CGI_WRITE, dirs => DIR_CGI_WRITE}, + $webdotdir => {files => WS_SERVE, dirs => DIR_CGI_WRITE | DIR_ALSO_WS_SERVE}, + $graphsdir => {files => WS_SERVE, dirs => DIR_CGI_WRITE | DIR_ALSO_WS_SERVE}, + "$datadir/db" => {files => CGI_WRITE, dirs => DIR_CGI_WRITE}, + $assetsdir => + {files => WS_SERVE, dirs => DIR_CGI_OVERWRITE | DIR_ALSO_WS_SERVE}, + + # Readable directories + "$datadir/mining" => {files => CGI_READ, dirs => DIR_CGI_READ}, + "$libdir/Bugzilla" => {files => CGI_READ, dirs => DIR_CGI_READ}, + $extlib => {files => CGI_READ, dirs => DIR_CGI_READ}, + $templatedir => {files => CGI_READ, dirs => DIR_CGI_READ}, + + # Directories in the extensions/ dir are WS_SERVE so that + # the web/ directories can be served by the web server. + # But, for extra security, we deny direct webserver access to + # the lib/ and template/ directories of extensions. + $extensionsdir => {files => CGI_READ, dirs => DIR_WS_SERVE}, + "$extensionsdir/*/lib" => {files => CGI_READ, dirs => DIR_CGI_READ}, + "$extensionsdir/*/template" => {files => CGI_READ, dirs => DIR_CGI_READ}, + + # Content served directly by the webserver + images => {files => WS_SERVE, dirs => DIR_WS_SERVE}, + js => {files => WS_SERVE, dirs => DIR_WS_SERVE}, + $skinsdir => {files => WS_SERVE, dirs => DIR_WS_SERVE}, + 'docs/*/html' => {files => WS_SERVE, dirs => DIR_WS_SERVE}, + 'docs/*/pdf' => {files => WS_SERVE, dirs => DIR_WS_SERVE}, + 'docs/*/txt' => {files => WS_SERVE, dirs => DIR_WS_SERVE}, + 'docs/*/images' => {files => WS_SERVE, dirs => DIR_WS_SERVE}, + "$extensionsdir/*/web" => {files => WS_SERVE, dirs => DIR_WS_SERVE}, + + # Directories only for the owner, not for the webserver. + '.bzr' => {files => OWNER_WRITE, dirs => DIR_OWNER_WRITE}, + t => {files => OWNER_WRITE, dirs => DIR_OWNER_WRITE}, + xt => {files => OWNER_WRITE, dirs => DIR_OWNER_WRITE}, + 'docs/lib' => {files => OWNER_WRITE, dirs => DIR_OWNER_WRITE}, + 'docs/*/xml' => {files => OWNER_WRITE, dirs => DIR_OWNER_WRITE}, + 'contrib' => {files => OWNER_EXECUTE, dirs => DIR_OWNER_WRITE,}, + ); + + # --- FILES TO CREATE --- # + + # The name of each directory that we should actually *create*, + # pointing at its default permissions. + my %create_dirs = ( + + # This is DIR_ALSO_WS_SERVE because it contains $webdotdir and + # $assetsdir. + $datadir => DIR_CGI_OVERWRITE | DIR_ALSO_WS_SERVE, + + # Directories that are read-only for cgi scripts + "$datadir/mining" => DIR_CGI_READ, + "$datadir/extensions" => DIR_CGI_READ, + $extensionsdir => DIR_CGI_READ, + + # Directories that cgi scripts can write to. + "$datadir/db" => DIR_CGI_WRITE, + $attachdir => DIR_CGI_WRITE, + $graphsdir => DIR_CGI_WRITE | DIR_ALSO_WS_SERVE, + $webdotdir => DIR_CGI_WRITE | DIR_ALSO_WS_SERVE, + $assetsdir => DIR_CGI_WRITE | DIR_ALSO_WS_SERVE, + + # Directories that contain content served directly by the web server. + "$skinsdir/custom" => DIR_WS_SERVE, + "$skinsdir/contrib" => DIR_WS_SERVE, + ); + + # The name of each file, pointing at its default permissions and + # default contents. + my %create_files = ( + "$datadir/extensions/additional" => {perms => CGI_READ, contents => ''}, + + # We create this file so that it always has the right owner + # and permissions. Otherwise, the webserver creates it as + # owned by itself, which can cause problems if jobqueue.pl + # or something else is not running as the webserver or root. + "$datadir/mailer.testfile" => {perms => CGI_WRITE, contents => ''}, + ); + + # Because checksetup controls the creation of index.html separately + # from all other files, it gets its very own hash. + my %index_html = ( + 'index.html' => { + perms => WS_SERVE, + contents => < @@ -317,35 +304,30 @@ sub FILESYSTEM { EOT - } - ); - - # Because checksetup controls the .htaccess creation separately - # by a localconfig variable, these go in a separate variable from - # %create_files. - # - # Note that these get WS_SERVE as their permission - # because they're *read* by the webserver, even though they're not - # actually, themselves, served. - my %htaccess = ( - "$attachdir/.htaccess" => { perms => WS_SERVE, - contents => HT_DEFAULT_DENY }, - "$libdir/Bugzilla/.htaccess" => { perms => WS_SERVE, - contents => HT_DEFAULT_DENY }, - "$extlib/.htaccess" => { perms => WS_SERVE, - contents => HT_DEFAULT_DENY }, - "$templatedir/.htaccess" => { perms => WS_SERVE, - contents => HT_DEFAULT_DENY }, - 'contrib/.htaccess' => { perms => WS_SERVE, - contents => HT_DEFAULT_DENY }, - 't/.htaccess' => { perms => WS_SERVE, - contents => HT_DEFAULT_DENY }, - 'xt/.htaccess' => { perms => WS_SERVE, - contents => HT_DEFAULT_DENY }, - "$datadir/.htaccess" => { perms => WS_SERVE, - contents => HT_DEFAULT_DENY }, - - "$graphsdir/.htaccess" => { perms => WS_SERVE, contents => < {perms => WS_SERVE, contents => HT_DEFAULT_DENY}, + "$libdir/Bugzilla/.htaccess" => + {perms => WS_SERVE, contents => HT_DEFAULT_DENY}, + "$extlib/.htaccess" => {perms => WS_SERVE, contents => HT_DEFAULT_DENY}, + "$templatedir/.htaccess" => {perms => WS_SERVE, contents => HT_DEFAULT_DENY}, + 'contrib/.htaccess' => {perms => WS_SERVE, contents => HT_DEFAULT_DENY}, + 't/.htaccess' => {perms => WS_SERVE, contents => HT_DEFAULT_DENY}, + 'xt/.htaccess' => {perms => WS_SERVE, contents => HT_DEFAULT_DENY}, + "$datadir/.htaccess" => {perms => WS_SERVE, contents => HT_DEFAULT_DENY}, + + "$graphsdir/.htaccess" => { + perms => WS_SERVE, + contents => < @@ -374,9 +356,11 @@ EOT Deny from all EOT - }, + }, - "$webdotdir/.htaccess" => { perms => WS_SERVE, contents => < { + perms => WS_SERVE, + contents => < EOT - }, + }, - "$assetsdir/.htaccess" => { perms => WS_SERVE, contents => < { + perms => WS_SERVE, + contents => < @@ -456,97 +442,102 @@ EOT Deny from all EOT - }, - - ); - - Bugzilla::Hook::process('install_filesystem', { - files => \%files, - create_dirs => \%create_dirs, - non_recurse_dirs => \%non_recurse_dirs, - recurse_dirs => \%recurse_dirs, - create_files => \%create_files, - htaccess => \%htaccess, - }); - - my %all_files = (%create_files, %htaccess, %index_html, %files); - my %all_dirs = (%create_dirs, %non_recurse_dirs); - - return { - create_dirs => \%create_dirs, - recurse_dirs => \%recurse_dirs, - all_dirs => \%all_dirs, - - create_files => \%create_files, - htaccess => \%htaccess, - index_html => \%index_html, - all_files => \%all_files, - }; -} + }, -sub update_filesystem { - my ($params) = @_; - my $fs = FILESYSTEM(); - my %dirs = %{$fs->{create_dirs}}; - my %files = %{$fs->{create_files}}; - - my $datadir = bz_locations->{'datadir'}; - my $graphsdir = bz_locations->{'graphsdir'}; - my $assetsdir = bz_locations->{'assetsdir'}; - # If the graphs/ directory doesn't exist, we're upgrading from - # a version old enough that we need to update the $datadir/mining - # format. - if (-d "$datadir/mining" && !-d $graphsdir) { - _update_old_charts($datadir); - } + ); - # If there is a file named '-All-' in $datadir/mining, then we're still - # having mining files named by product name, and we need to convert them to - # files named by product ID. - if (-e File::Spec->catfile($datadir, 'mining', '-All-')) { - _update_old_mining_filenames(File::Spec->catdir($datadir, 'mining')); + Bugzilla::Hook::process( + 'install_filesystem', + { + files => \%files, + create_dirs => \%create_dirs, + non_recurse_dirs => \%non_recurse_dirs, + recurse_dirs => \%recurse_dirs, + create_files => \%create_files, + htaccess => \%htaccess, } + ); - # By sorting the dirs, we assure that shorter-named directories - # (meaning parent directories) are always created before their - # child directories. - foreach my $dir (sort keys %dirs) { - unless (-d $dir) { - print "Creating $dir directory...\n"; - mkdir $dir or die "mkdir $dir failed: $!"; - # For some reason, passing in the permissions to "mkdir" - # doesn't work right, but doing a "chmod" does. - chmod $dirs{$dir}, $dir or warn "Cannot chmod $dir: $!"; - } - } + my %all_files = (%create_files, %htaccess, %index_html, %files); + my %all_dirs = (%create_dirs, %non_recurse_dirs); - # Move the testfile if we can't write to it, so that we can re-create - # it with the correct permissions below. - my $testfile = "$datadir/mailer.testfile"; - if (-e $testfile and !-w $testfile) { - _rename_file($testfile, "$testfile.old"); - } + return { + create_dirs => \%create_dirs, + recurse_dirs => \%recurse_dirs, + all_dirs => \%all_dirs, - # If old-params.txt exists in the root directory, move it to datadir. - my $oldparamsfile = "old_params.txt"; - if (-e $oldparamsfile) { - _rename_file($oldparamsfile, "$datadir/$oldparamsfile"); - } + create_files => \%create_files, + htaccess => \%htaccess, + index_html => \%index_html, + all_files => \%all_files, + }; +} - # Remove old assets htaccess file to force recreation with correct values. - if (-e "$assetsdir/.htaccess") { - if (read_text("$assetsdir/.htaccess") =~ //) { - unlink("$assetsdir/.htaccess"); - } +sub update_filesystem { + my ($params) = @_; + my $fs = FILESYSTEM(); + my %dirs = %{$fs->{create_dirs}}; + my %files = %{$fs->{create_files}}; + + my $datadir = bz_locations->{'datadir'}; + my $graphsdir = bz_locations->{'graphsdir'}; + my $assetsdir = bz_locations->{'assetsdir'}; + + # If the graphs/ directory doesn't exist, we're upgrading from + # a version old enough that we need to update the $datadir/mining + # format. + if (-d "$datadir/mining" && !-d $graphsdir) { + _update_old_charts($datadir); + } + + # If there is a file named '-All-' in $datadir/mining, then we're still + # having mining files named by product name, and we need to convert them to + # files named by product ID. + if (-e File::Spec->catfile($datadir, 'mining', '-All-')) { + _update_old_mining_filenames(File::Spec->catdir($datadir, 'mining')); + } + + # By sorting the dirs, we assure that shorter-named directories + # (meaning parent directories) are always created before their + # child directories. + foreach my $dir (sort keys %dirs) { + unless (-d $dir) { + print "Creating $dir directory...\n"; + mkdir $dir or die "mkdir $dir failed: $!"; + + # For some reason, passing in the permissions to "mkdir" + # doesn't work right, but doing a "chmod" does. + chmod $dirs{$dir}, $dir or warn "Cannot chmod $dir: $!"; } - - _create_files(%files); - if ($params->{index_html}) { - _create_files(%{$fs->{index_html}}); + } + + # Move the testfile if we can't write to it, so that we can re-create + # it with the correct permissions below. + my $testfile = "$datadir/mailer.testfile"; + if (-e $testfile and !-w $testfile) { + _rename_file($testfile, "$testfile.old"); + } + + # If old-params.txt exists in the root directory, move it to datadir. + my $oldparamsfile = "old_params.txt"; + if (-e $oldparamsfile) { + _rename_file($oldparamsfile, "$datadir/$oldparamsfile"); + } + + # Remove old assets htaccess file to force recreation with correct values. + if (-e "$assetsdir/.htaccess") { + if (read_text("$assetsdir/.htaccess") =~ //) { + unlink("$assetsdir/.htaccess"); } - elsif (-e 'index.html') { - my $templatedir = bz_locations()->{'templatedir'}; - print <{index_html}) { + _create_files(%{$fs->{index_html}}); + } + elsif (-e 'index.html') { + my $templatedir = bz_locations()->{'templatedir'}; + print <{'skinsdir'}; - foreach my $css_file (glob("$skinsdir/custom/*.css"), - glob("$skinsdir/contrib/*/*.css")) - { - _remove_empty_css($css_file); - } + my $skinsdir = bz_locations()->{'skinsdir'}; + foreach my $css_file (glob("$skinsdir/custom/*.css"), + glob("$skinsdir/contrib/*/*.css")) + { + _remove_empty_css($css_file); + } } # A simple helper for the update code that removes "empty" CSS files. sub _remove_empty_css { - my ($file) = @_; - my $basename = basename($file); - my $empty_contents = <; } - if ($file_contents eq $empty_contents) { - print install_string('file_remove', { name => $file }), "\n"; - unlink $file or warn "$file: $!"; - } - }; + if (length($empty_contents) == -s $file) { + open(my $fh, '<', $file) or warn "$file: $!"; + my $file_contents; + { local $/; $file_contents = <$fh>; } + if ($file_contents eq $empty_contents) { + print install_string('file_remove', {name => $file}), "\n"; + unlink $file or warn "$file: $!"; + } + } } # We used to allow a single css file in the skins/contrib/ directory # to be a whole skin. sub _convert_single_file_skins { - my $skinsdir = bz_locations()->{'skinsdir'}; - foreach my $skin_file (glob "$skinsdir/contrib/*.css") { - my $dir_name = $skin_file; - $dir_name =~ s/\.css$//; - mkdir $dir_name or warn "$dir_name: $!"; - _rename_file($skin_file, "$dir_name/global.css"); - } + my $skinsdir = bz_locations()->{'skinsdir'}; + foreach my $skin_file (glob "$skinsdir/contrib/*.css") { + my $dir_name = $skin_file; + $dir_name =~ s/\.css$//; + mkdir $dir_name or warn "$dir_name: $!"; + _rename_file($skin_file, "$dir_name/global.css"); + } } # delete all automatically generated css/js files to force recreation at the # next request. sub _remove_dynamic_assets { - my @files = ( - glob(bz_locations()->{assetsdir} . '/*.css'), - glob(bz_locations()->{assetsdir} . '/*.js'), - ); - foreach my $file (@files) { - unlink($file); - } - - # remove old skins/assets directory - my $old_path = bz_locations()->{skinsdir} . '/assets'; - if (-d $old_path) { - foreach my $file (glob("$old_path/*.css")) { - unlink($file); - } - rmdir($old_path); + my @files = ( + glob(bz_locations()->{assetsdir} . '/*.css'), + glob(bz_locations()->{assetsdir} . '/*.js'), + ); + foreach my $file (@files) { + unlink($file); + } + + # remove old skins/assets directory + my $old_path = bz_locations()->{skinsdir} . '/assets'; + if (-d $old_path) { + foreach my $file (glob("$old_path/*.css")) { + unlink($file); } + rmdir($old_path); + } } sub create_htaccess { - _create_files(%{FILESYSTEM()->{htaccess}}); - - # Repair old .htaccess files - - my $webdot_dir = bz_locations()->{'webdotdir'}; - # The public webdot IP address changed. - my $webdot = new IO::File("$webdot_dir/.htaccess", 'r') - || die "$webdot_dir/.htaccess: $!"; - my $webdot_data; - { local $/; $webdot_data = <$webdot>; } + _create_files(%{FILESYSTEM()->{htaccess}}); + + # Repair old .htaccess files + + my $webdot_dir = bz_locations()->{'webdotdir'}; + + # The public webdot IP address changed. + my $webdot = new IO::File("$webdot_dir/.htaccess", 'r') + || die "$webdot_dir/.htaccess: $!"; + my $webdot_data; + { local $/; $webdot_data = <$webdot>; } + $webdot->close; + if ($webdot_data =~ /192\.20\.225\.10/) { + print "Repairing $webdot_dir/.htaccess...\n"; + $webdot_data =~ s/192\.20\.225\.10/192.20.225.0\/24/g; + $webdot = new IO::File("$webdot_dir/.htaccess", 'w') || die $!; + print $webdot $webdot_data; $webdot->close; - if ($webdot_data =~ /192\.20\.225\.10/) { - print "Repairing $webdot_dir/.htaccess...\n"; - $webdot_data =~ s/192\.20\.225\.10/192.20.225.0\/24/g; - $webdot = new IO::File("$webdot_dir/.htaccess", 'w') || die $!; - print $webdot $webdot_data; - $webdot->close; - } + } } sub _rename_file { - my ($from, $to) = @_; - print install_string('file_rename', { from => $from, to => $to }), "\n"; - if (-e $to) { - warn "$to already exists, not moving\n"; - } - else { - move($from, $to) or warn $!; - } + my ($from, $to) = @_; + print install_string('file_rename', {from => $from, to => $to}), "\n"; + if (-e $to) { + warn "$to already exists, not moving\n"; + } + else { + move($from, $to) or warn $!; + } } # A helper for the above functions. sub _create_files { - my (%files) = @_; - - # It's not necessary to sort these, but it does make the - # output of checksetup.pl look a bit nicer. - foreach my $file (sort keys %files) { - unless (-e $file) { - print "Creating $file...\n"; - my $info = $files{$file}; - my $fh = new IO::File($file, O_WRONLY | O_CREAT, $info->{perms}) - || die $!; - print $fh $info->{contents} if $info->{contents}; - $fh->close; - } + my (%files) = @_; + + # It's not necessary to sort these, but it does make the + # output of checksetup.pl look a bit nicer. + foreach my $file (sort keys %files) { + unless (-e $file) { + print "Creating $file...\n"; + my $info = $files{$file}; + my $fh = new IO::File($file, O_WRONLY | O_CREAT, $info->{perms}) || die $!; + print $fh $info->{contents} if $info->{contents}; + $fh->close; } + } } # If you ran a REALLY old version of Bugzilla, your chart files are in the # wrong format. This code is a little messy, because it's very old, and -# when moving it into this module, I couldn't test it so I left it almost +# when moving it into this module, I couldn't test it so I left it almost # completely alone. sub _update_old_charts { - my ($datadir) = @_; - print "Updating old chart storage format...\n"; - foreach my $in_file (glob("$datadir/mining/*")) { - # Don't try and upgrade image or db files! - next if (($in_file =~ /\.gif$/i) || - ($in_file =~ /\.png$/i) || - ($in_file =~ /\.db$/i) || - ($in_file =~ /\.orig$/i)); - - rename("$in_file", "$in_file.orig") or next; - open(IN, "<", "$in_file.orig") or next; - open(OUT, '>', $in_file) or next; - - # Fields in the header - my @declared_fields; - - # Fields we changed to half way through by mistake - # This list comes from an old version of collectstats.pl - # This part is only for people who ran later versions of 2.11 (devel) - my @intermediate_fields = qw(DATE UNCONFIRMED NEW ASSIGNED REOPENED - RESOLVED VERIFIED CLOSED); - - # Fields we actually want (matches the current collectstats.pl) - my @out_fields = qw(DATE NEW ASSIGNED REOPENED UNCONFIRMED RESOLVED - VERIFIED CLOSED FIXED INVALID WONTFIX LATER REMIND - DUPLICATE WORKSFORME MOVED); - - while () { - if (/^# fields?: (.*)\s$/) { - @declared_fields = map uc, (split /\||\r/, $1); - print OUT "# fields: ", join('|', @out_fields), "\n"; - } - elsif (/^(\d+\|.*)/) { - my @data = split(/\||\r/, $1); - my %data; - if (@data == @declared_fields) { - # old format - for my $i (0 .. $#declared_fields) { - $data{$declared_fields[$i]} = $data[$i]; - } - } - elsif (@data == @intermediate_fields) { - # Must have changed over at this point - for my $i (0 .. $#intermediate_fields) { - $data{$intermediate_fields[$i]} = $data[$i]; - } - } - elsif (@data == @out_fields) { - # This line's fine - it has the right number of entries - for my $i (0 .. $#out_fields) { - $data{$out_fields[$i]} = $data[$i]; - } - } - else { - print "Oh dear, input line $. of $in_file had " . - scalar(@data) . " fields\nThis was unexpected.", - " You may want to check your data files.\n"; - } - - print OUT join('|', - map { defined ($data{$_}) ? ($data{$_}) : "" } @out_fields), - "\n"; - } - else { - print OUT; - } + my ($datadir) = @_; + print "Updating old chart storage format...\n"; + foreach my $in_file (glob("$datadir/mining/*")) { + + # Don't try and upgrade image or db files! + next + if (($in_file =~ /\.gif$/i) + || ($in_file =~ /\.png$/i) + || ($in_file =~ /\.db$/i) + || ($in_file =~ /\.orig$/i)); + + rename("$in_file", "$in_file.orig") or next; + open(IN, "<", "$in_file.orig") or next; + open(OUT, '>', $in_file) or next; + + # Fields in the header + my @declared_fields; + + # Fields we changed to half way through by mistake + # This list comes from an old version of collectstats.pl + # This part is only for people who ran later versions of 2.11 (devel) + my @intermediate_fields = qw(DATE UNCONFIRMED NEW ASSIGNED REOPENED + RESOLVED VERIFIED CLOSED); + + # Fields we actually want (matches the current collectstats.pl) + my @out_fields = qw(DATE NEW ASSIGNED REOPENED UNCONFIRMED RESOLVED + VERIFIED CLOSED FIXED INVALID WONTFIX LATER REMIND + DUPLICATE WORKSFORME MOVED); + + while () { + if (/^# fields?: (.*)\s$/) { + @declared_fields = map uc, (split /\||\r/, $1); + print OUT "# fields: ", join('|', @out_fields), "\n"; + } + elsif (/^(\d+\|.*)/) { + my @data = split(/\||\r/, $1); + my %data; + if (@data == @declared_fields) { + + # old format + for my $i (0 .. $#declared_fields) { + $data{$declared_fields[$i]} = $data[$i]; + } + } + elsif (@data == @intermediate_fields) { + + # Must have changed over at this point + for my $i (0 .. $#intermediate_fields) { + $data{$intermediate_fields[$i]} = $data[$i]; + } + } + elsif (@data == @out_fields) { + + # This line's fine - it has the right number of entries + for my $i (0 .. $#out_fields) { + $data{$out_fields[$i]} = $data[$i]; + } + } + else { + print "Oh dear, input line $. of $in_file had " + . scalar(@data) + . " fields\nThis was unexpected.", + " You may want to check your data files.\n"; } - close(IN); - close(OUT); - } + print OUT join('|', map { defined($data{$_}) ? ($data{$_}) : "" } @out_fields), + "\n"; + } + else { + print OUT; + } + } + + close(IN); + close(OUT); + } } # The old naming scheme has product names as mining file names; we rename them # to product IDs. sub _update_old_mining_filenames { - my ($miningdir) = @_; - my $dbh = Bugzilla->dbh; - my @conversion_errors; - - # We use a dummy product instance with ID 0, representing all products - my $product_all = {id => 0, name => '-All-'}; - - print "Updating old charting data file names..."; - 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})) { - push(@conversion_errors, - { product => $product, - message => 'A file named "' . $product->{id} . - '" already exists.' }); + my ($miningdir) = @_; + my $dbh = Bugzilla->dbh; + my @conversion_errors; + + # We use a dummy product instance with ID 0, representing all products + my $product_all = {id => 0, name => '-All-'}; + + print "Updating old charting data file names..."; + 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})) { + push( + @conversion_errors, + { + product => $product, + message => 'A file named "' . $product->{id} . '" already exists.' } + ); } + } - 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}))) { - push(@conversion_errors, - { product => $product, - message => $! }); - } - } - } + if (!@conversion_errors) { - # Error reporting - if (! @conversion_errors) { - print " done.\n"; + # 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}) + ) + ) + { + push(@conversion_errors, {product => $product, message => $!}); + } } - else { - 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->{message}; - } - print "You need to empty the \"$miningdir\" directory, then run\n", - " collectstats.pl --regenerate\n", - "in order to clean this up.\n"; + } + + # Error reporting + if (!@conversion_errors) { + print " done.\n"; + } + else { + 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->{message}; } + print "You need to empty the \"$miningdir\" directory, then run\n", + " collectstats.pl --regenerate\n", "in order to clean this up.\n"; + } } sub fix_dir_permissions { - my ($dir) = @_; - return if ON_WINDOWS; - # Note that _get_owner_and_group is always silent here. - my ($owner_id, $group_id) = _get_owner_and_group(); - - my $perms; - my $fs = FILESYSTEM(); - if ($perms = $fs->{recurse_dirs}->{$dir}) { - _fix_perms_recursively($dir, $owner_id, $group_id, $perms); - } - elsif ($perms = $fs->{all_dirs}->{$dir}) { - _fix_perms($dir, $owner_id, $group_id, $perms); - } - else { - # Do nothing. We know nothing about this directory. - warn "Unknown directory $dir"; - } + my ($dir) = @_; + return if ON_WINDOWS; + + # Note that _get_owner_and_group is always silent here. + my ($owner_id, $group_id) = _get_owner_and_group(); + + my $perms; + my $fs = FILESYSTEM(); + if ($perms = $fs->{recurse_dirs}->{$dir}) { + _fix_perms_recursively($dir, $owner_id, $group_id, $perms); + } + elsif ($perms = $fs->{all_dirs}->{$dir}) { + _fix_perms($dir, $owner_id, $group_id, $perms); + } + else { + # Do nothing. We know nothing about this directory. + warn "Unknown directory $dir"; + } } sub fix_file_permissions { - my ($file) = @_; - return if ON_WINDOWS; - my $perms = FILESYSTEM()->{all_files}->{$file}->{perms}; - # Note that _get_owner_and_group is always silent here. - my ($owner_id, $group_id) = _get_owner_and_group(); - _fix_perms($file, $owner_id, $group_id, $perms); + my ($file) = @_; + return if ON_WINDOWS; + my $perms = FILESYSTEM()->{all_files}->{$file}->{perms}; + + # Note that _get_owner_and_group is always silent here. + my ($owner_id, $group_id) = _get_owner_and_group(); + _fix_perms($file, $owner_id, $group_id, $perms); } sub fix_all_file_permissions { - my ($output) = @_; + my ($output) = @_; - # _get_owner_and_group also checks that the webservergroup is valid. - my ($owner_id, $group_id) = _get_owner_and_group($output); + # _get_owner_and_group also checks that the webservergroup is valid. + my ($owner_id, $group_id) = _get_owner_and_group($output); - return if ON_WINDOWS; + return if ON_WINDOWS; - my $fs = FILESYSTEM(); - my %files = %{$fs->{all_files}}; - my %dirs = %{$fs->{all_dirs}}; - my %recurse_dirs = %{$fs->{recurse_dirs}}; + my $fs = FILESYSTEM(); + my %files = %{$fs->{all_files}}; + my %dirs = %{$fs->{all_dirs}}; + my %recurse_dirs = %{$fs->{recurse_dirs}}; - print get_text('install_file_perms_fix') . "\n" if $output; + print get_text('install_file_perms_fix') . "\n" if $output; - foreach my $dir (sort keys %dirs) { - next unless -d $dir; - _fix_perms($dir, $owner_id, $group_id, $dirs{$dir}); - } + foreach my $dir (sort keys %dirs) { + next unless -d $dir; + _fix_perms($dir, $owner_id, $group_id, $dirs{$dir}); + } - foreach my $pattern (sort keys %recurse_dirs) { - my $perms = $recurse_dirs{$pattern}; - # %recurse_dirs supports globs - foreach my $dir (glob $pattern) { - next unless -d $dir; - _fix_perms_recursively($dir, $owner_id, $group_id, $perms); - } + foreach my $pattern (sort keys %recurse_dirs) { + my $perms = $recurse_dirs{$pattern}; + + # %recurse_dirs supports globs + foreach my $dir (glob $pattern) { + next unless -d $dir; + _fix_perms_recursively($dir, $owner_id, $group_id, $perms); } + } - foreach my $file (sort keys %files) { - # %files supports globs - foreach my $filename (glob $file) { - # Don't touch directories. - next if -d $filename || !-e $filename; - _fix_perms($filename, $owner_id, $group_id, - $files{$file}->{perms}); - } + foreach my $file (sort keys %files) { + + # %files supports globs + foreach my $filename (glob $file) { + + # Don't touch directories. + next if -d $filename || !-e $filename; + _fix_perms($filename, $owner_id, $group_id, $files{$file}->{perms}); } + } - _fix_cvs_dirs($owner_id, '.'); + _fix_cvs_dirs($owner_id, '.'); } sub _get_owner_and_group { - my ($output) = @_; - my $group_id = _check_web_server_group($output); - return () if ON_WINDOWS; + my ($output) = @_; + my $group_id = _check_web_server_group($output); + return () if ON_WINDOWS; - my $owner_id = POSIX::getuid(); - $group_id = POSIX::getgid() unless defined $group_id; - return ($owner_id, $group_id); + my $owner_id = POSIX::getuid(); + $group_id = POSIX::getgid() unless defined $group_id; + return ($owner_id, $group_id); } # A helper for fix_all_file_permissions sub _fix_cvs_dirs { - my ($owner_id, $dir) = @_; - my $owner_gid = POSIX::getgid(); - find({ no_chdir => 1, wanted => sub { + my ($owner_id, $dir) = @_; + my $owner_gid = POSIX::getgid(); + find( + { + no_chdir => 1, + wanted => sub { my $name = $File::Find::name; - if ($File::Find::dir =~ /\/CVS/ || $_ eq '.cvsignore' - || (-d $name && $_ =~ /CVS$/)) + if ( $File::Find::dir =~ /\/CVS/ + || $_ eq '.cvsignore' + || (-d $name && $_ =~ /CVS$/)) { - my $perms = 0600; - if (-d $name) { - $perms = 0700; - } - _fix_perms($name, $owner_id, $owner_gid, $perms); + my $perms = 0600; + if (-d $name) { + $perms = 0700; + } + _fix_perms($name, $owner_id, $owner_gid, $perms); } - }}, $dir); + } + }, + $dir + ); } sub _fix_perms { - my ($name, $owner, $group, $perms) = @_; - #printf ("Changing $name to %o\n", $perms); - - # The webserver should never try to chown files. - if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) { - chown $owner, $group, $name - or warn install_string('chown_failed', { path => $name, - error => $! }) . "\n"; - } - chmod $perms, $name - or warn install_string('chmod_failed', { path => $name, - error => $! }) . "\n"; + my ($name, $owner, $group, $perms) = @_; + + #printf ("Changing $name to %o\n", $perms); + + # The webserver should never try to chown files. + if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) { + chown $owner, $group, $name + or warn install_string('chown_failed', {path => $name, error => $!}) . "\n"; + } + chmod $perms, $name + or warn install_string('chmod_failed', {path => $name, error => $!}) . "\n"; } sub _fix_perms_recursively { - my ($dir, $owner_id, $group_id, $perms) = @_; - # Set permissions on the directory itself. - _fix_perms($dir, $owner_id, $group_id, $perms->{dirs}); - # Now recurse through the directory and set the correct permissions - # on subdirectories and files. - find({ no_chdir => 1, wanted => sub { + my ($dir, $owner_id, $group_id, $perms) = @_; + + # Set permissions on the directory itself. + _fix_perms($dir, $owner_id, $group_id, $perms->{dirs}); + + # Now recurse through the directory and set the correct permissions + # on subdirectories and files. + find( + { + no_chdir => 1, + wanted => sub { my $name = $File::Find::name; if (-d $name) { - _fix_perms($name, $owner_id, $group_id, $perms->{dirs}); + _fix_perms($name, $owner_id, $group_id, $perms->{dirs}); } else { - _fix_perms($name, $owner_id, $group_id, $perms->{files}); + _fix_perms($name, $owner_id, $group_id, $perms->{files}); } - }}, $dir); + } + }, + $dir + ); } sub _check_web_server_group { - my ($output) = @_; - - my $group = Bugzilla->localconfig->{'webservergroup'}; - my $filename = bz_locations()->{'localconfig'}; - my $group_id; - - # If we are on Windows, webservergroup does nothing - if (ON_WINDOWS && $group && $output) { - print "\n\n" . get_text('install_webservergroup_windows') . "\n\n"; - } - - # If we're not on Windows, make sure that webservergroup isn't - # empty. - elsif (!ON_WINDOWS && !$group && $output) { - print "\n\n" . get_text('install_webservergroup_empty') . "\n\n"; - } - - # If we're not on Windows, make sure we are actually a member of - # the webservergroup. - elsif (!ON_WINDOWS && $group) { - $group_id = getgrnam($group); - ThrowCodeError('invalid_webservergroup', { group => $group }) - unless defined $group_id; - - # If on unix, see if we need to print a warning about a webservergroup - # that we can't chgrp to - if ($output && $< != 0 && !grep($_ eq $group_id, split(" ", $)))) { - print "\n\n" . get_text('install_webservergroup_not_in') . "\n\n"; - } + my ($output) = @_; + + my $group = Bugzilla->localconfig->{'webservergroup'}; + my $filename = bz_locations()->{'localconfig'}; + my $group_id; + + # If we are on Windows, webservergroup does nothing + if (ON_WINDOWS && $group && $output) { + print "\n\n" . get_text('install_webservergroup_windows') . "\n\n"; + } + + # If we're not on Windows, make sure that webservergroup isn't + # empty. + elsif (!ON_WINDOWS && !$group && $output) { + print "\n\n" . get_text('install_webservergroup_empty') . "\n\n"; + } + + # If we're not on Windows, make sure we are actually a member of + # the webservergroup. + elsif (!ON_WINDOWS && $group) { + $group_id = getgrnam($group); + ThrowCodeError('invalid_webservergroup', {group => $group}) + unless defined $group_id; + + # If on unix, see if we need to print a warning about a webservergroup + # that we can't chgrp to + if ($output && $< != 0 && !grep($_ eq $group_id, split(" ", $)))) { + print "\n\n" . get_text('install_webservergroup_not_in') . "\n\n"; } + } - return $group_id; + return $group_id; } 1; diff --git a/Bugzilla/Install/Localconfig.pm b/Bugzilla/Install/Localconfig.pm index 7f473cc77..bc4557309 100644 --- a/Bugzilla/Install/Localconfig.pm +++ b/Bugzilla/Install/Localconfig.pm @@ -31,151 +31,104 @@ use Term::ANSIColor; use parent qw(Exporter); our @EXPORT_OK = qw( - read_localconfig - update_localconfig + read_localconfig + update_localconfig ); use constant LOCALCONFIG_VARS => ( - { - name => 'create_htaccess', - default => 1, - }, - { - name => 'webservergroup', - default => ON_WINDOWS ? '' : 'apache', - }, - { - name => 'use_suexec', - default => 0, - }, - { - name => 'db_driver', - default => 'mysql', - }, - { - name => 'db_host', - default => 'localhost', - }, - { - name => 'db_name', - default => 'bugs', - }, - { - name => 'db_user', - default => 'bugs', - }, - { - name => 'db_pass', - default => '', - }, - { - name => 'db_port', - default => 0, - }, - { - name => 'db_sock', - default => '', - }, - { - name => 'db_check', - default => 1, - }, - { - name => 'db_mysql_ssl_ca_file', - default => '', - }, - { - name => 'db_mysql_ssl_ca_path', - default => '', - }, - { - name => 'db_mysql_ssl_client_cert', - default => '', - }, - { - name => 'db_mysql_ssl_client_key', - default => '', - }, - { - name => 'index_html', - default => 0, - }, - { - name => 'interdiffbin', - default => sub { bin_loc('interdiff') }, - }, - { - name => 'diffpath', - default => sub { dirname(bin_loc('diff')) }, - }, - { - name => 'site_wide_secret', - # 64 characters is roughly the equivalent of a 384-bit key, which - # is larger than anybody would ever be able to brute-force. - default => sub { generate_random_password(64) }, - }, + {name => 'create_htaccess', default => 1,}, + {name => 'webservergroup', default => ON_WINDOWS ? '' : 'apache',}, + {name => 'use_suexec', default => 0,}, + {name => 'db_driver', default => 'mysql',}, + {name => 'db_host', default => 'localhost',}, + {name => 'db_name', default => 'bugs',}, + {name => 'db_user', default => 'bugs',}, + {name => 'db_pass', default => '',}, + {name => 'db_port', default => 0,}, + {name => 'db_sock', default => '',}, + {name => 'db_check', default => 1,}, + {name => 'db_mysql_ssl_ca_file', default => '',}, + {name => 'db_mysql_ssl_ca_path', default => '',}, + {name => 'db_mysql_ssl_client_cert', default => '',}, + {name => 'db_mysql_ssl_client_key', default => '',}, + {name => 'index_html', default => 0,}, + {name => 'interdiffbin', default => sub { bin_loc('interdiff') },}, + {name => 'diffpath', default => sub { dirname(bin_loc('diff')) },}, + { + name => 'site_wide_secret', + + # 64 characters is roughly the equivalent of a 384-bit key, which + # is larger than anybody would ever be able to brute-force. + default => sub { generate_random_password(64) }, + }, ); sub read_localconfig { - my ($include_deprecated) = @_; - my $filename = bz_locations()->{'localconfig'}; - - my %localconfig; - if (-e $filename) { - my $s = new Safe; - # Some people like to store their database password in another file. - $s->permit('dofile'); - - $s->rdo($filename); - if ($@ || $!) { - my $err_msg = $@ ? $@ : $!; - die install_string('error_localconfig_read', - { error => $err_msg, localconfig => $filename }), "\n"; - } + my ($include_deprecated) = @_; + my $filename = bz_locations()->{'localconfig'}; + + my %localconfig; + if (-e $filename) { + my $s = new Safe; + + # Some people like to store their database password in another file. + $s->permit('dofile'); + + $s->rdo($filename); + if ($@ || $!) { + my $err_msg = $@ ? $@ : $!; + die install_string( + 'error_localconfig_read', {error => $err_msg, localconfig => $filename} + ), + "\n"; + } - my @read_symbols; - if ($include_deprecated) { - # First we have to get the whole symbol table - my $safe_root = $s->root; - my %safe_package; - { no strict 'refs'; %safe_package = %{$safe_root . "::"}; } - # And now we read the contents of every var in the symbol table. - # However: - # * We only include symbols that start with an alphanumeric - # character. This excludes symbols like "_<./localconfig" - # that show up in some perls. - # * We ignore the INC symbol, which exists in every package. - # * Perl 5.10 imports a lot of random symbols that all - # contain "::", and we want to ignore those. - @read_symbols = grep { /^[A-Za-z0-1]/ and !/^INC$/ and !/::/ } - (keys %safe_package); - } - else { - @read_symbols = map($_->{name}, LOCALCONFIG_VARS); - } - foreach my $var (@read_symbols) { - my $glob = $s->varglob($var); - # We can't get the type of a variable out of a Safe automatically. - # We can only get the glob itself. So we figure out its type this - # way, by trying first a scalar, then an array, then a hash. - # - # The interesting thing is that this converts all deprecated - # array or hash vars into hashrefs or arrayrefs, but that's - # fine since as I write this all modern localconfig vars are - # actually scalars. - if (defined $$glob) { - $localconfig{$var} = $$glob; - } - elsif (@$glob) { - $localconfig{$var} = \@$glob; - } - elsif (%$glob) { - $localconfig{$var} = \%$glob; - } - } + my @read_symbols; + if ($include_deprecated) { + + # First we have to get the whole symbol table + my $safe_root = $s->root; + my %safe_package; + { no strict 'refs'; %safe_package = %{$safe_root . "::"}; } + + # And now we read the contents of every var in the symbol table. + # However: + # * We only include symbols that start with an alphanumeric + # character. This excludes symbols like "_<./localconfig" + # that show up in some perls. + # * We ignore the INC symbol, which exists in every package. + # * Perl 5.10 imports a lot of random symbols that all + # contain "::", and we want to ignore those. + @read_symbols + = grep { /^[A-Za-z0-1]/ and !/^INC$/ and !/::/ } (keys %safe_package); } + else { + @read_symbols = map($_->{name}, LOCALCONFIG_VARS); + } + foreach my $var (@read_symbols) { + my $glob = $s->varglob($var); + + # We can't get the type of a variable out of a Safe automatically. + # We can only get the glob itself. So we figure out its type this + # way, by trying first a scalar, then an array, then a hash. + # + # The interesting thing is that this converts all deprecated + # array or hash vars into hashrefs or arrayrefs, but that's + # fine since as I write this all modern localconfig vars are + # actually scalars. + if (defined $$glob) { + $localconfig{$var} = $$glob; + } + elsif (@$glob) { + $localconfig{$var} = \@$glob; + } + elsif (%$glob) { + $localconfig{$var} = \%$glob; + } + } + } - return \%localconfig; + return \%localconfig; } @@ -204,94 +157,97 @@ sub read_localconfig { # Cute, ey? # sub update_localconfig { - my ($params) = @_; - - my $output = $params->{output} || 0; - my $answer = Bugzilla->installation_answers; - my $localconfig = read_localconfig('include deprecated'); - - my @new_vars; - foreach my $var (LOCALCONFIG_VARS) { - my $name = $var->{name}; - my $value = $localconfig->{$name}; - # Regenerate site_wide_secret if it was made by our old, weak - # generate_random_password. Previously we used to generate - # 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) { - $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}; - } + my ($params) = @_; + + my $output = $params->{output} || 0; + my $answer = Bugzilla->installation_answers; + my $localconfig = read_localconfig('include deprecated'); + + my @new_vars; + foreach my $var (LOCALCONFIG_VARS) { + my $name = $var->{name}; + my $value = $localconfig->{$name}; + + # Regenerate site_wide_secret if it was made by our old, weak + # generate_random_password. Previously we used to generate + # 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) { + $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}; + } } - - if (!$localconfig->{'interdiffbin'} && $output) { - print "\n", install_string('patchutils_missing'), "\n"; + } + + if (!$localconfig->{'interdiffbin'} && $output) { + print "\n", install_string('patchutils_missing'), "\n"; + } + + my @old_vars; + foreach my $var (keys %$localconfig) { + push(@old_vars, $var) if !grep($_->{name} eq $var, LOCALCONFIG_VARS); + } + + my $filename = bz_locations->{'localconfig'}; + + # Move any custom or old variables into a separate file. + if (scalar @old_vars) { + my $filename_old = "$filename.old"; + open(my $old_file, ">>:utf8", $filename_old) or die "$filename_old: $!"; + local $Data::Dumper::Purity = 1; + foreach my $var (@old_vars) { + print $old_file Data::Dumper->Dump([$localconfig->{$var}], ["*$var"]) . "\n\n"; } - - my @old_vars; - foreach my $var (keys %$localconfig) { - push(@old_vars, $var) if !grep($_->{name} eq $var, LOCALCONFIG_VARS); - } - - my $filename = bz_locations->{'localconfig'}; - - # Move any custom or old variables into a separate file. - if (scalar @old_vars) { - my $filename_old = "$filename.old"; - open(my $old_file, ">>:utf8", $filename_old) - or die "$filename_old: $!"; - local $Data::Dumper::Purity = 1; - foreach my $var (@old_vars) { - print $old_file Data::Dumper->Dump([$localconfig->{$var}], - ["*$var"]) . "\n\n"; - } - close $old_file; - my $oldstuff = join(', ', @old_vars); - print install_string('lc_old_vars', - { localconfig => $filename, old_file => $filename_old, - vars => $oldstuff }), "\n"; - } - - # Re-write localconfig - open(my $fh, ">:utf8", $filename) or die "$filename: $!"; - foreach my $var (LOCALCONFIG_VARS) { - my $name = $var->{name}; - my $desc = install_string("localconfig_$name", { root => ROOT_USER }); - chomp($desc); - # Make the description into a comment. - $desc =~ s/^/# /mg; - print $fh $desc, "\n", - Data::Dumper->Dump([$localconfig->{$name}], - ["*$name"]), "\n"; - } - - if (@new_vars) { - my $newstuff = join(', ', @new_vars); - print "\n"; - print colored(install_string('lc_new_vars', { localconfig => $filename, - new_vars => wrap_hard($newstuff, 70) }), - COLOR_ERROR), "\n"; - exit; - } - - # Reset the cache for Bugzilla->localconfig so that it will be re-read - delete Bugzilla->request_cache->{localconfig}; - - return { old_vars => \@old_vars, new_vars => \@new_vars }; + close $old_file; + my $oldstuff = join(', ', @old_vars); + print install_string('lc_old_vars', + {localconfig => $filename, old_file => $filename_old, vars => $oldstuff}), + "\n"; + } + + # Re-write localconfig + open(my $fh, ">:utf8", $filename) or die "$filename: $!"; + foreach my $var (LOCALCONFIG_VARS) { + my $name = $var->{name}; + my $desc = install_string("localconfig_$name", {root => ROOT_USER}); + chomp($desc); + + # Make the description into a comment. + $desc =~ s/^/# /mg; + print $fh $desc, "\n", Data::Dumper->Dump([$localconfig->{$name}], ["*$name"]), + "\n"; + } + + if (@new_vars) { + my $newstuff = join(', ', @new_vars); + print "\n"; + print colored( + install_string( + 'lc_new_vars', {localconfig => $filename, new_vars => wrap_hard($newstuff, 70)} + ), + COLOR_ERROR + ), + "\n"; + exit; + } + + # Reset the cache for Bugzilla->localconfig so that it will be re-read + delete Bugzilla->request_cache->{localconfig}; + + return {old_vars => \@old_vars, new_vars => \@new_vars}; } 1; diff --git a/Bugzilla/Install/Requirements.pm b/Bugzilla/Install/Requirements.pm index 61496d843..4c10263ee 100644 --- a/Bugzilla/Install/Requirements.pm +++ b/Bugzilla/Install/Requirements.pm @@ -19,21 +19,21 @@ use warnings; use Bugzilla::Constants; use Bugzilla::Install::Util qw(install_string bin_loc - extension_requirement_packages); + extension_requirement_packages); use List::Util qw(max); use Term::ANSIColor; use parent qw(Exporter); our @EXPORT = qw( - REQUIRED_MODULES - OPTIONAL_MODULES - FEATURE_FILES - - check_requirements - check_graphviz - have_vers - install_command - map_files_to_features + REQUIRED_MODULES + OPTIONAL_MODULES + FEATURE_FILES + + check_requirements + check_graphviz + have_vers + install_command + map_files_to_features ); # This is how many *'s are in the top of each "box" message printed @@ -45,12 +45,12 @@ use constant TABLE_WIDTH => 71; # # The keys are the names of the modules, the values are what the module # is called in the output of "apachectl -t -D DUMP_MODULES". -use constant APACHE_MODULES => { - mod_headers => 'headers_module', - mod_env => 'env_module', - mod_expires => 'expires_module', - mod_rewrite => 'rewrite_module', - mod_version => 'version_module' +use constant APACHE_MODULES => { + mod_headers => 'headers_module', + mod_env => 'env_module', + mod_expires => 'expires_module', + mod_rewrite => 'rewrite_module', + mod_version => 'version_module' }; # These are all of the binaries that we could possibly use that can @@ -63,12 +63,14 @@ use constant APACHE => qw(apachectl httpd apache2 apache); # If we don't find any of the above binaries in the normal PATH, # these are extra places we look. -use constant APACHE_PATH => [qw( - /usr/sbin +use constant APACHE_PATH => [ + qw( + /usr/sbin /usr/local/sbin /usr/libexec /usr/local/libexec -)]; + ) +]; # The below two constants are subroutines so that they can implement # a hook. Other than that they are actually constants. @@ -82,737 +84,757 @@ use constant APACHE_PATH => [qw( # are 'blacklisted'--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 = ( + my @modules = ( { - package => 'CGI.pm', - module => 'CGI', - # 3.51 fixes a security problem that affects Bugzilla. - # (bug 591165) - version => '3.51', - }, - { - package => 'Digest-SHA', - module => 'Digest::SHA', - version => 0 + package => 'CGI.pm', + module => 'CGI', + + # 3.51 fixes a security problem that affects Bugzilla. + # (bug 591165) + version => '3.51', }, + {package => 'Digest-SHA', module => 'Digest::SHA', version => 0}, + # 0.23 fixes incorrect handling of 1/2 & 3/4 timezones. - { - package => 'TimeDate', - module => 'Date::Format', - version => '2.23' - }, + {package => 'TimeDate', module => 'Date::Format', version => '2.23'}, + # 0.75 fixes a warning thrown with Perl 5.17 and newer. - { - package => 'DateTime', - module => 'DateTime', - version => '0.75' - }, + {package => 'DateTime', module => 'DateTime', version => '0.75'}, + # 1.64 fixes a taint issue preventing the local timezone from # being determined on some systems. { - package => 'DateTime-TimeZone', - module => 'DateTime::TimeZone', - version => '1.64' + package => 'DateTime-TimeZone', + module => 'DateTime::TimeZone', + version => '1.64' }, + # 1.54 is required for Perl 5.10+. It also makes DBD::Oracle happy. { - package => 'DBI', - module => 'DBI', - version => ($^V >= v5.13.3) ? '1.614' : '1.54' + package => 'DBI', + module => 'DBI', + version => ($^V >= v5.13.3) ? '1.614' : '1.54' }, + # 2.24 contains several useful text virtual methods. - { - package => 'Template-Toolkit', - module => 'Template', - version => '2.24' - }, + {package => 'Template-Toolkit', module => 'Template', version => '2.24'}, + # 1.300011 has a debug mode for SMTP and automatically pass -i to sendmail. + {package => 'Email-Sender', module => 'Email::Sender', version => '1.300011',}, { - package => 'Email-Sender', - module => 'Email::Sender', - version => '1.300011', - }, - { - package => 'Email-MIME', - module => 'Email::MIME', - # This fixes a memory leak in walk_parts that affected jobqueue.pl. - version => '1.904' + package => 'Email-MIME', + module => 'Email::MIME', + + # This fixes a memory leak in walk_parts that affected jobqueue.pl. + version => '1.904' }, { - package => 'URI', - module => 'URI', - # Follows RFC 3986 to escape characters in URI::Escape. - version => '1.55', + package => 'URI', + module => 'URI', + + # Follows RFC 3986 to escape characters in URI::Escape. + version => '1.55', }, + # 0.32 fixes several memory leaks in the XS version of some functions. + {package => 'List-MoreUtils', module => 'List::MoreUtils', version => 0.32,}, { - package => 'List-MoreUtils', - module => 'List::MoreUtils', - version => 0.32, + package => 'Math-Random-ISAAC', + module => 'Math::Random::ISAAC', + version => '1.0.1', }, { - package => 'Math-Random-ISAAC', - module => 'Math::Random::ISAAC', - version => '1.0.1', - }, - { - package => 'JSON-XS', - module => 'JSON::XS', - # 2.0 is the first version that will work with JSON::RPC. - version => '2.01', + package => 'JSON-XS', + module => 'JSON::XS', + + # 2.0 is the first version that will work with JSON::RPC. + version => '2.01', }, - ); + ); - if (ON_WINDOWS) { - push(@modules, - { - package => 'Win32', - module => 'Win32', - # 0.35 fixes a memory leak in GetOSVersion, which we use. - version => 0.35, - }, - { - package => 'Win32-API', - 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 1.64, so this version must match. - version => '1.64', - } - ); - } + if (ON_WINDOWS) { + push( + @modules, + { + package => 'Win32', + module => 'Win32', - my $extra_modules = _get_extension_requirements('REQUIRED_MODULES'); - push(@modules, @$extra_modules); - return \@modules; -}; + # 0.35 fixes a memory leak in GetOSVersion, which we use. + version => 0.35, + }, + { + package => 'Win32-API', + 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 1.64, so this version must match. + version => '1.64', + } + ); + } + + my $extra_modules = _get_extension_requirements('REQUIRED_MODULES'); + push(@modules, @$extra_modules); + return \@modules; +} sub OPTIONAL_MODULES { - my @modules = ( + my @modules = ( { - package => 'GD', - module => 'GD', - version => '1.20', - feature => [qw(graphical_reports new_charts old_charts)], + package => 'GD', + module => 'GD', + version => '1.20', + feature => [qw(graphical_reports new_charts old_charts)], }, { - package => 'Chart', - module => 'Chart::Lines', - # Versions below 2.4.1 cannot be compared accurately, see - # https://rt.cpan.org/Public/Bug/Display.html?id=28218. - version => '2.4.1', - feature => [qw(new_charts old_charts)], + package => 'Chart', + module => 'Chart::Lines', + + # Versions below 2.4.1 cannot be compared accurately, see + # https://rt.cpan.org/Public/Bug/Display.html?id=28218. + version => '2.4.1', + feature => [qw(new_charts old_charts)], }, { - package => 'Template-GD', - # This module tells us whether or not Template-GD is installed - # on Template-Toolkits after 2.14, and still works with 2.14 and lower. - module => 'Template::Plugin::GD::Image', - version => 0, - feature => ['graphical_reports'], + package => 'Template-GD', + + # This module tells us whether or not Template-GD is installed + # on Template-Toolkits after 2.14, and still works with 2.14 and lower. + module => 'Template::Plugin::GD::Image', + version => 0, + feature => ['graphical_reports'], }, { - package => 'GDTextUtil', - module => 'GD::Text', - version => 0, - feature => ['graphical_reports'], + package => 'GDTextUtil', + module => 'GD::Text', + version => 0, + feature => ['graphical_reports'], }, { - package => 'GDGraph', - module => 'GD::Graph', - version => 0, - feature => ['graphical_reports'], + package => 'GDGraph', + module => 'GD::Graph', + version => 0, + feature => ['graphical_reports'], }, { - package => 'MIME-tools', - # MIME::Parser is packaged as MIME::Tools on ActiveState Perl - module => ON_WINDOWS ? 'MIME::Tools' : 'MIME::Parser', - version => '5.406', - feature => ['moving'], + package => 'MIME-tools', + + # MIME::Parser is packaged as MIME::Tools on ActiveState Perl + module => ON_WINDOWS ? 'MIME::Tools' : 'MIME::Parser', + version => '5.406', + feature => ['moving'], }, { - package => 'libwww-perl', - module => 'LWP::UserAgent', - version => 0, - feature => ['updates'], + package => 'libwww-perl', + module => 'LWP::UserAgent', + version => 0, + feature => ['updates'], }, { - package => 'XML-Twig', - module => 'XML::Twig', - version => 0, - feature => ['moving', 'updates'], + package => 'XML-Twig', + module => 'XML::Twig', + version => 0, + feature => ['moving', 'updates'], }, { - package => 'PatchReader', - module => 'PatchReader', - # 0.9.6 fixes two notable bugs and significantly improves the UX. - version => '0.9.6', - feature => ['patch_viewer'], + package => 'PatchReader', + module => 'PatchReader', + + # 0.9.6 fixes two notable bugs and significantly improves the UX. + version => '0.9.6', + feature => ['patch_viewer'], }, { - package => 'perl-ldap', - module => 'Net::LDAP', - version => 0, - feature => ['auth_ldap'], + package => 'perl-ldap', + module => 'Net::LDAP', + version => 0, + feature => ['auth_ldap'], }, { - package => 'Authen-SASL', - module => 'Authen::SASL', - version => 0, - feature => ['smtp_auth'], + package => 'Authen-SASL', + module => 'Authen::SASL', + version => 0, + feature => ['smtp_auth'], }, { - package => 'Net-SMTP-SSL', - module => 'Net::SMTP::SSL', - version => 1.01, - feature => ['smtp_ssl'], + package => 'Net-SMTP-SSL', + module => 'Net::SMTP::SSL', + version => 1.01, + feature => ['smtp_ssl'], }, { - package => 'RadiusPerl', - module => 'Authen::Radius', - version => 0, - feature => ['auth_radius'], + package => 'RadiusPerl', + module => 'Authen::Radius', + 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', - # Fixes various bugs, including 542931 and 552353 + stops - # throwing warnings with Perl 5.12. - version => '0.712', - # SOAP::Transport::HTTP 1.12 is bogus. - blacklist => ['^1\.12$'], - feature => ['xmlrpc'], + package => 'SOAP-Lite', + module => 'SOAP::Lite', + + # Fixes various bugs, including 542931 and 552353 + stops + # throwing warnings with Perl 5.12. + version => '0.712', + + # SOAP::Transport::HTTP 1.12 is bogus. + blacklist => ['^1\.12$'], + 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 => 'XMLRPC-Lite', + module => 'XMLRPC::Lite', + version => '0.712', + feature => ['xmlrpc'], }, { - package => 'JSON-RPC', - module => 'JSON::RPC', - version => 0, - feature => ['jsonrpc', 'rest'], + package => 'JSON-RPC', + module => 'JSON::RPC', + version => 0, + feature => ['jsonrpc', 'rest'], }, { - package => 'Test-Taint', - module => 'Test::Taint', - # 1.06 no longer throws warnings with Perl 5.10+. - version => 1.06, - feature => ['jsonrpc', 'xmlrpc', 'rest'], + package => 'Test-Taint', + module => 'Test::Taint', + + # 1.06 no longer throws warnings with Perl 5.10+. + version => 1.06, + feature => ['jsonrpc', 'xmlrpc', 'rest'], }, { - # We need the 'utf8_mode' method of HTML::Parser, for HTML::Scrubber. - package => 'HTML-Parser', - module => 'HTML::Parser', - version => ($^V >= v5.13.3) ? '3.67' : '3.40', - feature => ['html_desc'], + # We need the 'utf8_mode' method of HTML::Parser, for HTML::Scrubber. + package => 'HTML-Parser', + module => 'HTML::Parser', + version => ($^V >= v5.13.3) ? '3.67' : '3.40', + feature => ['html_desc'], }, { - package => 'HTML-Scrubber', - module => 'HTML::Scrubber', - version => 0, - feature => ['html_desc'], + package => 'HTML-Scrubber', + module => 'HTML::Scrubber', + version => 0, + feature => ['html_desc'], }, { - # we need version 2.21 of Encode for mime_name - package => 'Encode', - module => 'Encode', - version => 2.21, - feature => ['detect_charset'], + # we need version 2.21 of Encode for mime_name + package => 'Encode', + module => 'Encode', + version => 2.21, + feature => ['detect_charset'], }, { - package => 'Encode-Detect', - module => 'Encode::Detect', - version => 0, - feature => ['detect_charset'], + package => 'Encode-Detect', + module => 'Encode::Detect', + version => 0, + feature => ['detect_charset'], }, # Inbound Email { - package => 'Email-Reply', - module => 'Email::Reply', - version => 0, - feature => ['inbound_email'], + package => 'Email-Reply', + module => 'Email::Reply', + version => 0, + feature => ['inbound_email'], }, { - package => 'HTML-FormatText-WithLinks', - module => 'HTML::FormatText::WithLinks', - # We need 0.13 to set the "bold" marker to "*". - version => '0.13', - feature => ['inbound_email'], + package => 'HTML-FormatText-WithLinks', + module => 'HTML::FormatText::WithLinks', + + # We need 0.13 to set the "bold" marker to "*". + version => '0.13', + feature => ['inbound_email'], }, # Mail Queueing { - package => 'TheSchwartz', - module => 'TheSchwartz', - # 1.07 supports the prioritization of jobs. - version => 1.07, - feature => ['jobqueue'], + package => 'TheSchwartz', + module => 'TheSchwartz', + + # 1.07 supports the prioritization of jobs. + version => 1.07, + feature => ['jobqueue'], }, { - package => 'Daemon-Generic', - module => 'Daemon::Generic', - version => 0, - feature => ['jobqueue'], + package => 'Daemon-Generic', + module => 'Daemon::Generic', + version => 0, + feature => ['jobqueue'], }, # mod_perl { - package => 'mod_perl', - module => 'mod_perl2', - version => '1.999022', - feature => ['mod_perl'], + package => 'mod_perl', + module => 'mod_perl2', + version => '1.999022', + feature => ['mod_perl'], }, { - package => 'Apache-SizeLimit', - module => 'Apache2::SizeLimit', - # 0.96 properly determines process size on Linux. - version => '0.96', - feature => ['mod_perl'], + package => 'Apache-SizeLimit', + module => 'Apache2::SizeLimit', + + # 0.96 properly determines process size on Linux. + version => '0.96', + feature => ['mod_perl'], }, # typesniffer { - package => 'File-MimeInfo', - module => 'File::MimeInfo::Magic', - version => '0', - feature => ['typesniffer'], + package => 'File-MimeInfo', + module => 'File::MimeInfo::Magic', + version => '0', + feature => ['typesniffer'], }, { - package => 'IO-stringy', - module => 'IO::Scalar', - version => '0', - feature => ['typesniffer'], + package => 'IO-stringy', + module => 'IO::Scalar', + version => '0', + feature => ['typesniffer'], }, # memcached { - package => 'Cache-Memcached', - module => 'Cache::Memcached', - version => '0', - feature => ['memcached'], + package => 'Cache-Memcached', + module => 'Cache::Memcached', + version => '0', + feature => ['memcached'], }, # Documentation { - package => 'File-Copy-Recursive', - module => 'File::Copy::Recursive', - version => 0, - feature => ['documentation'], + package => 'File-Copy-Recursive', + module => 'File::Copy::Recursive', + version => 0, + feature => ['documentation'], }, { - package => 'File-Which', - module => 'File::Which', - version => 0, - feature => ['documentation'], + package => 'File-Which', + module => 'File::Which', + version => 0, + feature => ['documentation'], }, - ); + ); - my $extra_modules = _get_extension_requirements('OPTIONAL_MODULES'); - push(@modules, @$extra_modules); - return \@modules; -}; + my $extra_modules = _get_extension_requirements('OPTIONAL_MODULES'); + push(@modules, @$extra_modules); + return \@modules; +} # This maps features to the files that require that feature in order # to compile. It is used by t/001compile.t and mod_perl.pl. use constant FEATURE_FILES => ( - jsonrpc => ['Bugzilla/WebService/Server/JSONRPC.pm', 'jsonrpc.cgi'], - xmlrpc => ['Bugzilla/WebService/Server/XMLRPC.pm', 'xmlrpc.cgi', - 'Bugzilla/WebService.pm', 'Bugzilla/WebService/*.pm'], - rest => ['Bugzilla/WebService/Server/REST.pm', 'rest.cgi', - 'Bugzilla/WebService/Server/REST/Resources/*.pm'], - moving => ['importxml.pl'], - auth_ldap => ['Bugzilla/Auth/Verify/LDAP.pm'], - auth_radius => ['Bugzilla/Auth/Verify/RADIUS.pm'], - documentation => ['docs/makedocs.pl'], - inbound_email => ['email_in.pl'], - jobqueue => ['Bugzilla/Job/*', 'Bugzilla/JobQueue.pm', - 'Bugzilla/JobQueue/*', 'jobqueue.pl'], - patch_viewer => ['Bugzilla/Attachment/PatchReader.pm'], - updates => ['Bugzilla/Update.pm'], - memcached => ['Bugzilla/Memcache.pm'], + jsonrpc => ['Bugzilla/WebService/Server/JSONRPC.pm', 'jsonrpc.cgi'], + xmlrpc => [ + 'Bugzilla/WebService/Server/XMLRPC.pm', 'xmlrpc.cgi', + 'Bugzilla/WebService.pm', 'Bugzilla/WebService/*.pm' + ], + rest => [ + 'Bugzilla/WebService/Server/REST.pm', 'rest.cgi', + 'Bugzilla/WebService/Server/REST/Resources/*.pm' + ], + moving => ['importxml.pl'], + auth_ldap => ['Bugzilla/Auth/Verify/LDAP.pm'], + auth_radius => ['Bugzilla/Auth/Verify/RADIUS.pm'], + documentation => ['docs/makedocs.pl'], + inbound_email => ['email_in.pl'], + jobqueue => [ + 'Bugzilla/Job/*', 'Bugzilla/JobQueue.pm', + 'Bugzilla/JobQueue/*', 'jobqueue.pl' + ], + patch_viewer => ['Bugzilla/Attachment/PatchReader.pm'], + updates => ['Bugzilla/Update.pm'], + memcached => ['Bugzilla/Memcache.pm'], ); # This implements the REQUIRED_MODULES and OPTIONAL_MODULES stuff # described in in Bugzilla::Extension. sub _get_extension_requirements { - my ($function) = @_; - - my $packages = extension_requirement_packages(); - my @modules; - foreach my $package (@$packages) { - if ($package->can($function)) { - my $extra_modules = $package->$function; - push(@modules, @$extra_modules); - } - } - return \@modules; -}; + my ($function) = @_; -sub check_requirements { - my ($output) = @_; - - print "\n", install_string('checking_modules'), "\n" if $output; - my $root = ROOT_USER; - my $missing = _check_missing(REQUIRED_MODULES, $output); - - print "\n", install_string('checking_dbd'), "\n" if $output; - my $have_one_dbd = 0; - my $db_modules = DB_MODULE; - foreach my $db (keys %$db_modules) { - my $dbd = $db_modules->{$db}->{dbd}; - $have_one_dbd = 1 if have_vers($dbd, $output); + my $packages = extension_requirement_packages(); + my @modules; + foreach my $package (@$packages) { + if ($package->can($function)) { + my $extra_modules = $package->$function; + push(@modules, @$extra_modules); } + } + return \@modules; +} - print "\n", install_string('checking_optional'), "\n" if $output; - my $missing_optional = _check_missing(OPTIONAL_MODULES, $output); - - my $missing_apache = _missing_apache_modules(APACHE_MODULES, $output); - - # If we're running on Windows, reset the input line terminator so that - # console input works properly - loading CGI tends to mess it up - $/ = "\015\012" if ON_WINDOWS; - - my $pass = !scalar(@$missing) && $have_one_dbd; - return { - pass => $pass, - one_dbd => $have_one_dbd, - missing => $missing, - optional => $missing_optional, - apache => $missing_apache, - any_missing => !$pass || scalar(@$missing_optional), - }; +sub check_requirements { + my ($output) = @_; + + print "\n", install_string('checking_modules'), "\n" if $output; + my $root = ROOT_USER; + my $missing = _check_missing(REQUIRED_MODULES, $output); + + print "\n", install_string('checking_dbd'), "\n" if $output; + my $have_one_dbd = 0; + my $db_modules = DB_MODULE; + foreach my $db (keys %$db_modules) { + my $dbd = $db_modules->{$db}->{dbd}; + $have_one_dbd = 1 if have_vers($dbd, $output); + } + + print "\n", install_string('checking_optional'), "\n" if $output; + my $missing_optional = _check_missing(OPTIONAL_MODULES, $output); + + my $missing_apache = _missing_apache_modules(APACHE_MODULES, $output); + + # If we're running on Windows, reset the input line terminator so that + # console input works properly - loading CGI tends to mess it up + $/ = "\015\012" if ON_WINDOWS; + + my $pass = !scalar(@$missing) && $have_one_dbd; + return { + pass => $pass, + one_dbd => $have_one_dbd, + missing => $missing, + optional => $missing_optional, + apache => $missing_apache, + any_missing => !$pass || scalar(@$missing_optional), + }; } # A helper for check_requirements sub _check_missing { - my ($modules, $output) = @_; + my ($modules, $output) = @_; - my @missing; - foreach my $module (@$modules) { - unless (have_vers($module, $output)) { - push(@missing, $module); - } + my @missing; + foreach my $module (@$modules) { + unless (have_vers($module, $output)) { + push(@missing, $module); } + } - return \@missing; + return \@missing; } sub _missing_apache_modules { - my ($modules, $output) = @_; - my $apachectl = _get_apachectl(); - return [] if !$apachectl; - my $command = "$apachectl -t -D DUMP_MODULES"; - my $cmd_info = `$command 2>&1`; - # If apachectl returned a value greater than 0, then there was an - # error parsing Apache's configuration, and we can't check modules. - my $retval = $?; - if ($retval > 0) { - print STDERR install_string('apachectl_failed', - { command => $command, root => ROOT_USER }), "\n"; - return []; - } - my @missing; - foreach my $module (sort keys %$modules) { - my $ok = _check_apache_module($module, $modules->{$module}, - $cmd_info, $output); - push(@missing, $module) if !$ok; - } - return \@missing; + my ($modules, $output) = @_; + my $apachectl = _get_apachectl(); + return [] if !$apachectl; + my $command = "$apachectl -t -D DUMP_MODULES"; + my $cmd_info = `$command 2>&1`; + + # If apachectl returned a value greater than 0, then there was an + # error parsing Apache's configuration, and we can't check modules. + my $retval = $?; + if ($retval > 0) { + print STDERR install_string('apachectl_failed', + {command => $command, root => ROOT_USER}), "\n"; + return []; + } + my @missing; + foreach my $module (sort keys %$modules) { + my $ok = _check_apache_module($module, $modules->{$module}, $cmd_info, $output); + push(@missing, $module) if !$ok; + } + return \@missing; } sub _get_apachectl { - foreach my $bin_name (APACHE) { - my $bin = bin_loc($bin_name); - return $bin if $bin; - } - # Try again with a possibly different path. - foreach my $bin_name (APACHE) { - my $bin = bin_loc($bin_name, APACHE_PATH); - return $bin if $bin; - } - return undef; + foreach my $bin_name (APACHE) { + my $bin = bin_loc($bin_name); + return $bin if $bin; + } + + # Try again with a possibly different path. + foreach my $bin_name (APACHE) { + my $bin = bin_loc($bin_name, APACHE_PATH); + return $bin if $bin; + } + return undef; } sub _check_apache_module { - my ($module, $config_name, $mod_info, $output) = @_; - my $ok; - if ($mod_info =~ /^\s+\Q$config_name\E\b/m) { - $ok = 1; - } - if ($output) { - _checking_for({ package => $module, ok => $ok }); - } - return $ok; + my ($module, $config_name, $mod_info, $output) = @_; + my $ok; + if ($mod_info =~ /^\s+\Q$config_name\E\b/m) { + $ok = 1; + } + if ($output) { + _checking_for({package => $module, ok => $ok}); + } + return $ok; } sub print_module_instructions { - my ($check_results, $output) = @_; - - # First we print the long explanatory messages. - - if (scalar @{$check_results->{missing}}) { - print install_string('modules_message_required'); - } - - if (!$check_results->{one_dbd}) { - print install_string('modules_message_db'); - } - - if (my @missing = @{$check_results->{optional}} and $output) { - print install_string('modules_message_optional'); - # Now we have to determine how large the table cols will be. - my $longest_name = max(map(length($_->{package}), @missing)); - - # The first column header is at least 11 characters long. - $longest_name = 11 if $longest_name < 11; - - # The table is TABLE_WIDTH characters long. There are seven mandatory - # characters (* and space) in the string. So, we have a total - # of TABLE_WIDTH - 7 characters to work with. - my $remaining_space = (TABLE_WIDTH - 7) - $longest_name; - print '*' x TABLE_WIDTH . "\n"; - printf "* \%${longest_name}s * %-${remaining_space}s *\n", - 'MODULE NAME', 'ENABLES FEATURE(S)'; - print '*' x TABLE_WIDTH . "\n"; - foreach my $package (@missing) { - printf "* \%${longest_name}s * %-${remaining_space}s *\n", - $package->{package}, - _translate_feature($package->{feature}); - } - } - - if (my @missing = @{ $check_results->{apache} }) { - print install_string('modules_message_apache'); - my $missing_string = join(', ', @missing); - my $size = TABLE_WIDTH - 7; - printf "* \%-${size}s *\n", $missing_string; - my $spaces = TABLE_WIDTH - 2; - print "*", (' ' x $spaces), "*\n"; - } - - my $need_module_instructions = - ( (!$output and @{$check_results->{missing}}) - or ($output and $check_results->{any_missing}) ) ? 1 : 0; - - if ($need_module_instructions or @{ $check_results->{apache} }) { - # If any output was required, we want to close the "table" - print "*" x TABLE_WIDTH . "\n"; - } - - # And now we print the actual installation commands. - - if (my @missing = @{$check_results->{optional}} and $output) { - print install_string('commands_optional') . "\n\n"; - foreach my $module (@missing) { - my $command = install_command($module); - printf "%15s: $command\n", $module->{package}; - } - print "\n"; - } - - if (!$check_results->{one_dbd}) { - print install_string('commands_dbd') . "\n"; - my %db_modules = %{DB_MODULE()}; - foreach my $db (keys %db_modules) { - my $command = install_command($db_modules{$db}->{dbd}); - printf "%10s: \%s\n", $db_modules{$db}->{name}, $command; - } - print "\n"; - } - - if (my @missing = @{$check_results->{missing}}) { - print colored(install_string('commands_required'), COLOR_ERROR), "\n"; - foreach my $package (@missing) { - my $command = install_command($package); - print " $command\n"; - } - } - - if ($output && $check_results->{any_missing} && !ON_ACTIVESTATE - && !$check_results->{hide_all}) - { - print install_string('install_all', { perl => $^X }); - } - if (!$check_results->{pass}) { - print colored(install_string('installation_failed'), COLOR_ERROR), - "\n\n"; - } + my ($check_results, $output) = @_; + + # First we print the long explanatory messages. + + if (scalar @{$check_results->{missing}}) { + print install_string('modules_message_required'); + } + + if (!$check_results->{one_dbd}) { + print install_string('modules_message_db'); + } + + if (my @missing = @{$check_results->{optional}} and $output) { + print install_string('modules_message_optional'); + + # Now we have to determine how large the table cols will be. + my $longest_name = max(map(length($_->{package}), @missing)); + + # The first column header is at least 11 characters long. + $longest_name = 11 if $longest_name < 11; + + # The table is TABLE_WIDTH characters long. There are seven mandatory + # characters (* and space) in the string. So, we have a total + # of TABLE_WIDTH - 7 characters to work with. + my $remaining_space = (TABLE_WIDTH - 7) - $longest_name; + print '*' x TABLE_WIDTH . "\n"; + printf "* \%${longest_name}s * %-${remaining_space}s *\n", 'MODULE NAME', + 'ENABLES FEATURE(S)'; + print '*' x TABLE_WIDTH . "\n"; + foreach my $package (@missing) { + printf "* \%${longest_name}s * %-${remaining_space}s *\n", $package->{package}, + _translate_feature($package->{feature}); + } + } + + if (my @missing = @{$check_results->{apache}}) { + print install_string('modules_message_apache'); + my $missing_string = join(', ', @missing); + my $size = TABLE_WIDTH - 7; + printf "* \%-${size}s *\n", $missing_string; + my $spaces = TABLE_WIDTH - 2; + print "*", (' ' x $spaces), "*\n"; + } + + my $need_module_instructions = ( + (!$output and @{$check_results->{missing}}) + or ($output and $check_results->{any_missing}) + ) ? 1 : 0; + + if ($need_module_instructions or @{$check_results->{apache}}) { + + # If any output was required, we want to close the "table" + print "*" x TABLE_WIDTH . "\n"; + } + + # And now we print the actual installation commands. + + if (my @missing = @{$check_results->{optional}} and $output) { + print install_string('commands_optional') . "\n\n"; + foreach my $module (@missing) { + my $command = install_command($module); + printf "%15s: $command\n", $module->{package}; + } + print "\n"; + } + + if (!$check_results->{one_dbd}) { + print install_string('commands_dbd') . "\n"; + my %db_modules = %{DB_MODULE()}; + foreach my $db (keys %db_modules) { + my $command = install_command($db_modules{$db}->{dbd}); + printf "%10s: \%s\n", $db_modules{$db}->{name}, $command; + } + print "\n"; + } + + if (my @missing = @{$check_results->{missing}}) { + print colored(install_string('commands_required'), COLOR_ERROR), "\n"; + foreach my $package (@missing) { + my $command = install_command($package); + print " $command\n"; + } + } + + if ( $output + && $check_results->{any_missing} + && !ON_ACTIVESTATE + && !$check_results->{hide_all}) + { + print install_string('install_all', {perl => $^X}); + } + if (!$check_results->{pass}) { + print colored(install_string('installation_failed'), COLOR_ERROR), "\n\n"; + } } sub _translate_feature { - my $features = shift; - my @strings; - foreach my $feature (@$features) { - push(@strings, install_string("feature_$feature")); - } - return join(', ', @strings); + my $features = shift; + my @strings; + foreach my $feature (@$features) { + push(@strings, install_string("feature_$feature")); + } + return join(', ', @strings); } sub check_graphviz { - my ($output) = @_; + my ($output) = @_; - my $webdotbase = Bugzilla->params->{'webdotbase'}; - return 1 if $webdotbase =~ /^https?:/; + my $webdotbase = Bugzilla->params->{'webdotbase'}; + return 1 if $webdotbase =~ /^https?:/; - my $return; - $return = 1 if -x $webdotbase; + my $return; + $return = 1 if -x $webdotbase; - if ($output) { - _checking_for({ package => 'GraphViz', ok => $return }); - } + if ($output) { + _checking_for({package => 'GraphViz', ok => $return}); + } - if (!$return) { - print install_string('bad_executable', { bin => $webdotbase }), "\n"; - } + if (!$return) { + print install_string('bad_executable', {bin => $webdotbase}), "\n"; + } + + my $webdotdir = bz_locations()->{'webdotdir'}; - my $webdotdir = bz_locations()->{'webdotdir'}; - # Check .htaccess allows access to generated images - if (-e "$webdotdir/.htaccess") { - my $htaccess = new IO::File("$webdotdir/.htaccess", 'r') - || die "$webdotdir/.htaccess: " . $!; - if (!grep(/png/, $htaccess->getlines)) { - print STDERR install_string('webdot_bad_htaccess', - { dir => $webdotdir }), "\n"; - } - $htaccess->close; + # Check .htaccess allows access to generated images + if (-e "$webdotdir/.htaccess") { + my $htaccess = new IO::File("$webdotdir/.htaccess", 'r') + || die "$webdotdir/.htaccess: " . $!; + if (!grep(/png/, $htaccess->getlines)) { + print STDERR install_string('webdot_bad_htaccess', {dir => $webdotdir}), "\n"; } + $htaccess->close; + } - return $return; + return $return; } # 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}; - my $package = $params->{package}; - if (!$package) { - $package = $module; - $package =~ s/::/-/g; - } - my $wanted = $params->{version}; - - eval "require $module;"; - # Don't let loading a module change the output-encoding of STDOUT - # or STDERR. (CGI.pm tries to set "binmode" on these file handles when - # it's loaded, and other modules may do the same in the future.) - Bugzilla::Install::Util::set_output_encoding(); - - # VERSION is provided by UNIVERSAL::, and can be called even if - # 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; - - # 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; - } - - if ($output) { - _checking_for({ - package => $package, ok => $vok, wanted => $wanted, - found => $vnum, blacklisted => $blacklisted - }); - } - - return $vok ? 1 : 0; + my ($params, $output) = @_; + my $module = $params->{module}; + my $package = $params->{package}; + if (!$package) { + $package = $module; + $package =~ s/::/-/g; + } + my $wanted = $params->{version}; + + eval "require $module;"; + + # Don't let loading a module change the output-encoding of STDOUT + # or STDERR. (CGI.pm tries to set "binmode" on these file handles when + # it's loaded, and other modules may do the same in the future.) + Bugzilla::Install::Util::set_output_encoding(); + + # VERSION is provided by UNIVERSAL::, and can be called even if + # 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; + + # 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; + } + + if ($output) { + _checking_for({ + package => $package, + ok => $vok, + wanted => $wanted, + found => $vnum, + blacklisted => $blacklisted + }); + } + + return $vok ? 1 : 0; } sub _checking_for { - my ($params) = @_; - my ($package, $ok, $wanted, $blacklisted, $found) = - @$params{qw(package ok wanted blacklisted found)}; - - my $ok_string = $ok ? install_string('module_ok') : ''; - - # If we're actually checking versions (like for Perl modules), then - # we have some rather complex logic to determine what we want to - # show. If we're not checking versions (like for GraphViz) we just - # show "ok" or "not found". - if (exists $params->{found}) { - my $found_string; - # We do a string compare in case it's non-numeric. We make sure - # it's not a version object as negative versions are forbidden. - if ($found && !ref($found) && $found eq '-1') { - $found_string = install_string('module_not_found'); - } - elsif ($found) { - $found_string = install_string('module_found', { ver => $found }); - } - else { - $found_string = install_string('module_unknown_version'); - } - $ok_string = $ok ? "$ok_string: $found_string" : $found_string; + my ($params) = @_; + my ($package, $ok, $wanted, $blacklisted, $found) + = @$params{qw(package ok wanted blacklisted found)}; + + my $ok_string = $ok ? install_string('module_ok') : ''; + + # If we're actually checking versions (like for Perl modules), then + # we have some rather complex logic to determine what we want to + # show. If we're not checking versions (like for GraphViz) we just + # show "ok" or "not found". + if (exists $params->{found}) { + my $found_string; + + # We do a string compare in case it's non-numeric. We make sure + # it's not a version object as negative versions are forbidden. + if ($found && !ref($found) && $found eq '-1') { + $found_string = install_string('module_not_found'); + } + elsif ($found) { + $found_string = install_string('module_found', {ver => $found}); } - elsif (!$ok) { - $ok_string = install_string('module_not_found'); + else { + $found_string = install_string('module_unknown_version'); } + $ok_string = $ok ? "$ok_string: $found_string" : $found_string; + } + elsif (!$ok) { + $ok_string = install_string('module_not_found'); + } - my $black_string = $blacklisted ? install_string('blacklisted') : ''; - my $want_string = $wanted ? "v$wanted" : install_string('any'); + my $black_string = $blacklisted ? install_string('blacklisted') : ''; + my $want_string = $wanted ? "v$wanted" : install_string('any'); - my $str = sprintf "%s %20s %-11s $ok_string $black_string\n", - install_string('checking_for'), $package, "($want_string)"; - print $ok ? $str : colored($str, COLOR_ERROR); + my $str = sprintf "%s %20s %-11s $ok_string $black_string\n", + install_string('checking_for'), $package, "($want_string)"; + print $ok ? $str : colored($str, COLOR_ERROR); } sub install_command { - my $module = shift; - my ($command, $package); - - if (ON_ACTIVESTATE) { - $command = 'ppm install %s'; - $package = $module->{package}; - } - else { - $command = "$^X install-module.pl \%s"; - # Non-Windows installations need to use module names, because - # CPAN doesn't understand package names. - $package = $module->{module}; - } - return sprintf $command, $package; + my $module = shift; + my ($command, $package); + + if (ON_ACTIVESTATE) { + $command = 'ppm install %s'; + $package = $module->{package}; + } + else { + $command = "$^X install-module.pl \%s"; + + # Non-Windows installations need to use module names, because + # CPAN doesn't understand package names. + $package = $module->{module}; + } + return sprintf $command, $package; } # This does a reverse mapping for FEATURE_FILES. sub map_files_to_features { - my %features = FEATURE_FILES; - my %files; - foreach my $feature (keys %features) { - my @my_files = @{ $features{$feature} }; - foreach my $pattern (@my_files) { - foreach my $file (glob $pattern) { - $files{$file} = $feature; - } - } - } - return \%files; + my %features = FEATURE_FILES; + my %files; + foreach my $feature (keys %features) { + my @my_files = @{$features{$feature}}; + foreach my $pattern (@my_files) { + foreach my $file (glob $pattern) { + $files{$file} = $feature; + } + } + } + return \%files; } 1; diff --git a/Bugzilla/Install/Util.pm b/Bugzilla/Install/Util.pm index c05037061..26e58c772 100644 --- a/Bugzilla/Install/Util.pm +++ b/Bugzilla/Install/Util.pm @@ -27,195 +27,206 @@ use PerlIO; use parent qw(Exporter); our @EXPORT_OK = qw( - bin_loc - get_version_and_os - extension_code_files - extension_package_directory - extension_requirement_packages - extension_template_directory - extension_web_directory - indicate_progress - install_string - include_languages - success - template_include_path - init_console + bin_loc + get_version_and_os + extension_code_files + extension_package_directory + extension_requirement_packages + extension_template_directory + extension_web_directory + indicate_progress + install_string + include_languages + success + template_include_path + init_console ); sub bin_loc { - my ($bin, $path) = @_; - # This module is not needed most of the time and is a bit slow, - # so we only load it when calling bin_loc(). - require ExtUtils::MM; - - # If the binary is a full path... - if ($bin =~ m{[/\\]}) { - return MM->maybe_command($bin) || ''; - } - - # Otherwise we look for it in the path in a cross-platform way. - my @path = $path ? @$path : File::Spec->path; - foreach my $dir (@path) { - next if !-d $dir; - my $full_path = File::Spec->catfile($dir, $bin); - # MM is an alias for ExtUtils::MM. maybe_command is nice - # because it checks .com, .bat, .exe (etc.) on Windows. - my $command = MM->maybe_command($full_path); - return $command if $command; - } - - return ''; + my ($bin, $path) = @_; + + # This module is not needed most of the time and is a bit slow, + # so we only load it when calling bin_loc(). + require ExtUtils::MM; + + # If the binary is a full path... + if ($bin =~ m{[/\\]}) { + return MM->maybe_command($bin) || ''; + } + + # Otherwise we look for it in the path in a cross-platform way. + my @path = $path ? @$path : File::Spec->path; + foreach my $dir (@path) { + next if !-d $dir; + my $full_path = File::Spec->catfile($dir, $bin); + + # MM is an alias for ExtUtils::MM. maybe_command is nice + # because it checks .com, .bat, .exe (etc.) on Windows. + my $command = MM->maybe_command($full_path); + return $command if $command; + } + + return ''; } sub get_version_and_os { - # Display version information - my @os_details = POSIX::uname; - # 0 is the name of the OS, 2 is the major version, - my $os_name = $os_details[0] . ' ' . $os_details[2]; - if (ON_WINDOWS) { - require Win32; - $os_name = Win32::GetOSName(); - } - # $os_details[3] is the minor version. - return { bz_ver => BUGZILLA_VERSION, - perl_ver => sprintf('%vd', $^V), - os_name => $os_name, - os_ver => $os_details[3] }; + + # Display version information + my @os_details = POSIX::uname; + + # 0 is the name of the OS, 2 is the major version, + my $os_name = $os_details[0] . ' ' . $os_details[2]; + if (ON_WINDOWS) { + require Win32; + $os_name = Win32::GetOSName(); + } + + # $os_details[3] is the minor version. + return { + bz_ver => BUGZILLA_VERSION, + perl_ver => sprintf('%vd', $^V), + os_name => $os_name, + os_ver => $os_details[3] + }; } sub _extension_paths { - my $dir = bz_locations()->{'extensionsdir'}; - my @extension_items = glob("$dir/*"); - my @paths; - foreach my $item (@extension_items) { - my $basename = basename($item); - # Skip CVS directories and any hidden files/dirs. - next if ($basename eq 'CVS' or $basename =~ /^\./); - if (-d $item) { - if (!-e "$item/disabled") { - push(@paths, $item); - } - } - elsif ($item =~ /\.pm$/i) { - push(@paths, $item); - } - } - return @paths; + my $dir = bz_locations()->{'extensionsdir'}; + my @extension_items = glob("$dir/*"); + my @paths; + foreach my $item (@extension_items) { + my $basename = basename($item); + + # Skip CVS directories and any hidden files/dirs. + next if ($basename eq 'CVS' or $basename =~ /^\./); + if (-d $item) { + if (!-e "$item/disabled") { + push(@paths, $item); + } + } + elsif ($item =~ /\.pm$/i) { + push(@paths, $item); + } + } + return @paths; } sub extension_code_files { - my ($requirements_only) = @_; - my @files; - foreach my $path (_extension_paths()) { - my @load_files; - if (-d $path) { - my $extension_file = "$path/Extension.pm"; - my $config_file = "$path/Config.pm"; - if (-e $extension_file) { - push(@load_files, $extension_file); - } - if (-e $config_file) { - push(@load_files, $config_file); - } - - # Don't load Extension.pm if we just want Config.pm and - # we found both. - if ($requirements_only and scalar(@load_files) == 2) { - shift(@load_files); - } - } - else { - push(@load_files, $path); - } - next if !scalar(@load_files); - # We know that these paths are safe, because they came from - # extensionsdir and we checked them specifically for their format. - # Also, the only thing we ever do with them is pass them to "require". - trick_taint($_) foreach @load_files; - push(@files, \@load_files); - } - - my @additional; - my $datadir = bz_locations()->{'datadir'}; - my $addl_file = "$datadir/extensions/additional"; - if (-e $addl_file) { - open(my $fh, '<', $addl_file) || die "$addl_file: $!"; - @additional = map { trim($_) } <$fh>; - close($fh); + my ($requirements_only) = @_; + my @files; + foreach my $path (_extension_paths()) { + my @load_files; + if (-d $path) { + my $extension_file = "$path/Extension.pm"; + my $config_file = "$path/Config.pm"; + if (-e $extension_file) { + push(@load_files, $extension_file); + } + if (-e $config_file) { + push(@load_files, $config_file); + } + + # Don't load Extension.pm if we just want Config.pm and + # we found both. + if ($requirements_only and scalar(@load_files) == 2) { + shift(@load_files); + } } - return (\@files, \@additional); + else { + push(@load_files, $path); + } + next if !scalar(@load_files); + + # We know that these paths are safe, because they came from + # extensionsdir and we checked them specifically for their format. + # Also, the only thing we ever do with them is pass them to "require". + trick_taint($_) foreach @load_files; + push(@files, \@load_files); + } + + my @additional; + my $datadir = bz_locations()->{'datadir'}; + my $addl_file = "$datadir/extensions/additional"; + if (-e $addl_file) { + open(my $fh, '<', $addl_file) || die "$addl_file: $!"; + @additional = map { trim($_) } <$fh>; + close($fh); + } + return (\@files, \@additional); } # Used by _get_extension_requirements in Bugzilla::Install::Requirements. sub extension_requirement_packages { - # If we're in a .cgi script or some time that's not the requirements phase, - # just use Bugzilla->extensions. This avoids running the below code during - # a normal Bugzilla page, which is important because the below code - # doesn't actually function right if it runs after - # Bugzilla::Extension->load_all (because stuff has already been loaded). - # (This matters because almost every page calls Bugzilla->feature, which - # calls OPTIONAL_MODULES, which calls this method.) - # - # We check if Bugzilla.pm is already loaded, instead of doing a "require", - # because we *do* want the code lower down to run during the Requirements - # phase of checksetup.pl, instead of Bugzilla->extensions, and Bugzilla.pm - # actually *can* be loaded during the Requirements phase if all the - # requirements have already been installed. - if ($INC{'Bugzilla.pm'}) { - return Bugzilla->extensions; - } - my $packages = _cache()->{extension_requirement_packages}; - return $packages if $packages; - $packages = []; - my %package_map; - - my ($file_sets, $extra_packages) = extension_code_files('requirements only'); - foreach my $file_set (@$file_sets) { - my $file = shift @$file_set; - my $name = require $file; - if ($name =~ /^\d+$/) { - die install_string('extension_must_return_name', - { file => $file, returned => $name }); - } - my $package = "Bugzilla::Extension::$name"; - if ($package->can('package_dir')) { - $package->package_dir($file); - } - else { - extension_package_directory($package, $file); - } - $package_map{$file} = $package; - push(@$packages, $package); - } - foreach my $package (@$extra_packages) { - eval("require $package") || die $@; - push(@$packages, $package); - } - _cache()->{extension_requirement_packages} = $packages; - # Used by Bugzilla::Extension->load if it's called after this method - # (which only happens during checksetup.pl, currently). - _cache()->{extension_requirement_package_map} = \%package_map; - return $packages; + # If we're in a .cgi script or some time that's not the requirements phase, + # just use Bugzilla->extensions. This avoids running the below code during + # a normal Bugzilla page, which is important because the below code + # doesn't actually function right if it runs after + # Bugzilla::Extension->load_all (because stuff has already been loaded). + # (This matters because almost every page calls Bugzilla->feature, which + # calls OPTIONAL_MODULES, which calls this method.) + # + # We check if Bugzilla.pm is already loaded, instead of doing a "require", + # because we *do* want the code lower down to run during the Requirements + # phase of checksetup.pl, instead of Bugzilla->extensions, and Bugzilla.pm + # actually *can* be loaded during the Requirements phase if all the + # requirements have already been installed. + if ($INC{'Bugzilla.pm'}) { + return Bugzilla->extensions; + } + my $packages = _cache()->{extension_requirement_packages}; + return $packages if $packages; + $packages = []; + my %package_map; + + my ($file_sets, $extra_packages) = extension_code_files('requirements only'); + foreach my $file_set (@$file_sets) { + my $file = shift @$file_set; + my $name = require $file; + if ($name =~ /^\d+$/) { + die install_string('extension_must_return_name', + {file => $file, returned => $name}); + } + my $package = "Bugzilla::Extension::$name"; + if ($package->can('package_dir')) { + $package->package_dir($file); + } + else { + extension_package_directory($package, $file); + } + $package_map{$file} = $package; + push(@$packages, $package); + } + foreach my $package (@$extra_packages) { + eval("require $package") || die $@; + push(@$packages, $package); + } + + _cache()->{extension_requirement_packages} = $packages; + + # Used by Bugzilla::Extension->load if it's called after this method + # (which only happens during checksetup.pl, currently). + _cache()->{extension_requirement_package_map} = \%package_map; + return $packages; } # Used in this file and in Bugzilla::Extension. sub extension_template_directory { - my $extension = shift; - my $class = ref($extension) || $extension; - my $base_dir = extension_package_directory($class); - if ($base_dir eq bz_locations->{'extensionsdir'}) { - return bz_locations->{'templatedir'}; - } - return "$base_dir/template"; + my $extension = shift; + my $class = ref($extension) || $extension; + my $base_dir = extension_package_directory($class); + if ($base_dir eq bz_locations->{'extensionsdir'}) { + return bz_locations->{'templatedir'}; + } + return "$base_dir/template"; } # Used in this file and in Bugzilla::Extension. sub extension_web_directory { - my $extension = shift; - my $class = ref($extension) || $extension; - my $base_dir = extension_package_directory($class); - return "$base_dir/web"; + my $extension = shift; + my $class = ref($extension) || $extension; + my $base_dir = extension_package_directory($class); + return "$base_dir/web"; } # For extensions that are in the extensions/ dir, this both sets and fetches @@ -223,263 +234,271 @@ sub extension_web_directory { # when determining the template directory for extensions (or other things # that are relative to the extension's base directory). sub extension_package_directory { - my ($invocant, $file) = @_; - my $class = ref($invocant) || $invocant; - - # $file is set on the first invocation, store the value in the extension's - # package for retrieval on subsequent calls - my $var; - { - no warnings 'once'; - no strict 'refs'; - $var = \${"${class}::EXTENSION_PACKAGE_DIR"}; - } - if ($file) { - $$var = dirname($file); - } - my $value = $$var; - - # This is for extensions loaded from data/extensions/additional. - if (!$value) { - my $short_path = $class; - $short_path =~ s/::/\//g; - $short_path .= ".pm"; - my $long_path = $INC{$short_path}; - die "$short_path is not in \%INC" if !$long_path; - $value = $long_path; - $value =~ s/\.pm//; - } - return $value; + my ($invocant, $file) = @_; + my $class = ref($invocant) || $invocant; + + # $file is set on the first invocation, store the value in the extension's + # package for retrieval on subsequent calls + my $var; + { + no warnings 'once'; + no strict 'refs'; + $var = \${"${class}::EXTENSION_PACKAGE_DIR"}; + } + if ($file) { + $$var = dirname($file); + } + my $value = $$var; + + # This is for extensions loaded from data/extensions/additional. + if (!$value) { + my $short_path = $class; + $short_path =~ s/::/\//g; + $short_path .= ".pm"; + my $long_path = $INC{$short_path}; + die "$short_path is not in \%INC" if !$long_path; + $value = $long_path; + $value =~ s/\.pm//; + } + return $value; } sub indicate_progress { - my ($params) = @_; - my $current = $params->{current}; - my $total = $params->{total}; - my $every = $params->{every} || 1; - - print "." if !($current % $every); - if ($current == $total || $current % ($every * 60) == 0) { - print "$current/$total (" . int($current * 100 / $total) . "%)\n"; - } + my ($params) = @_; + my $current = $params->{current}; + my $total = $params->{total}; + my $every = $params->{every} || 1; + + print "." if !($current % $every); + if ($current == $total || $current % ($every * 60) == 0) { + print "$current/$total (" . int($current * 100 / $total) . "%)\n"; + } } sub install_string { - my ($string_id, $vars) = @_; - _cache()->{install_string_path} ||= template_include_path(); - my $path = _cache()->{install_string_path}; - - my $string_template; - # Find the first template that defines this string. - foreach my $dir (@$path) { - my $base = "$dir/setup/strings"; - $string_template = _get_string_from_file($string_id, "$base.txt.pl") - if !defined $string_template; - last if defined $string_template; - } - - die "No language defines the string '$string_id'" - if !defined $string_template; - - utf8::decode($string_template) if !utf8::is_utf8($string_template); - - $vars ||= {}; - my @replace_keys = keys %$vars; - foreach my $key (@replace_keys) { - my $replacement = $vars->{$key}; - die "'$key' in '$string_id' is tainted: '$replacement'" - if tainted($replacement); - # We don't want people to start getting clever and inserting - # ##variable## into their values. So we check if any other - # key is listed in the *replacement* string, before doing - # the replacement. This is mostly to protect programmers from - # making mistakes. - if (grep($replacement =~ /##$key##/, @replace_keys)) { - die "Unsafe replacement for '$key' in '$string_id': '$replacement'"; - } - $string_template =~ s/\Q##$key##\E/$replacement/g; - } - - return $string_template; + my ($string_id, $vars) = @_; + _cache()->{install_string_path} ||= template_include_path(); + my $path = _cache()->{install_string_path}; + + my $string_template; + + # Find the first template that defines this string. + foreach my $dir (@$path) { + my $base = "$dir/setup/strings"; + $string_template = _get_string_from_file($string_id, "$base.txt.pl") + if !defined $string_template; + last if defined $string_template; + } + + die "No language defines the string '$string_id'" if !defined $string_template; + + utf8::decode($string_template) if !utf8::is_utf8($string_template); + + $vars ||= {}; + my @replace_keys = keys %$vars; + foreach my $key (@replace_keys) { + my $replacement = $vars->{$key}; + die "'$key' in '$string_id' is tainted: '$replacement'" + if tainted($replacement); + + # We don't want people to start getting clever and inserting + # ##variable## into their values. So we check if any other + # key is listed in the *replacement* string, before doing + # the replacement. This is mostly to protect programmers from + # making mistakes. + if (grep($replacement =~ /##$key##/, @replace_keys)) { + die "Unsafe replacement for '$key' in '$string_id': '$replacement'"; + } + $string_template =~ s/\Q##$key##\E/$replacement/g; + } + + return $string_template; } sub _wanted_languages { - my ($requested, @wanted); - - # Checking SERVER_SOFTWARE is the same as i_am_cgi() in Bugzilla::Util. - if (exists $ENV{'SERVER_SOFTWARE'}) { - my $cgi = eval { Bugzilla->cgi } || eval { require CGI; return CGI->new() }; - $requested = $cgi->http('Accept-Language') || ''; - my $lang = $cgi->cookie('LANG'); - push(@wanted, $lang) if $lang; - } - else { - $requested = get_console_locale(); - } - - push(@wanted, _sort_accept_language($requested)); - return \@wanted; + my ($requested, @wanted); + + # Checking SERVER_SOFTWARE is the same as i_am_cgi() in Bugzilla::Util. + if (exists $ENV{'SERVER_SOFTWARE'}) { + my $cgi = eval { Bugzilla->cgi } || eval { require CGI; return CGI->new() }; + $requested = $cgi->http('Accept-Language') || ''; + my $lang = $cgi->cookie('LANG'); + push(@wanted, $lang) if $lang; + } + else { + $requested = get_console_locale(); + } + + push(@wanted, _sort_accept_language($requested)); + return \@wanted; } sub _wanted_to_actual_languages { - my ($wanted, $supported) = @_; - - my @actual; - foreach my $lang (@$wanted) { - # If we support the language we want, or *any version* of - # the language we want, it gets pushed into @actual. - # - # Per RFC 1766 and RFC 2616, things like 'en' match 'en-us' and - # 'en-uk', but not the other way around. (This is unfortunately - # not very clearly stated in those RFC; see comment just over 14.5 - # in http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4) - my @found = grep(/^\Q$lang\E(-.+)?$/i, @$supported); - push(@actual, @found) if @found; - } + my ($wanted, $supported) = @_; - # We always include English at the bottom if it's not there, even if - # it wasn't selected by the user. - if (!grep($_ eq 'en', @actual)) { - push(@actual, 'en'); - } + my @actual; + foreach my $lang (@$wanted) { - return \@actual; + # If we support the language we want, or *any version* of + # the language we want, it gets pushed into @actual. + # + # Per RFC 1766 and RFC 2616, things like 'en' match 'en-us' and + # 'en-uk', but not the other way around. (This is unfortunately + # not very clearly stated in those RFC; see comment just over 14.5 + # in http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4) + my @found = grep(/^\Q$lang\E(-.+)?$/i, @$supported); + push(@actual, @found) if @found; + } + + # We always include English at the bottom if it's not there, even if + # it wasn't selected by the user. + if (!grep($_ eq 'en', @actual)) { + push(@actual, 'en'); + } + + return \@actual; } sub supported_languages { - my $cache = _cache(); - return $cache->{supported_languages} if $cache->{supported_languages}; - - my @dirs = glob(bz_locations()->{'templatedir'} . "/*"); - my @languages; - foreach my $dir (@dirs) { - # It's a language directory only if it contains "default" or - # "custom". This auto-excludes CVS directories as well. - next if (!-d "$dir/default" and !-d "$dir/custom"); - my $lang = basename($dir); - # Check for language tag format conforming to RFC 1766. - next unless $lang =~ /^[a-zA-Z]{1,8}(-[a-zA-Z]{1,8})?$/; - push(@languages, $lang); - } + my $cache = _cache(); + return $cache->{supported_languages} if $cache->{supported_languages}; + + my @dirs = glob(bz_locations()->{'templatedir'} . "/*"); + my @languages; + foreach my $dir (@dirs) { + + # It's a language directory only if it contains "default" or + # "custom". This auto-excludes CVS directories as well. + next if (!-d "$dir/default" and !-d "$dir/custom"); + my $lang = basename($dir); + + # Check for language tag format conforming to RFC 1766. + next unless $lang =~ /^[a-zA-Z]{1,8}(-[a-zA-Z]{1,8})?$/; + push(@languages, $lang); + } - $cache->{supported_languages} = \@languages; - return \@languages; + $cache->{supported_languages} = \@languages; + return \@languages; } sub include_languages { - my ($params) = @_; - - # Basically, the way this works is that we have a list of languages - # that we *want*, and a list of languages that Bugzilla actually - # supports. If there is only one language installed, we take it. - my $supported = supported_languages(); - return @$supported if @$supported == 1; - - my $wanted; - if ($params->{language}) { - # We can pass several languages at once as an arrayref - # or a single language. - $wanted = $params->{language}; - $wanted = [$wanted] unless ref $wanted; - } - else { - $wanted = _wanted_languages(); - } - my $actual = _wanted_to_actual_languages($wanted, $supported); - return @$actual; + my ($params) = @_; + + # Basically, the way this works is that we have a list of languages + # that we *want*, and a list of languages that Bugzilla actually + # supports. If there is only one language installed, we take it. + my $supported = supported_languages(); + return @$supported if @$supported == 1; + + my $wanted; + if ($params->{language}) { + + # We can pass several languages at once as an arrayref + # or a single language. + $wanted = $params->{language}; + $wanted = [$wanted] unless ref $wanted; + } + else { + $wanted = _wanted_languages(); + } + my $actual = _wanted_to_actual_languages($wanted, $supported); + return @$actual; } # Used by template_include_path sub _template_lang_directories { - my ($languages, $templatedir) = @_; - - my @add = qw(custom default); - my $project = bz_locations->{'project'}; - unshift(@add, $project) if $project; - - my @result; - foreach my $lang (@$languages) { - foreach my $dir (@add) { - my $full_dir = "$templatedir/$lang/$dir"; - if (-d $full_dir) { - trick_taint($full_dir); - push(@result, $full_dir); - } - } - } - return @result; + my ($languages, $templatedir) = @_; + + my @add = qw(custom default); + my $project = bz_locations->{'project'}; + unshift(@add, $project) if $project; + + my @result; + foreach my $lang (@$languages) { + foreach my $dir (@add) { + my $full_dir = "$templatedir/$lang/$dir"; + if (-d $full_dir) { + trick_taint($full_dir); + push(@result, $full_dir); + } + } + } + return @result; } # Used by template_include_path. sub _template_base_directories { - # First, we add extension template directories, because extension templates - # override standard templates. Extensions may be localized in the same way - # that Bugzilla templates are localized. - # - # We use extension_requirement_packages instead of Bugzilla->extensions - # because this fucntion is called during the requirements phase of - # installation (so Bugzilla->extensions isn't available). - my $extensions = extension_requirement_packages(); - my @template_dirs; - foreach my $extension (@$extensions) { - my $dir; - # If there's a template_dir method available in the extension - # package, then call it. Note that this has to be defined in - # Config.pm for extensions that have a Config.pm, to be effective - # during the Requirements phase of checksetup.pl. - if ($extension->can('template_dir')) { - $dir = $extension->template_dir; - } - else { - $dir = extension_template_directory($extension); - } - if (-d $dir) { - push(@template_dirs, $dir); - } + + # First, we add extension template directories, because extension templates + # override standard templates. Extensions may be localized in the same way + # that Bugzilla templates are localized. + # + # We use extension_requirement_packages instead of Bugzilla->extensions + # because this fucntion is called during the requirements phase of + # installation (so Bugzilla->extensions isn't available). + my $extensions = extension_requirement_packages(); + my @template_dirs; + foreach my $extension (@$extensions) { + my $dir; + + # If there's a template_dir method available in the extension + # package, then call it. Note that this has to be defined in + # Config.pm for extensions that have a Config.pm, to be effective + # during the Requirements phase of checksetup.pl. + if ($extension->can('template_dir')) { + $dir = $extension->template_dir; + } + else { + $dir = extension_template_directory($extension); } + if (-d $dir) { + push(@template_dirs, $dir); + } + } - # Extensions may also contain *only* templates, in which case they - # won't show up in extension_requirement_packages. - foreach my $path (_extension_paths()) { - next if !-d $path; - if (!-e "$path/Extension.pm" and !-e "$path/Config.pm" - and -d "$path/template") - { - push(@template_dirs, "$path/template"); - } + # Extensions may also contain *only* templates, in which case they + # won't show up in extension_requirement_packages. + foreach my $path (_extension_paths()) { + next if !-d $path; + if (!-e "$path/Extension.pm" and !-e "$path/Config.pm" and -d "$path/template") + { + push(@template_dirs, "$path/template"); } + } - push(@template_dirs, bz_locations()->{'templatedir'}); - return \@template_dirs; + push(@template_dirs, bz_locations()->{'templatedir'}); + return \@template_dirs; } sub template_include_path { - my ($params) = @_; - my @used_languages = include_languages($params); - # Now, we add template directories in the order they will be searched: - my $template_dirs = _template_base_directories(); - - my @include_path; - foreach my $template_dir (@$template_dirs) { - my @lang_dirs = _template_lang_directories(\@used_languages, - $template_dir); - # Hooks get each set of extension directories separately. - if ($params->{hook}) { - push(@include_path, \@lang_dirs); - } - # Whereas everything else just gets a whole INCLUDE_PATH. - else { - push(@include_path, @lang_dirs); - } + my ($params) = @_; + my @used_languages = include_languages($params); + + # Now, we add template directories in the order they will be searched: + my $template_dirs = _template_base_directories(); + + my @include_path; + foreach my $template_dir (@$template_dirs) { + my @lang_dirs = _template_lang_directories(\@used_languages, $template_dir); + + # Hooks get each set of extension directories separately. + if ($params->{hook}) { + push(@include_path, \@lang_dirs); + } + + # Whereas everything else just gets a whole INCLUDE_PATH. + else { + push(@include_path, @lang_dirs); } - return \@include_path; + } + return \@include_path; } sub no_checksetup_from_cgi { - print "Content-Type: text/html; charset=UTF-8\r\n\r\n"; - print install_string('no_checksetup_from_cgi'); - exit; + print "Content-Type: text/html; charset=UTF-8\r\n\r\n"; + print install_string('no_checksetup_from_cgi'); + exit; } ###################### @@ -488,172 +507,183 @@ sub no_checksetup_from_cgi { # Used by install_string sub _get_string_from_file { - my ($string_id, $file) = @_; - # If we already loaded the file, then use its copy from the cache. - if (my $strings = _cache()->{strings_from_file}->{$file}) { - return $strings->{$string_id}; - } - - # This module is only needed by checksetup.pl, - # so only load it when needed. - require Safe; - - return undef if !-e $file; - my $safe = new Safe; - $safe->rdo($file); - my %strings = %{$safe->varglob('strings')}; - _cache()->{strings_from_file}->{$file} = \%strings; - return $strings{$string_id}; + my ($string_id, $file) = @_; + + # If we already loaded the file, then use its copy from the cache. + if (my $strings = _cache()->{strings_from_file}->{$file}) { + return $strings->{$string_id}; + } + + # This module is only needed by checksetup.pl, + # so only load it when needed. + require Safe; + + return undef if !-e $file; + my $safe = new Safe; + $safe->rdo($file); + my %strings = %{$safe->varglob('strings')}; + _cache()->{strings_from_file}->{$file} = \%strings; + return $strings{$string_id}; } # Make an ordered list out of a HTTP Accept-Language header (see RFC 2616, 14.4) # We ignore '*' and ;q=0 # For languages with the same priority q the order remains unchanged. sub _sort_accept_language { - sub sortQvalue { $b->{'qvalue'} <=> $a->{'qvalue'} } - my $accept_language = $_[0]; - - # clean up string. - $accept_language =~ s/[^A-Za-z;q=0-9\.\-,]//g; - my @qlanguages; - my @languages; - foreach(split /,/, $accept_language) { - if (m/([A-Za-z\-]+)(?:;q=(\d(?:\.\d+)))?/) { - my $lang = $1; - my $qvalue = $2; - $qvalue = 1 if not defined $qvalue; - next if $qvalue == 0; - $qvalue = 1 if $qvalue > 1; - push(@qlanguages, {'qvalue' => $qvalue, 'language' => $lang}); - } - } - - return map($_->{'language'}, (sort sortQvalue @qlanguages)); + sub sortQvalue { $b->{'qvalue'} <=> $a->{'qvalue'} } + my $accept_language = $_[0]; + + # clean up string. + $accept_language =~ s/[^A-Za-z;q=0-9\.\-,]//g; + my @qlanguages; + my @languages; + foreach (split /,/, $accept_language) { + if (m/([A-Za-z\-]+)(?:;q=(\d(?:\.\d+)))?/) { + my $lang = $1; + my $qvalue = $2; + $qvalue = 1 if not defined $qvalue; + next if $qvalue == 0; + $qvalue = 1 if $qvalue > 1; + push(@qlanguages, {'qvalue' => $qvalue, 'language' => $lang}); + } + } + + return map($_->{'language'}, (sort sortQvalue @qlanguages)); } sub get_console_locale { - require Locale::Language; - my $locale = setlocale(LC_CTYPE); - my $language; - # Some distros set e.g. LC_CTYPE = fr_CH.UTF-8. We clean it up. - if ($locale =~ /^([^\.]+)/) { - $locale = $1; - } - $locale =~ s/_/-/; - # It's pretty sure that there is no language pack of the form fr-CH - # installed, so we also include fr as a wanted language. - if ($locale =~ /^(\S+)\-/) { - $language = $1; - $locale .= ",$language"; - } - else { - $language = $locale; - } - - # Some OSs or distributions may have setlocale return a string of the form - # German_Germany.1252 (this example taken from a Windows XP system), which - # is unsuitable for our needs because Bugzilla works on language codes. - # We try and convert them here. - if ($language = Locale::Language::language2code($language)) { - $locale .= ",$language"; - } - - return $locale; + require Locale::Language; + my $locale = setlocale(LC_CTYPE); + my $language; + + # Some distros set e.g. LC_CTYPE = fr_CH.UTF-8. We clean it up. + if ($locale =~ /^([^\.]+)/) { + $locale = $1; + } + $locale =~ s/_/-/; + + # It's pretty sure that there is no language pack of the form fr-CH + # installed, so we also include fr as a wanted language. + if ($locale =~ /^(\S+)\-/) { + $language = $1; + $locale .= ",$language"; + } + else { + $language = $locale; + } + + # Some OSs or distributions may have setlocale return a string of the form + # German_Germany.1252 (this example taken from a Windows XP system), which + # is unsuitable for our needs because Bugzilla works on language codes. + # We try and convert them here. + if ($language = Locale::Language::language2code($language)) { + $locale .= ",$language"; + } + + return $locale; } sub set_output_encoding { - # If we've already set an encoding layer on STDOUT, don't - # add another one. - my @stdout_layers = PerlIO::get_layers(STDOUT); - return if grep(/^encoding/, @stdout_layers); - - my $encoding; - if (ON_WINDOWS and eval { require Win32::Console }) { - # Although setlocale() works on Windows, it doesn't always return - # the current *console's* encoding. So we use OutputCP here instead, - # when we can. - $encoding = Win32::Console::OutputCP(); - } - else { - my $locale = setlocale(LC_CTYPE); - if ($locale =~ /\.([^\.]+)$/) { - $encoding = $1; - } - } - $encoding = "cp$encoding" if ON_WINDOWS; - $encoding = Encode::resolve_alias($encoding) if $encoding; - if ($encoding and $encoding !~ /utf-8/i) { - binmode STDOUT, ":encoding($encoding)"; - binmode STDERR, ":encoding($encoding)"; - } - else { - binmode STDOUT, ':utf8'; - binmode STDERR, ':utf8'; - } + # If we've already set an encoding layer on STDOUT, don't + # add another one. + my @stdout_layers = PerlIO::get_layers(STDOUT); + return if grep(/^encoding/, @stdout_layers); + + my $encoding; + if (ON_WINDOWS and eval { require Win32::Console }) { + + # Although setlocale() works on Windows, it doesn't always return + # the current *console's* encoding. So we use OutputCP here instead, + # when we can. + $encoding = Win32::Console::OutputCP(); + } + else { + my $locale = setlocale(LC_CTYPE); + if ($locale =~ /\.([^\.]+)$/) { + $encoding = $1; + } + } + $encoding = "cp$encoding" if ON_WINDOWS; + + $encoding = Encode::resolve_alias($encoding) if $encoding; + if ($encoding and $encoding !~ /utf-8/i) { + binmode STDOUT, ":encoding($encoding)"; + binmode STDERR, ":encoding($encoding)"; + } + else { + binmode STDOUT, ':utf8'; + binmode STDERR, ':utf8'; + } } sub init_console { - eval { ON_WINDOWS && require Win32::Console::ANSI; }; - $ENV{'ANSI_COLORS_DISABLED'} = 1 if ($@ || !-t *STDOUT); - $SIG{__DIE__} = \&_console_die; - prevent_windows_dialog_boxes(); - set_output_encoding(); + eval { ON_WINDOWS && require Win32::Console::ANSI; }; + $ENV{'ANSI_COLORS_DISABLED'} = 1 if ($@ || !-t *STDOUT); + $SIG{__DIE__} = \&_console_die; + prevent_windows_dialog_boxes(); + set_output_encoding(); } sub _console_die { - my ($message) = @_; - # $^S means "we are in an eval" - if ($^S) { - die $message; - } - # Remove newlines from the message before we color it, and then - # add them back in on display. Otherwise the ANSI escape code - # for resetting the color comes after the newline, and Perl thinks - # that it should put "at Bugzilla/Install.pm line 1234" after the - # message. - $message =~ s/\n+$//; - # We put quotes around the message to stringify any object exceptions, - # like Template::Exception. - die colored("$message", COLOR_ERROR) . "\n"; + my ($message) = @_; + + # $^S means "we are in an eval" + if ($^S) { + die $message; + } + + # Remove newlines from the message before we color it, and then + # add them back in on display. Otherwise the ANSI escape code + # for resetting the color comes after the newline, and Perl thinks + # that it should put "at Bugzilla/Install.pm line 1234" after the + # message. + $message =~ s/\n+$//; + + # We put quotes around the message to stringify any object exceptions, + # like Template::Exception. + die colored("$message", COLOR_ERROR) . "\n"; } sub success { - my ($message) = @_; - print colored($message, COLOR_SUCCESS), "\n"; + my ($message) = @_; + print colored($message, COLOR_SUCCESS), "\n"; } sub prevent_windows_dialog_boxes { - # This code comes from http://bugs.activestate.com/show_bug.cgi?id=82183 - # and prevents Perl modules from popping up dialog boxes, particularly - # during checksetup (since loading DBD::Oracle during checksetup when - # Oracle isn't installed causes a scary popup and pauses checksetup). - # - # Win32::API ships with ActiveState by default, though there could - # theoretically be a Windows installation without it, I suppose. - if (ON_WINDOWS and eval { require Win32::API }) { - # Call kernel32.SetErrorMode with arguments that mean: - # "The system does not display the critical-error-handler message box. - # Instead, the system sends the error to the calling process." and - # "A child process inherits the error mode of its parent process." - my $SetErrorMode = Win32::API->new('kernel32', 'SetErrorMode', - 'I', 'I'); - my $SEM_FAILCRITICALERRORS = 0x0001; - my $SEM_NOGPFAULTERRORBOX = 0x0002; - $SetErrorMode->Call($SEM_FAILCRITICALERRORS | $SEM_NOGPFAULTERRORBOX); - } + + # This code comes from http://bugs.activestate.com/show_bug.cgi?id=82183 + # and prevents Perl modules from popping up dialog boxes, particularly + # during checksetup (since loading DBD::Oracle during checksetup when + # Oracle isn't installed causes a scary popup and pauses checksetup). + # + # Win32::API ships with ActiveState by default, though there could + # theoretically be a Windows installation without it, I suppose. + if (ON_WINDOWS and eval { require Win32::API }) { + + # Call kernel32.SetErrorMode with arguments that mean: + # "The system does not display the critical-error-handler message box. + # Instead, the system sends the error to the calling process." and + # "A child process inherits the error mode of its parent process." + my $SetErrorMode = Win32::API->new('kernel32', 'SetErrorMode', 'I', 'I'); + my $SEM_FAILCRITICALERRORS = 0x0001; + my $SEM_NOGPFAULTERRORBOX = 0x0002; + $SetErrorMode->Call($SEM_FAILCRITICALERRORS | $SEM_NOGPFAULTERRORBOX); + } } # This is like request_cache, but it's used only by installation code # for checksetup.pl and things like that. our $_cache = {}; + sub _cache { - # If the normal request_cache is available (which happens any time - # after the requirements phase) then we should use that. - if (eval { Bugzilla->request_cache; }) { - return Bugzilla->request_cache; - } - return $_cache; + + # If the normal request_cache is available (which happens any time + # after the requirements phase) then we should use that. + if (eval { Bugzilla->request_cache; }) { + return Bugzilla->request_cache; + } + return $_cache; } ############################### @@ -661,20 +691,20 @@ sub _cache { ############################## sub trick_taint { - require Carp; - Carp::confess("Undef to trick_taint") unless defined $_[0]; - my $match = $_[0] =~ /^(.*)$/s; - $_[0] = $match ? $1 : undef; - return (defined($_[0])); + require Carp; + Carp::confess("Undef to trick_taint") unless defined $_[0]; + my $match = $_[0] =~ /^(.*)$/s; + $_[0] = $match ? $1 : undef; + return (defined($_[0])); } sub trim { - my ($str) = @_; - if ($str) { - $str =~ s/^\s+//g; - $str =~ s/\s+$//g; - } - return $str; + my ($str) = @_; + if ($str) { + $str =~ s/^\s+//g; + $str =~ s/\s+$//g; + } + return $str; } __END__ diff --git a/Bugzilla/Job/BugMail.pm b/Bugzilla/Job/BugMail.pm index e0b7f5448..a6deb5777 100644 --- a/Bugzilla/Job/BugMail.pm +++ b/Bugzilla/Job/BugMail.pm @@ -15,18 +15,18 @@ use Bugzilla::BugMail; BEGIN { eval "use parent qw(Bugzilla::Job::Mailer)"; } sub work { - my ($class, $job) = @_; - my $success = eval { - Bugzilla::BugMail::dequeue($job->arg->{vars}); - 1; - }; - if (!$success) { - $job->failed($@); - undef $@; - } - else { - $job->completed; - } + my ($class, $job) = @_; + my $success = eval { + Bugzilla::BugMail::dequeue($job->arg->{vars}); + 1; + }; + if (!$success) { + $job->failed($@); + undef $@; + } + else { + $job->completed; + } } 1; diff --git a/Bugzilla/Job/Mailer.pm b/Bugzilla/Job/Mailer.pm index cd1c23445..4a32f0d05 100644 --- a/Bugzilla/Job/Mailer.pm +++ b/Bugzilla/Job/Mailer.pm @@ -16,31 +16,33 @@ BEGIN { eval "use parent qw(TheSchwartz::Worker)"; } # The longest we expect a job to possibly take, in seconds. use constant grab_for => 300; + # We don't want email to fail permanently very easily. Retry for 30 days. use constant max_retries => 725; # The first few retries happen quickly, but after that we wait an hour for # each retry. sub retry_delay { - my ($class, $num_retries) = @_; - if ($num_retries < 5) { - return (10, 30, 60, 300, 600)[$num_retries]; - } - # One hour - return 60*60; + my ($class, $num_retries) = @_; + if ($num_retries < 5) { + return (10, 30, 60, 300, 600)[$num_retries]; + } + + # One hour + return 60 * 60; } sub work { - my ($class, $job) = @_; - my $msg = $job->arg->{msg}; - my $success = eval { MessageToMTA($msg, 1); 1; }; - if (!$success) { - $job->failed($@); - undef $@; - } - else { - $job->completed; - } + my ($class, $job) = @_; + my $msg = $job->arg->{msg}; + my $success = eval { MessageToMTA($msg, 1); 1; }; + if (!$success) { + $job->failed($@); + undef $@; + } + else { + $job->completed; + } } 1; diff --git a/Bugzilla/JobQueue.pm b/Bugzilla/JobQueue.pm index 6ff85d84f..e48182007 100644 --- a/Bugzilla/JobQueue.pm +++ b/Bugzilla/JobQueue.pm @@ -21,153 +21,155 @@ use fields qw(_worker_pidfile); # This maps job names for Bugzilla::JobQueue to the appropriate modules. # If you add new types of jobs, you should add a mapping here. -use constant JOB_MAP => { - send_mail => 'Bugzilla::Job::Mailer', - bug_mail => 'Bugzilla::Job::BugMail', -}; +use constant JOB_MAP => + {send_mail => 'Bugzilla::Job::Mailer', bug_mail => 'Bugzilla::Job::BugMail',}; # Without a driver cache TheSchwartz opens a new database connection # for each email it sends. This cached connection doesn't persist # across requests. -use constant DRIVER_CACHE_TIME => 300; # 5 minutes +use constant DRIVER_CACHE_TIME => 300; # 5 minutes # To avoid memory leak/fragmentation, a worker process won't process more than # MAX_MESSAGES messages. use constant MAX_MESSAGES => 1000; sub job_map { - if (!defined(Bugzilla->request_cache->{job_map})) { - my $job_map = JOB_MAP; - Bugzilla::Hook::process('job_map', { job_map => $job_map }); - Bugzilla->request_cache->{job_map} = $job_map; - } - - return Bugzilla->request_cache->{job_map}; + if (!defined(Bugzilla->request_cache->{job_map})) { + my $job_map = JOB_MAP; + Bugzilla::Hook::process('job_map', {job_map => $job_map}); + Bugzilla->request_cache->{job_map} = $job_map; + } + + return Bugzilla->request_cache->{job_map}; } sub new { - my $class = shift; - - if (!Bugzilla->feature('jobqueue')) { - ThrowUserError('feature_disabled', { feature => 'jobqueue' }); - } - - my $lc = Bugzilla->localconfig; - # We need to use the main DB as TheSchwartz module is going - # to write to it. - my $self = $class->SUPER::new( - databases => [{ - dsn => Bugzilla->dbh_main->{private_bz_dsn}, - user => $lc->{db_user}, - pass => $lc->{db_pass}, - prefix => 'ts_', - }], - driver_cache_expiration => DRIVER_CACHE_TIME, - prioritize => 1, - ); - - return $self; + my $class = shift; + + if (!Bugzilla->feature('jobqueue')) { + ThrowUserError('feature_disabled', {feature => 'jobqueue'}); + } + + my $lc = Bugzilla->localconfig; + + # We need to use the main DB as TheSchwartz module is going + # to write to it. + my $self = $class->SUPER::new( + databases => [{ + dsn => Bugzilla->dbh_main->{private_bz_dsn}, + user => $lc->{db_user}, + pass => $lc->{db_pass}, + prefix => 'ts_', + }], + driver_cache_expiration => DRIVER_CACHE_TIME, + prioritize => 1, + ); + + return $self; } # A way to get access to the underlying databases directly. sub bz_databases { - my $self = shift; - my @hashes = keys %{ $self->{databases} }; - return map { $self->driver_for($_) } @hashes; + my $self = shift; + my @hashes = keys %{$self->{databases}}; + return map { $self->driver_for($_) } @hashes; } # inserts a job into the queue to be processed and returns immediately sub insert { - my $self = shift; - my $job = shift; - - if (!ref($job)) { - my $mapped_job = Bugzilla::JobQueue->job_map()->{$job}; - ThrowCodeError('jobqueue_no_job_mapping', { job => $job }) - if !$mapped_job; - - $job = new TheSchwartz::Job( - funcname => $mapped_job, - arg => $_[0], - priority => $_[1] || 5 - ); - } - - my $retval = $self->SUPER::insert($job); - # XXX Need to get an error message here if insert fails, but - # I don't see any way to do that in TheSchwartz. - ThrowCodeError('jobqueue_insert_failed', { job => $job, errmsg => $@ }) - if !$retval; - - return $retval; + my $self = shift; + my $job = shift; + + if (!ref($job)) { + my $mapped_job = Bugzilla::JobQueue->job_map()->{$job}; + ThrowCodeError('jobqueue_no_job_mapping', {job => $job}) if !$mapped_job; + + $job = new TheSchwartz::Job( + funcname => $mapped_job, + arg => $_[0], + priority => $_[1] || 5 + ); + } + + my $retval = $self->SUPER::insert($job); + + # XXX Need to get an error message here if insert fails, but + # I don't see any way to do that in TheSchwartz. + ThrowCodeError('jobqueue_insert_failed', {job => $job, errmsg => $@}) + if !$retval; + + return $retval; } # To avoid memory leaks/fragmentation which tends to happen for long running # perl processes; check for jobs, and spawn a new process to empty the queue. sub subprocess_worker { - my $self = shift; - - my $command = "$0 -d -p '" . $self->{_worker_pidfile} . "' onepass"; - - while (1) { - my $time = (time); - my @jobs = $self->list_jobs({ - funcname => $self->{all_abilities}, - run_after => $time, - grabbed_until => $time, - limit => 1, - }); - if (@jobs) { - $self->debug("Spawning queue worker process"); - # Run the worker as a daemon - system $command; - # 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_text($self->{_worker_pidfile}, err_mode => 'quiet'); - if ($pid) { - sleep(3) while(kill(0, $pid)); - } - $self->debug("Queue worker process completed"); - } else { - $self->debug("No jobs found"); - } - sleep(5); + my $self = shift; + + my $command = "$0 -d -p '" . $self->{_worker_pidfile} . "' onepass"; + + while (1) { + my $time = (time); + my @jobs = $self->list_jobs({ + funcname => $self->{all_abilities}, + run_after => $time, + grabbed_until => $time, + limit => 1, + }); + if (@jobs) { + $self->debug("Spawning queue worker process"); + + # Run the worker as a daemon + system $command; + + # 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_text($self->{_worker_pidfile}, err_mode => 'quiet'); + if ($pid) { + sleep(3) while (kill(0, $pid)); + } + $self->debug("Queue worker process completed"); } + else { + $self->debug("No jobs found"); + } + sleep(5); + } } sub kill_worker { - my $self = Bugzilla->job_queue(); - if ($self->{_worker_pidfile} && -e $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"; - } + my $self = Bugzilla->job_queue(); + if ($self->{_worker_pidfile} && -e $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"; } + } } sub set_pidfile { - my ($self, $pidfile) = @_; - $self->{_worker_pidfile} = bz_locations->{'datadir'} . - '/worker-' . basename($pidfile); + my ($self, $pidfile) = @_; + $self->{_worker_pidfile} + = bz_locations->{'datadir'} . '/worker-' . basename($pidfile); } # Clear the request cache at the start of each run. sub work_once { - my $self = shift; - Bugzilla->clear_request_cache(); - return $self->SUPER::work_once(@_); + my $self = shift; + Bugzilla->clear_request_cache(); + return $self->SUPER::work_once(@_); } # Never process more than MAX_MESSAGES in one batch, to avoid memory # leak/fragmentation issues. sub work_until_done { - my $self = shift; - my $count = 0; - while ($count++ < MAX_MESSAGES) { - $self->work_once or last; - } + my $self = shift; + my $count = 0; + while ($count++ < MAX_MESSAGES) { + $self->work_once or last; + } } 1; diff --git a/Bugzilla/JobQueue/Runner.pm b/Bugzilla/JobQueue/Runner.pm index 104a97b0b..a1803be6e 100644 --- a/Bugzilla/JobQueue/Runner.pm +++ b/Bugzilla/JobQueue/Runner.pm @@ -28,8 +28,8 @@ BEGIN { eval "use parent qw(Daemon::Generic)"; } our $VERSION = BUGZILLA_VERSION; # Info we need to install/uninstall the daemon. -our $chkconfig = "/sbin/chkconfig"; -our $initd = "/etc/init.d"; +our $chkconfig = "/sbin/chkconfig"; +our $initd = "/etc/init.d"; our $initscript = "bugzilla-queue"; # The Daemon::Generic docs say that it uses all sorts of @@ -37,187 +37,188 @@ our $initscript = "bugzilla-queue"; # only thing it uses from gd_preconfig is the "pidfile" # config parameter. sub gd_preconfig { - my $self = shift; - - $self->{_run_command} = 'subprocess_worker'; - my $pidfile = $self->{gd_args}{pidfile}; - if (!$pidfile) { - $pidfile = bz_locations()->{datadir} . '/' . $self->{gd_progname} - . ".pid"; - } - return (pidfile => $pidfile); + my $self = shift; + + $self->{_run_command} = 'subprocess_worker'; + my $pidfile = $self->{gd_args}{pidfile}; + if (!$pidfile) { + $pidfile = bz_locations()->{datadir} . '/' . $self->{gd_progname} . ".pid"; + } + return (pidfile => $pidfile); } # All config other than the pidfile has to be done in gd_getopt # in order for it to be set up early enough. sub gd_getopt { - my $self = shift; + my $self = shift; - $self->SUPER::gd_getopt(); + $self->SUPER::gd_getopt(); - if ($self->{gd_args}{progname}) { - $self->{gd_progname} = $self->{gd_args}{progname}; - } - else { - $self->{gd_progname} = basename($0); - } + if ($self->{gd_args}{progname}) { + $self->{gd_progname} = $self->{gd_args}{progname}; + } + else { + $self->{gd_progname} = basename($0); + } - # There are places that Daemon Generic's new() uses $0 instead of - # gd_progname, which it really shouldn't, but this hack fixes it. - $self->{_original_zero} = $0; - $0 = $self->{gd_progname}; + # There are places that Daemon Generic's new() uses $0 instead of + # gd_progname, which it really shouldn't, but this hack fixes it. + $self->{_original_zero} = $0; + $0 = $self->{gd_progname}; } sub gd_postconfig { - my $self = shift; - # See the hack above in gd_getopt. This just reverses it - # in case anything else needs the accurate $0. - $0 = delete $self->{_original_zero}; + my $self = shift; + + # See the hack above in gd_getopt. This just reverses it + # in case anything else needs the accurate $0. + $0 = delete $self->{_original_zero}; } sub gd_more_opt { - my $self = shift; - return ( - 'pidfile=s' => \$self->{gd_args}{pidfile}, - 'n=s' => \$self->{gd_args}{progname}, - ); + my $self = shift; + return ( + 'pidfile=s' => \$self->{gd_args}{pidfile}, + 'n=s' => \$self->{gd_args}{progname}, + ); } sub gd_usage { - pod2usage({ -verbose => 0, -exitval => 'NOEXIT' }); - return 0 + pod2usage({-verbose => 0, -exitval => 'NOEXIT'}); + return 0; } sub gd_can_install { - my $self = shift; + my $self = shift; + + my $source_file; + if (-e "/etc/SuSE-release") { + $source_file = "contrib/$initscript.suse"; + } + else { + $source_file = "contrib/$initscript.rhel"; + } + my $dest_file = "$initd/$initscript"; + my $sysconfig = '/etc/sysconfig'; + my $config_file = "$sysconfig/$initscript"; + + if (!-x $chkconfig or !-d $initd) { + return $self->SUPER::gd_can_install(@_); + } - my $source_file; - if ( -e "/etc/SuSE-release" ) { - $source_file = "contrib/$initscript.suse"; - } else { - $source_file = "contrib/$initscript.rhel"; + return sub { + if (!-w $initd) { + print "You must run the 'install' command as root.\n"; + return; } - my $dest_file = "$initd/$initscript"; - my $sysconfig = '/etc/sysconfig'; - my $config_file = "$sysconfig/$initscript"; - - if (!-x $chkconfig or !-d $initd) { - return $self->SUPER::gd_can_install(@_); + if (-e $dest_file) { + print "$initscript already in $initd.\n"; + } + else { + copy($source_file, $dest_file) + or die "Could not copy $source_file to $dest_file: $!"; + chmod(0755, $dest_file) or die "Could not change permissions on $dest_file: $!"; } - return sub { - if (!-w $initd) { - print "You must run the 'install' command as root.\n"; - return; - } - if (-e $dest_file) { - print "$initscript already in $initd.\n"; - } - else { - copy($source_file, $dest_file) - or die "Could not copy $source_file to $dest_file: $!"; - chmod(0755, $dest_file) - or die "Could not change permissions on $dest_file: $!"; - } - - system($chkconfig, '--add', $initscript); - print "$initscript installed.", - " To start the daemon, do \"$dest_file start\" as root.\n"; - - if (-d $sysconfig and -w $sysconfig) { - if (-e $config_file) { - print "$config_file already exists.\n"; - return; - } - - open(my $config_fh, ">", $config_file) - or die "Could not write to $config_file: $!"; - my $directory = abs_path(dirname($self->{_original_zero})); - my $owner_id = (stat $self->{_original_zero})[4]; - my $owner = getpwuid($owner_id); - print $config_fh <", $config_file) + or die "Could not write to $config_file: $!"; + my $directory = abs_path(dirname($self->{_original_zero})); + my $owner_id = (stat $self->{_original_zero})[4]; + my $owner = getpwuid($owner_id); + print $config_fh <SUPER::gd_can_install(@_); + if (-x $chkconfig and -d $initd) { + return sub { + if (!-e "$initd/$initscript") { + print "$initscript not installed.\n"; + return; + } + system($chkconfig, '--del', $initscript); + print "$initscript disabled.", " To stop it, run: $initd/$initscript stop\n"; + } + } + + return $self->SUPER::gd_can_install(@_); } sub gd_check { - my $self = shift; - - # Get a count of all the jobs currently in the queue. - my $jq = Bugzilla->job_queue(); - my @dbs = $jq->bz_databases(); - my $count = 0; - foreach my $driver (@dbs) { - $count += $driver->select_one('SELECT COUNT(*) FROM ts_job', []); - } - print get_text('job_queue_depth', { count => $count }) . "\n"; + my $self = shift; + + # Get a count of all the jobs currently in the queue. + my $jq = Bugzilla->job_queue(); + my @dbs = $jq->bz_databases(); + my $count = 0; + foreach my $driver (@dbs) { + $count += $driver->select_one('SELECT COUNT(*) FROM ts_job', []); + } + print get_text('job_queue_depth', {count => $count}) . "\n"; } sub gd_setup_signals { - my $self = shift; - $self->SUPER::gd_setup_signals(); - $SIG{TERM} = sub { $self->gd_quit_event(); } + my $self = shift; + $self->SUPER::gd_setup_signals(); + $SIG{TERM} = sub { $self->gd_quit_event(); } } sub gd_quit_event { - Bugzilla->job_queue->kill_worker(); - exit(1); + Bugzilla->job_queue->kill_worker(); + exit(1); } sub gd_other_cmd { - my ($self, $do, $locked) = @_; - if ($do eq "once") { - $self->{_run_command} = 'work_once'; - } elsif ($do eq "onepass") { - $self->{_run_command} = 'work_until_done'; - } else { - $self->SUPER::gd_other_cmd($do, $locked); - } + my ($self, $do, $locked) = @_; + if ($do eq "once") { + $self->{_run_command} = 'work_once'; + } + elsif ($do eq "onepass") { + $self->{_run_command} = 'work_until_done'; + } + else { + $self->SUPER::gd_other_cmd($do, $locked); + } } sub gd_run { - my $self = shift; - $self->_do_work($self->{_run_command}); + my $self = shift; + $self->_do_work($self->{_run_command}); } sub _do_work { - my ($self, $fn) = @_; - - my $jq = Bugzilla->job_queue(); - $jq->set_verbose($self->{debug}); - $jq->set_pidfile($self->{gd_pidfile}); - foreach my $module (values %{ Bugzilla::JobQueue->job_map() }) { - eval "use $module"; - $jq->can_do($module); - } - $jq->$fn; + my ($self, $fn) = @_; + + my $jq = Bugzilla->job_queue(); + $jq->set_verbose($self->{debug}); + $jq->set_pidfile($self->{gd_pidfile}); + foreach my $module (values %{Bugzilla::JobQueue->job_map()}) { + eval "use $module"; + $jq->can_do($module); + } + $jq->$fn; } 1; diff --git a/Bugzilla/Keyword.pm b/Bugzilla/Keyword.pm index afa93e1e9..f1cb6cadf 100644 --- a/Bugzilla/Keyword.pm +++ b/Bugzilla/Keyword.pm @@ -23,44 +23,42 @@ use Bugzilla::Util; use constant IS_CONFIG => 1; use constant DB_COLUMNS => qw( - keyworddefs.id - keyworddefs.name - keyworddefs.description + keyworddefs.id + keyworddefs.name + keyworddefs.description ); use constant DB_TABLE => 'keyworddefs'; -use constant VALIDATORS => { - name => \&_check_name, - description => \&_check_description, -}; +use constant VALIDATORS => + {name => \&_check_name, description => \&_check_description,}; use constant UPDATE_COLUMNS => qw( - name - description + name + description ); ############################### #### Accessors ###### ############################### -sub description { return $_[0]->{'description'}; } +sub description { return $_[0]->{'description'}; } sub bug_count { - my ($self) = @_; - return $self->{'bug_count'} if defined $self->{'bug_count'}; - ($self->{'bug_count'}) = - Bugzilla->dbh->selectrow_array( - 'SELECT COUNT(*) FROM keywords WHERE keywordid = ?', - undef, $self->id); - return $self->{'bug_count'}; + my ($self) = @_; + return $self->{'bug_count'} if defined $self->{'bug_count'}; + ($self->{'bug_count'}) + = Bugzilla->dbh->selectrow_array( + 'SELECT COUNT(*) FROM keywords WHERE keywordid = ?', + undef, $self->id); + return $self->{'bug_count'}; } ############################### #### Mutators ##### ############################### -sub set_name { $_[0]->set('name', $_[1]); } +sub set_name { $_[0]->set('name', $_[1]); } sub set_description { $_[0]->set('description', $_[1]); } ############################### @@ -68,27 +66,29 @@ sub set_description { $_[0]->set('description', $_[1]); } ############################### sub get_all_with_bug_count { - my $class = shift; - my $dbh = Bugzilla->dbh; - my $keywords = - $dbh->selectall_arrayref('SELECT ' - . join(', ', $class->_get_db_columns) . ', + my $class = shift; + my $dbh = Bugzilla->dbh; + my $keywords = $dbh->selectall_arrayref( + 'SELECT ' . join(', ', $class->_get_db_columns) . ', COUNT(keywords.bug_id) AS bug_count FROM keyworddefs LEFT JOIN keywords - ON keyworddefs.id = keywords.keywordid ' . - $dbh->sql_group_by('keyworddefs.id', - 'keyworddefs.name, - keyworddefs.description') . ' - ORDER BY keyworddefs.name', {'Slice' => {}}); - if (!$keywords) { - return []; - } - - foreach my $keyword (@$keywords) { - bless($keyword, $class); - } - return $keywords; + ON keyworddefs.id = keywords.keywordid ' + . $dbh->sql_group_by( + 'keyworddefs.id', 'keyworddefs.name, + keyworddefs.description' + ) . ' + ORDER BY keyworddefs.name', + {'Slice' => {}} + ); + if (!$keywords) { + return []; + } + + foreach my $keyword (@$keywords) { + bless($keyword, $class); + } + return $keywords; } ############################### @@ -96,33 +96,33 @@ sub get_all_with_bug_count { ############################### sub _check_name { - my ($self, $name) = @_; - - $name = trim($name); - if (!defined $name or $name eq "") { - ThrowUserError("keyword_blank_name"); - } - if ($name =~ /[\s,]/) { - ThrowUserError("keyword_invalid_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) || lc($self->name) ne lc($name)) { - my $keyword = new Bugzilla::Keyword({ name => $name }); - ThrowUserError("keyword_already_exists", { name => $name }) if $keyword; - } - - return $name; + my ($self, $name) = @_; + + $name = trim($name); + if (!defined $name or $name eq "") { + ThrowUserError("keyword_blank_name"); + } + if ($name =~ /[\s,]/) { + ThrowUserError("keyword_invalid_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) || lc($self->name) ne lc($name)) { + my $keyword = new Bugzilla::Keyword({name => $name}); + ThrowUserError("keyword_already_exists", {name => $name}) if $keyword; + } + + return $name; } sub _check_description { - my ($self, $desc) = @_; - $desc = trim($desc); - if (!defined $desc or $desc eq '') { - ThrowUserError("keyword_blank_description"); - } - return $desc; + my ($self, $desc) = @_; + $desc = trim($desc); + if (!defined $desc or $desc eq '') { + ThrowUserError("keyword_blank_description"); + } + return $desc; } 1; diff --git a/Bugzilla/MIME.pm b/Bugzilla/MIME.pm index 8c6c141bb..660799e66 100644 --- a/Bugzilla/MIME.pm +++ b/Bugzilla/MIME.pm @@ -14,91 +14,91 @@ use warnings; use parent qw(Email::MIME); sub new { - my ($class, $msg) = @_; - state $use_utf8 = Bugzilla->params->{'utf8'}; - - # Template-Toolkit trims trailing newlines, which is problematic when - # parsing headers. - $msg =~ s/\n*$/\n/; - - # Because the encoding headers are not present in our email templates, we - # need to treat them as binary UTF-8 when parsing. - my ($in_header, $has_type, $has_encoding, $has_body) = (1); - foreach my $line (split(/\n/, $msg)) { - if ($line eq '') { - $in_header = 0; - next; - } - if (!$in_header) { - $has_body = 1; - last; - } - $has_type = 1 if $line =~ /^Content-Type:/i; - $has_encoding = 1 if $line =~ /^Content-Transfer-Encoding:/i; + my ($class, $msg) = @_; + state $use_utf8 = Bugzilla->params->{'utf8'}; + + # Template-Toolkit trims trailing newlines, which is problematic when + # parsing headers. + $msg =~ s/\n*$/\n/; + + # Because the encoding headers are not present in our email templates, we + # need to treat them as binary UTF-8 when parsing. + my ($in_header, $has_type, $has_encoding, $has_body) = (1); + foreach my $line (split(/\n/, $msg)) { + if ($line eq '') { + $in_header = 0; + next; } - if ($has_body) { - if (!$has_type && $use_utf8) { - $msg = qq#Content-Type: text/plain; charset="UTF-8"\n# . $msg; - } - if (!$has_encoding) { - $msg = qq#Content-Transfer-Encoding: binary\n# . $msg; - } + if (!$in_header) { + $has_body = 1; + last; } - if ($use_utf8 && utf8::is_utf8($msg)) { - utf8::encode($msg); + $has_type = 1 if $line =~ /^Content-Type:/i; + $has_encoding = 1 if $line =~ /^Content-Transfer-Encoding:/i; + } + if ($has_body) { + if (!$has_type && $use_utf8) { + $msg = qq#Content-Type: text/plain; charset="UTF-8"\n# . $msg; } - - # RFC 2822 requires us to have CRLF for our line endings and - # Email::MIME doesn't do this for us. We use \015 (CR) and \012 (LF) - # directly because Perl translates "\n" depending on what platform - # you're running on. See http://perldoc.perl.org/perlport.html#Newlines - $msg =~ s/(?:\015+)?\012/\015\012/msg; - - return $class->SUPER::new($msg); + if (!$has_encoding) { + $msg = qq#Content-Transfer-Encoding: binary\n# . $msg; + } + } + if ($use_utf8 && utf8::is_utf8($msg)) { + utf8::encode($msg); + } + + # RFC 2822 requires us to have CRLF for our line endings and + # Email::MIME doesn't do this for us. We use \015 (CR) and \012 (LF) + # directly because Perl translates "\n" depending on what platform + # you're running on. See http://perldoc.perl.org/perlport.html#Newlines + $msg =~ s/(?:\015+)?\012/\015\012/msg; + + return $class->SUPER::new($msg); } sub as_string { - my $self = shift; - state $use_utf8 = Bugzilla->params->{'utf8'}; - - # We add this header to uniquely identify all email that we - # send as coming from this Bugzilla installation. - # - # We don't use correct_urlbase, because we want this URL to - # *always* be the same for this Bugzilla, in every email, - # even if the admin changes the "ssl_redirect" parameter some day. - $self->header_set('X-Bugzilla-URL', Bugzilla->params->{'urlbase'}); - - # We add this header to mark the mail as "auto-generated" and - # thus to hopefully avoid auto replies. - $self->header_set('Auto-Submitted', 'auto-generated'); - - # MIME-Version must be set otherwise some mailsystems ignore the charset - $self->header_set('MIME-Version', '1.0') if !$self->header('MIME-Version'); - - # Encode the headers correctly. - foreach my $header ($self->header_names) { - my @values = $self->header($header); - map { utf8::decode($_) if defined($_) && !utf8::is_utf8($_) } @values; - - $self->header_str_set($header, @values); - } - - # Ensure the character-set and encoding is set correctly on single part - # emails. Multipart emails should have these already set when the parts - # are assembled. - if (scalar($self->parts) == 1) { - $self->charset_set('UTF-8') if $use_utf8; - $self->encoding_set('quoted-printable'); - } - - # Ensure we always return the encoded string - my $value = $self->SUPER::as_string(); - if ($use_utf8 && utf8::is_utf8($value)) { - utf8::encode($value); - } - - return $value; + my $self = shift; + state $use_utf8 = Bugzilla->params->{'utf8'}; + + # We add this header to uniquely identify all email that we + # send as coming from this Bugzilla installation. + # + # We don't use correct_urlbase, because we want this URL to + # *always* be the same for this Bugzilla, in every email, + # even if the admin changes the "ssl_redirect" parameter some day. + $self->header_set('X-Bugzilla-URL', Bugzilla->params->{'urlbase'}); + + # We add this header to mark the mail as "auto-generated" and + # thus to hopefully avoid auto replies. + $self->header_set('Auto-Submitted', 'auto-generated'); + + # MIME-Version must be set otherwise some mailsystems ignore the charset + $self->header_set('MIME-Version', '1.0') if !$self->header('MIME-Version'); + + # Encode the headers correctly. + foreach my $header ($self->header_names) { + my @values = $self->header($header); + map { utf8::decode($_) if defined($_) && !utf8::is_utf8($_) } @values; + + $self->header_str_set($header, @values); + } + + # Ensure the character-set and encoding is set correctly on single part + # emails. Multipart emails should have these already set when the parts + # are assembled. + if (scalar($self->parts) == 1) { + $self->charset_set('UTF-8') if $use_utf8; + $self->encoding_set('quoted-printable'); + } + + # Ensure we always return the encoded string + my $value = $self->SUPER::as_string(); + if ($use_utf8 && utf8::is_utf8($value)) { + utf8::encode($value); + } + + return $value; } 1; diff --git a/Bugzilla/Mailer.pm b/Bugzilla/Mailer.pm index 5ccf2d1ed..a5f79b9bc 100644 --- a/Bugzilla/Mailer.pm +++ b/Bugzilla/Mailer.pm @@ -28,199 +28,209 @@ 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, - ) - ); - 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, - ); - } - - 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; + 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, + )); + 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, + ); + } + + 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'}; - return if $method eq 'None'; - - if (Bugzilla->params->{'use_mailer_queue'} - && ! $send_now - && ! Bugzilla->dbh->bz_in_transaction() - ) { - Bugzilla->job_queue->insert('send_mail', { msg => $msg }); - return; - } - - my $dbh = Bugzilla->dbh; - - my $email = ref($msg) ? $msg : Bugzilla::MIME->new($msg); - - # If we're called from within a transaction, we don't want to send the - # email immediately, in case the transaction is rolled back. Instead we - # insert it into the mail_staging table, and bz_commit_transaction calls - # send_staged_mail() after the transaction is committed. - if (! $send_now && $dbh->bz_in_transaction()) { - # The e-mail string may contain tainted values. - my $string = $email->as_string; - trick_taint($string); - - my $sth = $dbh->prepare("INSERT INTO mail_staging (message) VALUES (?)"); - $sth->bind_param(1, $string, $dbh->BLOB_TYPE); - $sth->execute; - return; - } - - my $from = $email->header('From'); - - my $hostname; - my $transport; - if ($method eq "Sendmail") { - if (ON_WINDOWS) { - $transport = Bugzilla::Sender::Transport::Sendmail->new({ sendmail => SENDMAIL_EXE }); - } - else { - $transport = Bugzilla::Sender::Transport::Sendmail->new(); - } + my ($msg, $send_now) = (@_); + my $method = Bugzilla->params->{'mail_delivery_method'}; + return if $method eq 'None'; + + if ( Bugzilla->params->{'use_mailer_queue'} + && !$send_now + && !Bugzilla->dbh->bz_in_transaction()) + { + Bugzilla->job_queue->insert('send_mail', {msg => $msg}); + return; + } + + my $dbh = Bugzilla->dbh; + + my $email = ref($msg) ? $msg : Bugzilla::MIME->new($msg); + + # If we're called from within a transaction, we don't want to send the + # email immediately, in case the transaction is rolled back. Instead we + # insert it into the mail_staging table, and bz_commit_transaction calls + # send_staged_mail() after the transaction is committed. + if (!$send_now && $dbh->bz_in_transaction()) { + + # The e-mail string may contain tainted values. + my $string = $email->as_string; + trick_taint($string); + + my $sth = $dbh->prepare("INSERT INTO mail_staging (message) VALUES (?)"); + $sth->bind_param(1, $string, $dbh->BLOB_TYPE); + $sth->execute; + return; + } + + my $from = $email->header('From'); + + my $hostname; + my $transport; + if ($method eq "Sendmail") { + if (ON_WINDOWS) { + $transport + = Bugzilla::Sender::Transport::Sendmail->new({sendmail => SENDMAIL_EXE}); } else { - # Sendmail will automatically append our hostname to the From - # address, but other mailers won't. - my $urlbase = Bugzilla->params->{'urlbase'}; - $urlbase =~ m|//([^:/]+)[:/]?|; - $hostname = $1 || 'localhost'; - $from .= "\@$hostname" if $from !~ /@/; - $email->header_set('From', $from); - - # Sendmail adds a Date: header also, but others may not. - if (!defined $email->header('Date')) { - $email->header_set('Date', time2str("%a, %d %b %Y %T %z", time())); - } - } - - if ($method eq "SMTP") { - my ($host, $port) = split(/:/, Bugzilla->params->{'smtpserver'}, 2); - $transport = Bugzilla->request_cache->{smtp} //= - Email::Sender::Transport::SMTP::Persistent->new({ - host => $host, - defined($port) ? (port => $port) : (), - sasl_username => Bugzilla->params->{'smtp_username'}, - sasl_password => Bugzilla->params->{'smtp_password'}, - helo => $hostname, - ssl => Bugzilla->params->{'smtp_ssl'}, - debug => Bugzilla->params->{'smtp_debug'} }); + $transport = Bugzilla::Sender::Transport::Sendmail->new(); } - - Bugzilla::Hook::process('mailer_before_send', { email => $email }); - - return if $email->header('to') eq ''; - - if ($method eq "Test") { - my $filename = bz_locations()->{'datadir'} . '/mailer.testfile'; - open TESTFILE, '>>', $filename; - # From - is required to be a valid mbox file. - print TESTFILE "\n\nFrom - " . $email->header('Date') . "\n" . $email->as_string; - close TESTFILE; + } + else { + # Sendmail will automatically append our hostname to the From + # address, but other mailers won't. + my $urlbase = Bugzilla->params->{'urlbase'}; + $urlbase =~ m|//([^:/]+)[:/]?|; + $hostname = $1 || 'localhost'; + $from .= "\@$hostname" if $from !~ /@/; + $email->header_set('From', $from); + + # Sendmail adds a Date: header also, but others may not. + if (!defined $email->header('Date')) { + $email->header_set('Date', time2str("%a, %d %b %Y %T %z", time())); } - else { - # This is useful for Sendmail, so we put it out here. - local $ENV{PATH} = SENDMAIL_PATH; - eval { sendmail($email, { transport => $transport }) }; - if ($@) { - ThrowCodeError('mail_send_error', { msg => $@->message, mail => $email }); - } + } + + if ($method eq "SMTP") { + my ($host, $port) = split(/:/, Bugzilla->params->{'smtpserver'}, 2); + $transport = Bugzilla->request_cache->{smtp} + //= Email::Sender::Transport::SMTP::Persistent->new({ + host => $host, + defined($port) ? (port => $port) : (), + sasl_username => Bugzilla->params->{'smtp_username'}, + sasl_password => Bugzilla->params->{'smtp_password'}, + helo => $hostname, + ssl => Bugzilla->params->{'smtp_ssl'}, + debug => Bugzilla->params->{'smtp_debug'} + }); + } + + Bugzilla::Hook::process('mailer_before_send', {email => $email}); + + return if $email->header('to') eq ''; + + if ($method eq "Test") { + my $filename = bz_locations()->{'datadir'} . '/mailer.testfile'; + open TESTFILE, '>>', $filename; + + # From - is required to be a valid mbox file. + print TESTFILE "\n\nFrom - " + . $email->header('Date') . "\n" + . $email->as_string; + close TESTFILE; + } + else { + # This is useful for Sendmail, so we put it out here. + local $ENV{PATH} = SENDMAIL_PATH; + eval { sendmail($email, {transport => $transport}) }; + if ($@) { + ThrowCodeError('mail_send_error', {msg => $@->message, mail => $email}); } + } } # Builds header suitable for use as a threading marker in email notifications sub build_thread_marker { - my ($bug_id, $user_id, $is_new) = @_; - - if (!defined $user_id) { - $user_id = Bugzilla->user->id; - } - - my $sitespec = '@' . Bugzilla->params->{'urlbase'}; - $sitespec =~ s/:\/\//\./; # Make the protocol look like part of the domain - $sitespec =~ s/^([^:\/]+):(\d+)/$1/; # Remove a port number, to relocate - if ($2) { - $sitespec = "-$2$sitespec"; # Put the port number back in, before the '@' - } - - my $threadingmarker; - if ($is_new) { - $threadingmarker = "Message-ID: "; - } - else { - my $rand_bits = generate_random_password(10); - $threadingmarker = "Message-ID: " . - "\nIn-Reply-To: " . - "\nReferences: "; - } - - return $threadingmarker; + my ($bug_id, $user_id, $is_new) = @_; + + if (!defined $user_id) { + $user_id = Bugzilla->user->id; + } + + my $sitespec = '@' . Bugzilla->params->{'urlbase'}; + $sitespec =~ s/:\/\//\./; # Make the protocol look like part of the domain + $sitespec =~ s/^([^:\/]+):(\d+)/$1/; # Remove a port number, to relocate + if ($2) { + $sitespec = "-$2$sitespec"; # Put the port number back in, before the '@' + } + + my $threadingmarker; + if ($is_new) { + $threadingmarker = "Message-ID: "; + } + else { + my $rand_bits = generate_random_password(10); + $threadingmarker + = "Message-ID: " + . "\nIn-Reply-To: " + . "\nReferences: "; + } + + return $threadingmarker; } sub send_staged_mail { - my $dbh = Bugzilla->dbh; + my $dbh = Bugzilla->dbh; - my $emails = $dbh->selectall_arrayref('SELECT id, message FROM mail_staging'); - my $sth = $dbh->prepare('DELETE FROM mail_staging WHERE id = ?'); + my $emails = $dbh->selectall_arrayref('SELECT id, message FROM mail_staging'); + my $sth = $dbh->prepare('DELETE FROM mail_staging WHERE id = ?'); - foreach my $email (@$emails) { - my ($id, $message) = @$email; - MessageToMTA($message); - $sth->execute($id); - } + foreach my $email (@$emails) { + my ($id, $message) = @$email; + MessageToMTA($message); + $sth->execute($id); + } } 1; diff --git a/Bugzilla/Memcached.pm b/Bugzilla/Memcached.pm index df90fef93..2f30c186a 100644 --- a/Bugzilla/Memcached.pm +++ b/Bugzilla/Memcached.pm @@ -20,281 +20,278 @@ use URI::Escape; use constant MAX_KEY_LENGTH => 250; sub _new { - my $invocant = shift; - my $class = ref($invocant) || $invocant; - my $self = {}; - - # always return an object to simplify calling code when memcached is - # disabled. - if (Bugzilla->feature('memcached') - && Bugzilla->params->{memcached_servers}) - { - require Cache::Memcached; - $self->{namespace} = Bugzilla->params->{memcached_namespace} || ''; - $self->{memcached} = - Cache::Memcached->new({ - servers => [ split(/[, ]+/, Bugzilla->params->{memcached_servers}) ], - namespace => $self->{namespace}, - }); - } - return bless($self, $class); + my $invocant = shift; + my $class = ref($invocant) || $invocant; + my $self = {}; + + # always return an object to simplify calling code when memcached is + # disabled. + if (Bugzilla->feature('memcached') && Bugzilla->params->{memcached_servers}) { + require Cache::Memcached; + $self->{namespace} = Bugzilla->params->{memcached_namespace} || ''; + $self->{memcached} = Cache::Memcached->new({ + servers => [split(/[, ]+/, Bugzilla->params->{memcached_servers})], + namespace => $self->{namespace}, + }); + } + return bless($self, $class); } sub enabled { - return $_[0]->{memcached} ? 1 : 0; + return $_[0]->{memcached} ? 1 : 0; } sub set { - my ($self, $args) = @_; - return unless $self->{memcached}; - - # { key => $key, value => $value } - if (exists $args->{key}) { - $self->_set($args->{key}, $args->{value}); + my ($self, $args) = @_; + return unless $self->{memcached}; + + # { key => $key, value => $value } + if (exists $args->{key}) { + $self->_set($args->{key}, $args->{value}); + } + + # { table => $table, id => $id, name => $name, data => $data } + elsif (exists $args->{table} && exists $args->{id} && exists $args->{name}) { + + # For caching of Bugzilla::Object, we have to be able to clear the + # cached values when given either the object's id or name. + my ($table, $id, $name, $data) = @$args{qw(table id name data)}; + $self->_set("$table.id.$id", $data); + if (defined $name) { + $self->_set("$table.name_id.$name", $id); + $self->_set("$table.id_name.$id", $name); } + } - # { table => $table, id => $id, name => $name, data => $data } - elsif (exists $args->{table} && exists $args->{id} && exists $args->{name}) { - # For caching of Bugzilla::Object, we have to be able to clear the - # cached values when given either the object's id or name. - my ($table, $id, $name, $data) = @$args{qw(table id name data)}; - $self->_set("$table.id.$id", $data); - if (defined $name) { - $self->_set("$table.name_id.$name", $id); - $self->_set("$table.id_name.$id", $name); - } - } - - else { - ThrowCodeError('params_required', { function => "Bugzilla::Memcached::set", - params => [ 'key', 'table' ] }); - } + else { + ThrowCodeError('params_required', + {function => "Bugzilla::Memcached::set", params => ['key', 'table']}); + } } sub get { - my ($self, $args) = @_; - return unless $self->{memcached}; - - # { key => $key } - if (exists $args->{key}) { - return $self->_get($args->{key}); - } - - # { table => $table, id => $id } - elsif (exists $args->{table} && exists $args->{id}) { - my ($table, $id) = @$args{qw(table id)}; - return $self->_get("$table.id.$id"); - } - - # { table => $table, name => $name } - elsif (exists $args->{table} && exists $args->{name}) { - my ($table, $name) = @$args{qw(table name)}; - return unless my $id = $self->_get("$table.name_id.$name"); - return $self->_get("$table.id.$id"); - } - - else { - ThrowCodeError('params_required', { function => "Bugzilla::Memcached::get", - params => [ 'key', 'table' ] }); - } + my ($self, $args) = @_; + return unless $self->{memcached}; + + # { key => $key } + if (exists $args->{key}) { + return $self->_get($args->{key}); + } + + # { table => $table, id => $id } + elsif (exists $args->{table} && exists $args->{id}) { + my ($table, $id) = @$args{qw(table id)}; + return $self->_get("$table.id.$id"); + } + + # { table => $table, name => $name } + elsif (exists $args->{table} && exists $args->{name}) { + my ($table, $name) = @$args{qw(table name)}; + return unless my $id = $self->_get("$table.name_id.$name"); + return $self->_get("$table.id.$id"); + } + + else { + ThrowCodeError('params_required', + {function => "Bugzilla::Memcached::get", params => ['key', 'table']}); + } } sub set_config { - my ($self, $args) = @_; - return unless $self->{memcached}; - - if (exists $args->{key}) { - return $self->_set($self->_config_prefix . '.' . $args->{key}, $args->{data}); - } - else { - ThrowCodeError('params_required', { function => "Bugzilla::Memcached::set_config", - params => [ 'key' ] }); - } + my ($self, $args) = @_; + return unless $self->{memcached}; + + if (exists $args->{key}) { + return $self->_set($self->_config_prefix . '.' . $args->{key}, $args->{data}); + } + else { + ThrowCodeError('params_required', + {function => "Bugzilla::Memcached::set_config", params => ['key']}); + } } sub get_config { - my ($self, $args) = @_; - return unless $self->{memcached}; - - if (exists $args->{key}) { - return $self->_get($self->_config_prefix . '.' . $args->{key}); - } - else { - ThrowCodeError('params_required', { function => "Bugzilla::Memcached::get_config", - params => [ 'key' ] }); - } + my ($self, $args) = @_; + return unless $self->{memcached}; + + if (exists $args->{key}) { + return $self->_get($self->_config_prefix . '.' . $args->{key}); + } + else { + ThrowCodeError('params_required', + {function => "Bugzilla::Memcached::get_config", params => ['key']}); + } } sub clear { - my ($self, $args) = @_; - return unless $self->{memcached}; - - # { key => $key } - if (exists $args->{key}) { - $self->_delete($args->{key}); - } - - # { table => $table, id => $id } - elsif (exists $args->{table} && exists $args->{id}) { - my ($table, $id) = @$args{qw(table id)}; - my $name = $self->_get("$table.id_name.$id"); - $self->_delete("$table.id.$id"); - $self->_delete("$table.name_id.$name") if defined $name; - $self->_delete("$table.id_name.$id"); - } - - # { table => $table, name => $name } - elsif (exists $args->{table} && exists $args->{name}) { - my ($table, $name) = @$args{qw(table name)}; - return unless my $id = $self->_get("$table.name_id.$name"); - $self->_delete("$table.id.$id"); - $self->_delete("$table.name_id.$name"); - $self->_delete("$table.id_name.$id"); - } - - else { - ThrowCodeError('params_required', { function => "Bugzilla::Memcached::clear", - params => [ 'key', 'table' ] }); - } + my ($self, $args) = @_; + return unless $self->{memcached}; + + # { key => $key } + if (exists $args->{key}) { + $self->_delete($args->{key}); + } + + # { table => $table, id => $id } + elsif (exists $args->{table} && exists $args->{id}) { + my ($table, $id) = @$args{qw(table id)}; + my $name = $self->_get("$table.id_name.$id"); + $self->_delete("$table.id.$id"); + $self->_delete("$table.name_id.$name") if defined $name; + $self->_delete("$table.id_name.$id"); + } + + # { table => $table, name => $name } + elsif (exists $args->{table} && exists $args->{name}) { + my ($table, $name) = @$args{qw(table name)}; + return unless my $id = $self->_get("$table.name_id.$name"); + $self->_delete("$table.id.$id"); + $self->_delete("$table.name_id.$name"); + $self->_delete("$table.id_name.$id"); + } + + else { + ThrowCodeError('params_required', + {function => "Bugzilla::Memcached::clear", params => ['key', 'table']}); + } } sub clear_all { - my ($self) = @_; - return unless $self->{memcached}; - $self->_inc_prefix("global"); + my ($self) = @_; + return unless $self->{memcached}; + $self->_inc_prefix("global"); } sub clear_config { - my ($self, $args) = @_; - return unless $self->{memcached}; - if ($args && exists $args->{key}) { - $self->_delete($self->_config_prefix . '.' . $args->{key}); - } - else { - $self->_inc_prefix("config"); - } + my ($self, $args) = @_; + return unless $self->{memcached}; + if ($args && exists $args->{key}) { + $self->_delete($self->_config_prefix . '.' . $args->{key}); + } + else { + $self->_inc_prefix("config"); + } } # in order to clear all our keys, we add a prefix to all our keys. when we # need to "clear" all current keys, we increment the prefix. sub _prefix { - my ($self, $name) = @_; - # we don't want to change prefixes in the middle of a request - my $request_cache = Bugzilla->request_cache; - my $request_cache_key = "memcached_prefix_$name"; - if (!$request_cache->{$request_cache_key}) { - my $memcached = $self->{memcached}; - my $prefix = $memcached->get($name); - if (!$prefix) { - $prefix = time(); - if (!$memcached->add($name, $prefix)) { - # if this failed, either another process set the prefix, or - # memcached is down. assume we lost the race, and get the new - # value. if that fails, memcached is down so use a dummy - # prefix for this request. - $prefix = $memcached->get($name) || 0; - } - } - $request_cache->{$request_cache_key} = $prefix; + my ($self, $name) = @_; + + # we don't want to change prefixes in the middle of a request + my $request_cache = Bugzilla->request_cache; + my $request_cache_key = "memcached_prefix_$name"; + if (!$request_cache->{$request_cache_key}) { + my $memcached = $self->{memcached}; + my $prefix = $memcached->get($name); + if (!$prefix) { + $prefix = time(); + if (!$memcached->add($name, $prefix)) { + + # if this failed, either another process set the prefix, or + # memcached is down. assume we lost the race, and get the new + # value. if that fails, memcached is down so use a dummy + # prefix for this request. + $prefix = $memcached->get($name) || 0; + } } - return $request_cache->{$request_cache_key}; + $request_cache->{$request_cache_key} = $prefix; + } + return $request_cache->{$request_cache_key}; } sub _inc_prefix { - my ($self, $name) = @_; - my $memcached = $self->{memcached}; - if (!$memcached->incr($name, 1)) { - $memcached->add($name, time()); - } - delete Bugzilla->request_cache->{"memcached_prefix_$name"}; + my ($self, $name) = @_; + my $memcached = $self->{memcached}; + if (!$memcached->incr($name, 1)) { + $memcached->add($name, time()); + } + delete Bugzilla->request_cache->{"memcached_prefix_$name"}; } sub _global_prefix { - return $_[0]->_prefix("global"); + return $_[0]->_prefix("global"); } sub _config_prefix { - return $_[0]->_prefix("config"); + return $_[0]->_prefix("config"); } sub _encode_key { - my ($self, $key) = @_; - $key = $self->_global_prefix . '.' . uri_escape_utf8($key); - return length($self->{namespace} . $key) > MAX_KEY_LENGTH - ? undef - : $key; + my ($self, $key) = @_; + $key = $self->_global_prefix . '.' . uri_escape_utf8($key); + return length($self->{namespace} . $key) > MAX_KEY_LENGTH ? undef : $key; } sub _set { - my ($self, $key, $value) = @_; - if (blessed($value)) { - # we don't support blessed objects - ThrowCodeError('param_invalid', { function => "Bugzilla::Memcached::set", - param => "value" }); - } + my ($self, $key, $value) = @_; + if (blessed($value)) { + + # we don't support blessed objects + ThrowCodeError('param_invalid', + {function => "Bugzilla::Memcached::set", param => "value"}); + } - $key = $self->_encode_key($key) - or return; - return $self->{memcached}->set($key, $value); + $key = $self->_encode_key($key) or return; + return $self->{memcached}->set($key, $value); } sub _get { - my ($self, $key) = @_; - - $key = $self->_encode_key($key) - or return; - my $value = $self->{memcached}->get($key); - return unless defined $value; - - # detaint returned values - # hashes and arrays are detainted just one level deep - if (ref($value) eq 'HASH') { + my ($self, $key) = @_; + + $key = $self->_encode_key($key) or return; + my $value = $self->{memcached}->get($key); + return unless defined $value; + + # detaint returned values + # hashes and arrays are detainted just one level deep + if (ref($value) eq 'HASH') { + _detaint_hashref($value); + } + elsif (ref($value) eq 'ARRAY') { + foreach my $value (@$value) { + next unless defined $value; + + # arrays of hashes and arrays are common + if (ref($value) eq 'HASH') { _detaint_hashref($value); - } - elsif (ref($value) eq 'ARRAY') { - foreach my $value (@$value) { - next unless defined $value; - # arrays of hashes and arrays are common - if (ref($value) eq 'HASH') { - _detaint_hashref($value); - } - elsif (ref($value) eq 'ARRAY') { - _detaint_arrayref($value); - } - elsif (!ref($value)) { - trick_taint($value); - } - } - } - elsif (!ref($value)) { + } + elsif (ref($value) eq 'ARRAY') { + _detaint_arrayref($value); + } + elsif (!ref($value)) { trick_taint($value); + } } - return $value; + } + elsif (!ref($value)) { + trick_taint($value); + } + return $value; } sub _detaint_hashref { - my ($hashref) = @_; - foreach my $value (values %$hashref) { - if (defined($value) && !ref($value)) { - trick_taint($value); - } + my ($hashref) = @_; + foreach my $value (values %$hashref) { + if (defined($value) && !ref($value)) { + trick_taint($value); } + } } sub _detaint_arrayref { - my ($arrayref) = @_; - foreach my $value (@$arrayref) { - if (defined($value) && !ref($value)) { - trick_taint($value); - } + my ($arrayref) = @_; + foreach my $value (@$arrayref) { + if (defined($value) && !ref($value)) { + trick_taint($value); } + } } sub _delete { - my ($self, $key) = @_; - $key = $self->_encode_key($key) - or return; - return $self->{memcached}->delete($key); + my ($self, $key) = @_; + $key = $self->_encode_key($key) or return; + return $self->{memcached}->delete($key); } 1; diff --git a/Bugzilla/Migrate.pm b/Bugzilla/Migrate.pm index 7865c842d..75b5eda59 100644 --- a/Bugzilla/Migrate.pm +++ b/Bugzilla/Migrate.pm @@ -20,7 +20,7 @@ use Bugzilla::Install::Requirements (); use Bugzilla::Install::Util qw(indicate_progress); use Bugzilla::Product; use Bugzilla::Util qw(get_text trim generate_random_password); -use Bugzilla::User (); +use Bugzilla::User (); use Bugzilla::Status (); use Bugzilla::Version; @@ -37,10 +37,10 @@ use constant REQUIRED_MODULES => []; use constant NON_COMMENT_FIELDS => (); use constant CONFIG_VARS => ( - { - name => 'translate_fields', - default => {}, - desc => <<'END', + { + name => 'translate_fields', + default => {}, + desc => <<'END', # This maps field names in your bug-tracker to Bugzilla field names. If a field # has the same name in your bug-tracker and Bugzilla (case-insensitively), it # doesn't need a mapping here. If a field isn't listed here and doesn't have @@ -64,11 +64,11 @@ use constant CONFIG_VARS => ( # variable by default, then that field will be automatically created by # the migrator and you don't have to worry about it. END - }, - { - name => 'translate_values', - default => {}, - desc => <<'END', + }, + { + name => 'translate_values', + default => {}, + desc => <<'END', # This configuration variable allows you to say that a particular field # value in your current bug-tracker should be translated to a different # value when it's imported into Bugzilla. @@ -108,22 +108,22 @@ END # # Values that don't get translated will be imported as-is. END - }, - { - name => 'starting_bug_id', - default => 0, - desc => <<'END', + }, + { + name => 'starting_bug_id', + default => 0, + desc => <<'END', # What bug ID do you want the first imported bug to get? If you set this to # 0, then the imported bug ids will just start right after the current # bug ids. If you use this configuration variable, you must make sure that # nobody else is using your Bugzilla while you run the migration, or a new # bug filed by a user might take this ID instead. END - }, - { - name => 'timezone', - default => 'local', - desc => <<'END', + }, + { + name => 'timezone', + default => 'local', + desc => <<'END', # If migrate.pl comes across any dates without timezones, while doing the # migration, what timezone should we assume those dates are in? # The best format for this variable is something like "America/Los Angeles". @@ -133,7 +133,7 @@ END # The special value "local" means "use the same timezone as the system I # am running this script on now". END - }, + }, ); use constant USER_FIELDS => qw(user assigned_to qa_contact reporter cc); @@ -143,43 +143,46 @@ use constant USER_FIELDS => qw(user assigned_to qa_contact reporter cc); ######################### sub do_migration { - my $self = shift; - my $dbh = Bugzilla->dbh; - # On MySQL, setting serial values implicitly commits a transaction, - # so we want to do it up here, outside of any transaction. This also - # has the advantage of loading the config before anything else is done. - if ($self->config('starting_bug_id')) { - $dbh->bz_set_next_serial_value('bugs', 'bug_id', - $self->config('starting_bug_id')); - } - $dbh->bz_start_transaction(); - - $self->before_read(); - # Read Other Database - my $users = $self->users; - my $products = $self->products; - my $bugs = $self->bugs; - $self->after_read(); - - $self->translate_all_bugs($bugs); - - Bugzilla->set_user(Bugzilla::User->super_user); - - # Insert into Bugzilla - $self->before_insert(); - $self->insert_users($users); - $self->insert_products($products); - $self->create_custom_fields(); - $self->create_legal_values($bugs); - $self->insert_bugs($bugs); - $self->after_insert(); - if ($self->dry_run) { - $dbh->bz_rollback_transaction(); - $self->reset_serial_values(); - } - else { - $dbh->bz_commit_transaction(); - } + my $self = shift; + my $dbh = Bugzilla->dbh; + + # On MySQL, setting serial values implicitly commits a transaction, + # so we want to do it up here, outside of any transaction. This also + # has the advantage of loading the config before anything else is done. + if ($self->config('starting_bug_id')) { + $dbh->bz_set_next_serial_value('bugs', 'bug_id', + $self->config('starting_bug_id')); + } + $dbh->bz_start_transaction(); + + $self->before_read(); + + # Read Other Database + my $users = $self->users; + my $products = $self->products; + my $bugs = $self->bugs; + $self->after_read(); + + $self->translate_all_bugs($bugs); + + Bugzilla->set_user(Bugzilla::User->super_user); + + # Insert into Bugzilla + $self->before_insert(); + $self->insert_users($users); + $self->insert_products($products); + $self->create_custom_fields(); + $self->create_legal_values($bugs); + $self->insert_bugs($bugs); + $self->after_insert(); + + if ($self->dry_run) { + $dbh->bz_rollback_transaction(); + $self->reset_serial_values(); + } + else { + $dbh->bz_commit_transaction(); + } } ################ @@ -187,24 +190,23 @@ sub do_migration { ################ sub new { - my ($class) = @_; - my $self = { }; - bless $self, $class; - return $self; + my ($class) = @_; + my $self = {}; + bless $self, $class; + return $self; } sub load { - my ($class, $from) = @_; - my $libdir = bz_locations()->{libpath}; - my @migration_modules = glob("$libdir/Bugzilla/Migrate/*"); - my ($module) = grep { basename($_) =~ /^\Q$from\E\.pm$/i } - @migration_modules; - if (!$module) { - ThrowUserError('migrate_from_invalid', { from => $from }); - } - require $module; - my $canonical_name = _canonical_name($module); - return "Bugzilla::Migrate::$canonical_name"->new; + my ($class, $from) = @_; + my $libdir = bz_locations()->{libpath}; + my @migration_modules = glob("$libdir/Bugzilla/Migrate/*"); + my ($module) = grep { basename($_) =~ /^\Q$from\E\.pm$/i } @migration_modules; + if (!$module) { + ThrowUserError('migrate_from_invalid', {from => $from}); + } + require $module; + my $canonical_name = _canonical_name($module); + return "Bugzilla::Migrate::$canonical_name"->new; } ############# @@ -212,67 +214,67 @@ sub load { ############# sub name { - my $self = shift; - return _canonical_name(ref $self); + my $self = shift; + return _canonical_name(ref $self); } sub dry_run { - my ($self, $value) = @_; - if (scalar(@_) > 1) { - $self->{dry_run} = $value; - } - return $self->{dry_run} || 0; + my ($self, $value) = @_; + if (scalar(@_) > 1) { + $self->{dry_run} = $value; + } + return $self->{dry_run} || 0; } sub verbose { - my ($self, $value) = @_; - if (scalar(@_) > 1) { - $self->{verbose} = $value; - } - return $self->{verbose} || 0; + my ($self, $value) = @_; + if (scalar(@_) > 1) { + $self->{verbose} = $value; + } + return $self->{verbose} || 0; } sub debug { - my ($self, $value, $level) = @_; - $level ||= 1; - if ($self->verbose >= $level) { - $value = Dumper($value) if ref $value; - print STDERR $value, "\n"; - } + my ($self, $value, $level) = @_; + $level ||= 1; + if ($self->verbose >= $level) { + $value = Dumper($value) if ref $value; + print STDERR $value, "\n"; + } } sub bug_fields { - my $self = shift; - $self->{bug_fields} ||= Bugzilla->fields({ by_name => 1 }); - return $self->{bug_fields}; + my $self = shift; + $self->{bug_fields} ||= Bugzilla->fields({by_name => 1}); + return $self->{bug_fields}; } sub users { - my $self = shift; - if (!exists $self->{users}) { - say get_text('migrate_reading_users'); - $self->{users} = $self->_read_users(); - } - return $self->{users}; + my $self = shift; + if (!exists $self->{users}) { + say get_text('migrate_reading_users'); + $self->{users} = $self->_read_users(); + } + return $self->{users}; } sub products { - my $self = shift; - if (!exists $self->{products}) { - say get_text('migrate_reading_products'); - $self->{products} = $self->_read_products(); - } - return $self->{products}; + my $self = shift; + if (!exists $self->{products}) { + say get_text('migrate_reading_products'); + $self->{products} = $self->_read_products(); + } + return $self->{products}; } sub bugs { - my $self = shift; - if (!exists $self->{bugs}) { - say get_text('migrate_reading_bugs'); - $self->{bugs} = $self->_read_bugs(); - } - return $self->{bugs}; + my $self = shift; + if (!exists $self->{bugs}) { + say get_text('migrate_reading_bugs'); + $self->{bugs} = $self->_read_bugs(); + } + return $self->{bugs}; } ########### @@ -280,49 +282,49 @@ sub bugs { ########### sub check_requirements { - my $self = shift; - my $missing = Bugzilla::Install::Requirements::_check_missing( - $self->REQUIRED_MODULES, 1); - my %results = ( - apache => [], - pass => @$missing ? 0 : 1, - missing => $missing, - any_missing => @$missing ? 1 : 0, - hide_all => 1, - # These are just for compatibility with print_module_instructions - one_dbd => 1, - optional => [], - ); - Bugzilla::Install::Requirements::print_module_instructions( - \%results, 1); - exit(1) if @$missing; + my $self = shift; + my $missing + = Bugzilla::Install::Requirements::_check_missing($self->REQUIRED_MODULES, 1); + my %results = ( + apache => [], + pass => @$missing ? 0 : 1, + missing => $missing, + any_missing => @$missing ? 1 : 0, + hide_all => 1, + + # These are just for compatibility with print_module_instructions + one_dbd => 1, + optional => [], + ); + Bugzilla::Install::Requirements::print_module_instructions(\%results, 1); + exit(1) if @$missing; } sub reset_serial_values { - my $self = shift; - return if $self->{serial_values_reset}; - my $dbh = Bugzilla->dbh; - my %reset = ( - 'bugs' => 'bug_id', - 'attachments' => 'attach_id', - 'profiles' => 'userid', - 'longdescs' => 'comment_id', - 'products' => 'id', - 'components' => 'id', - 'versions' => 'id', - 'milestones' => 'id', - ); - my @select_fields = grep { $_->is_select } (values %{ $self->bug_fields }); - foreach my $field (@select_fields) { - next if $field->is_abnormal; - $reset{$field->name} = 'id'; - } - - while (my ($table, $column) = each %reset) { - $dbh->bz_set_next_serial_value($table, $column); - } - - $self->{serial_values_reset} = 1; + my $self = shift; + return if $self->{serial_values_reset}; + my $dbh = Bugzilla->dbh; + my %reset = ( + 'bugs' => 'bug_id', + 'attachments' => 'attach_id', + 'profiles' => 'userid', + 'longdescs' => 'comment_id', + 'products' => 'id', + 'components' => 'id', + 'versions' => 'id', + 'milestones' => 'id', + ); + my @select_fields = grep { $_->is_select } (values %{$self->bug_fields}); + foreach my $field (@select_fields) { + next if $field->is_abnormal; + $reset{$field->name} = 'id'; + } + + while (my ($table, $column) = each %reset) { + $dbh->bz_set_next_serial_value($table, $column); + } + + $self->{serial_values_reset} = 1; } ################### @@ -330,160 +332,167 @@ sub reset_serial_values { ################### sub translate_all_bugs { - my ($self, $bugs) = @_; - say get_text('migrate_translating_bugs'); - # We modify the array in place so that $self->bugs will return the - # modified bugs, in case $self->before_insert wants them. - my $num_bugs = scalar(@$bugs); - for (my $i = 0; $i < $num_bugs; $i++) { - $bugs->[$i] = $self->translate_bug($bugs->[$i]); - } + my ($self, $bugs) = @_; + say get_text('migrate_translating_bugs'); + + # We modify the array in place so that $self->bugs will return the + # modified bugs, in case $self->before_insert wants them. + my $num_bugs = scalar(@$bugs); + for (my $i = 0; $i < $num_bugs; $i++) { + $bugs->[$i] = $self->translate_bug($bugs->[$i]); + } } sub translate_bug { - my ($self, $fields) = @_; - my (%bug, %other_fields); - my $original_status; - foreach my $field (keys %$fields) { - my $value = delete $fields->{$field}; - my $bz_field = $self->translate_field($field); - if ($bz_field) { - $bug{$bz_field} = $self->translate_value($bz_field, $value); - if ($bz_field eq 'bug_status') { - $original_status = $value; - } - } - else { - $other_fields{$field} = $value; - } + my ($self, $fields) = @_; + my (%bug, %other_fields); + my $original_status; + foreach my $field (keys %$fields) { + my $value = delete $fields->{$field}; + my $bz_field = $self->translate_field($field); + if ($bz_field) { + $bug{$bz_field} = $self->translate_value($bz_field, $value); + if ($bz_field eq 'bug_status') { + $original_status = $value; + } } - - if (defined $original_status and !defined $bug{resolution} - and $self->map_value('bug_status_resolution', $original_status)) - { - $bug{resolution} = $self->map_value('bug_status_resolution', - $original_status); + else { + $other_fields{$field} = $value; } - - $bug{comment} = $self->_generate_description(\%bug, \%other_fields); - - return wantarray ? (\%bug, \%other_fields) : \%bug; + } + + if ( defined $original_status + and !defined $bug{resolution} + and $self->map_value('bug_status_resolution', $original_status)) + { + $bug{resolution} = $self->map_value('bug_status_resolution', $original_status); + } + + $bug{comment} = $self->_generate_description(\%bug, \%other_fields); + + return wantarray ? (\%bug, \%other_fields) : \%bug; } sub _generate_description { - my ($self, $bug, $fields) = @_; - - my $description = ""; - foreach my $field (sort keys %$fields) { - next if grep($_ eq $field, $self->NON_COMMENT_FIELDS); - my $value = delete $fields->{$field}; - next if $value eq ''; - $description .= "$field: $value\n"; - } - $description .= "\n" if $description; - - return $description . $bug->{comment}; + my ($self, $bug, $fields) = @_; + + my $description = ""; + foreach my $field (sort keys %$fields) { + next if grep($_ eq $field, $self->NON_COMMENT_FIELDS); + my $value = delete $fields->{$field}; + next if $value eq ''; + $description .= "$field: $value\n"; + } + $description .= "\n" if $description; + + return $description . $bug->{comment}; } sub translate_field { - my ($self, $field) = @_; - my $mapped = $self->config('translate_fields')->{$field}; - return $mapped if defined $mapped; - ($mapped) = grep { lc($_) eq lc($field) } (keys %{ $self->bug_fields }); - return $mapped; + my ($self, $field) = @_; + my $mapped = $self->config('translate_fields')->{$field}; + return $mapped if defined $mapped; + ($mapped) = grep { lc($_) eq lc($field) } (keys %{$self->bug_fields}); + return $mapped; } sub parse_date { - my ($self, $date) = @_; - my @time = strptime($date); - # Handle times with timezones that strptime doesn't know about. - if (!scalar @time) { - $date =~ s/\s+\S+$//; - @time = strptime($date); + my ($self, $date) = @_; + my @time = strptime($date); + + # Handle times with timezones that strptime doesn't know about. + if (!scalar @time) { + $date =~ s/\s+\S+$//; + @time = strptime($date); + } + my $tz; + if ($time[6]) { + $tz = DateTime::TimeZone->offset_as_string($time[6]); + } + else { + $tz = $self->config('timezone'); + $tz =~ s/\s/_/g; + if ($tz eq 'local') { + $tz = Bugzilla->local_timezone; } - my $tz; - if ($time[6]) { - $tz = DateTime::TimeZone->offset_as_string($time[6]); - } - else { - $tz = $self->config('timezone'); - $tz =~ s/\s/_/g; - if ($tz eq 'local') { - $tz = Bugzilla->local_timezone; - } - } - my $dt = DateTime->new({ - year => $time[5] + 1900, - month => $time[4] + 1, - day => $time[3], - hour => $time[2], - minute => $time[1], - second => int($time[0]), - time_zone => $tz, - }); - $dt->set_time_zone(Bugzilla->local_timezone); - return $dt->iso8601; + } + my $dt = DateTime->new({ + year => $time[5] + 1900, + month => $time[4] + 1, + day => $time[3], + hour => $time[2], + minute => $time[1], + second => int($time[0]), + time_zone => $tz, + }); + $dt->set_time_zone(Bugzilla->local_timezone); + return $dt->iso8601; } sub translate_value { - my ($self, $field, $value) = @_; - - if (!defined $value) { - warn("Got undefined value for $field\n"); - $value = ''; - } - - if (ref($value) eq 'ARRAY') { - return [ map($self->translate_value($field, $_), @$value) ]; - } + my ($self, $field, $value) = @_; - - if (defined $self->map_value($field, $value)) { - return $self->map_value($field, $value); - } - - if (grep($_ eq $field, USER_FIELDS)) { - if (defined $self->map_value('user', $value)) { - return $self->map_value('user', $value); - } - } + if (!defined $value) { + warn("Got undefined value for $field\n"); + $value = ''; + } + + if (ref($value) eq 'ARRAY') { + return [map($self->translate_value($field, $_), @$value)]; + } + + + if (defined $self->map_value($field, $value)) { + return $self->map_value($field, $value); + } - my $field_obj = $self->bug_fields->{$field}; - if ($field eq 'creation_ts' - or $field eq 'delta_ts' - or ($field_obj and - ($field_obj->type == FIELD_TYPE_DATETIME - or $field_obj->type == FIELD_TYPE_DATE))) - { - $value = trim($value); - return undef if !$value; - return $self->parse_date($value); + if (grep($_ eq $field, USER_FIELDS)) { + if (defined $self->map_value('user', $value)) { + return $self->map_value('user', $value); } - - return $value; + } + + my $field_obj = $self->bug_fields->{$field}; + if ( + $field eq 'creation_ts' + or $field eq 'delta_ts' + or ( + $field_obj + and + ($field_obj->type == FIELD_TYPE_DATETIME or $field_obj->type == FIELD_TYPE_DATE) + ) + ) + { + $value = trim($value); + return undef if !$value; + return $self->parse_date($value); + } + + return $value; } sub map_value { - my ($self, $field, $value) = @_; - return $self->_value_map->{$field}->{lc($value)}; + my ($self, $field, $value) = @_; + return $self->_value_map->{$field}->{lc($value)}; } sub _value_map { - my $self = shift; - if (!defined $self->{_value_map}) { - # Lowercase all values to make them case-insensitive. - my %map; - my $translation = $self->config('translate_values'); - foreach my $field (keys %$translation) { - my $value_mapping = $translation->{$field}; - foreach my $value (keys %$value_mapping) { - $map{$field}->{lc($value)} = $value_mapping->{$value}; - } - } - $self->{_value_map} = \%map; + my $self = shift; + if (!defined $self->{_value_map}) { + + # Lowercase all values to make them case-insensitive. + my %map; + my $translation = $self->config('translate_values'); + foreach my $field (keys %$translation) { + my $value_mapping = $translation->{$field}; + foreach my $value (keys %$value_mapping) { + $map{$field}->{lc($value)} = $value_mapping->{$value}; + } } - return $self->{_value_map}; + $self->{_value_map} = \%map; + } + return $self->{_value_map}; } ################# @@ -491,387 +500,402 @@ sub _value_map { ################# sub config { - my ($self, $var) = @_; - if (!exists $self->{config}) { - $self->{config} = $self->read_config; - } - return $self->{config}->{$var}; + my ($self, $var) = @_; + if (!exists $self->{config}) { + $self->{config} = $self->read_config; + } + return $self->{config}->{$var}; } sub config_file_name { - my $self = shift; - my $name = $self->name; - my $dir = bz_locations()->{datadir}; - return "$dir/migrate-$name.cfg" + my $self = shift; + my $name = $self->name; + my $dir = bz_locations()->{datadir}; + return "$dir/migrate-$name.cfg"; } sub read_config { - my ($self) = @_; - my $file = $self->config_file_name; - if (!-e $file) { - $self->write_config(); - ThrowUserError('migrate_config_created', { file => $file }); - } - open(my $fh, "<", $file) || die "$file: $!"; - my $safe = new Safe; - $safe->rdo($file); - my @read_symbols = map($_->{name}, $self->CONFIG_VARS); - my %config; - foreach my $var (@read_symbols) { - my $glob = $safe->varglob($var); - $config{$var} = $$glob; - } - return \%config; + my ($self) = @_; + my $file = $self->config_file_name; + if (!-e $file) { + $self->write_config(); + ThrowUserError('migrate_config_created', {file => $file}); + } + open(my $fh, "<", $file) || die "$file: $!"; + my $safe = new Safe; + $safe->rdo($file); + my @read_symbols = map($_->{name}, $self->CONFIG_VARS); + my %config; + foreach my $var (@read_symbols) { + my $glob = $safe->varglob($var); + $config{$var} = $$glob; + } + return \%config; } sub write_config { - my ($self) = @_; - my $file = $self->config_file_name; - open(my $fh, ">", $file) || die "$file: $!"; - # Fixed indentation - local $Data::Dumper::Indent = 1; - local $Data::Dumper::Quotekeys = 0; - local $Data::Dumper::Sortkeys = 1; - foreach my $var ($self->CONFIG_VARS) { - print $fh "\n", $var->{desc}, - Data::Dumper->Dump([$var->{default}], [$var->{name}]); - } - close($fh); + my ($self) = @_; + my $file = $self->config_file_name; + open(my $fh, ">", $file) || die "$file: $!"; + + # Fixed indentation + local $Data::Dumper::Indent = 1; + local $Data::Dumper::Quotekeys = 0; + local $Data::Dumper::Sortkeys = 1; + foreach my $var ($self->CONFIG_VARS) { + print $fh "\n", $var->{desc}, + Data::Dumper->Dump([$var->{default}], [$var->{name}]); + } + close($fh); } #################################### # Default Implementations of Hooks # #################################### -sub after_insert {} -sub before_insert {} -sub after_read {} -sub before_read {} +sub after_insert { } +sub before_insert { } +sub after_read { } +sub before_read { } ############# # Inserters # ############# sub insert_users { - my ($self, $users) = @_; - foreach my $user (@$users) { - next if new Bugzilla::User({ name => $user->{login_name} }); - my $generated_password; - if (!defined $user->{cryptpassword}) { - $generated_password = lc(generate_random_password()); - $user->{cryptpassword} = $generated_password; - } - my $created = Bugzilla::User->create($user); - print get_text('migrate_user_created', - { created => $created, - password => $generated_password }), "\n"; + my ($self, $users) = @_; + foreach my $user (@$users) { + next if new Bugzilla::User({name => $user->{login_name}}); + my $generated_password; + if (!defined $user->{cryptpassword}) { + $generated_password = lc(generate_random_password()); + $user->{cryptpassword} = $generated_password; } + my $created = Bugzilla::User->create($user); + print get_text('migrate_user_created', + {created => $created, password => $generated_password}), + "\n"; + } } # XXX This should also insert Classifications. sub insert_products { - my ($self, $products) = @_; - foreach my $product (@$products) { - my $components = delete $product->{components}; - - my $created_prod = new Bugzilla::Product({ name => $product->{name} }); - if (!$created_prod) { - $created_prod = Bugzilla::Product->create($product); - print get_text('migrate_product_created', - { created => $created_prod }), "\n"; - } - - foreach my $component (@$components) { - next if new Bugzilla::Component({ product => $created_prod, - name => $component->{name} }); - my $created_comp = Bugzilla::Component->create( - { %$component, product => $created_prod }); - print ' ', get_text('migrate_component_created', - { comp => $created_comp, - product => $created_prod }), "\n"; - } + my ($self, $products) = @_; + foreach my $product (@$products) { + my $components = delete $product->{components}; + + my $created_prod = new Bugzilla::Product({name => $product->{name}}); + if (!$created_prod) { + $created_prod = Bugzilla::Product->create($product); + print get_text('migrate_product_created', {created => $created_prod}), "\n"; } + + foreach my $component (@$components) { + next + if new Bugzilla::Component({ + product => $created_prod, name => $component->{name} + }); + my $created_comp + = Bugzilla::Component->create({%$component, product => $created_prod}); + print ' ', + get_text('migrate_component_created', + {comp => $created_comp, product => $created_prod}), + "\n"; + } + } } sub create_custom_fields { - my $self = shift; - foreach my $field (keys %{ $self->CUSTOM_FIELDS }) { - next if new Bugzilla::Field({ name => $field }); - my %values = %{ $self->CUSTOM_FIELDS->{$field} }; - # We set these all here for the dry-run case. - my $created = { %values, name => $field, custom => 1 }; - if (!$self->dry_run) { - $created = Bugzilla::Field->create($created); - } - say get_text('migrate_field_created', { field => $created }); + my $self = shift; + foreach my $field (keys %{$self->CUSTOM_FIELDS}) { + next if new Bugzilla::Field({name => $field}); + my %values = %{$self->CUSTOM_FIELDS->{$field}}; + + # We set these all here for the dry-run case. + my $created = {%values, name => $field, custom => 1}; + if (!$self->dry_run) { + $created = Bugzilla::Field->create($created); } - delete $self->{bug_fields}; + say get_text('migrate_field_created', {field => $created}); + } + delete $self->{bug_fields}; } sub create_legal_values { - my ($self, $bugs) = @_; - my @select_fields = grep($_->is_select, values %{ $self->bug_fields }); - - # Get all the values in use on all the bugs we're importing. - my (%values, %product_values); - foreach my $bug (@$bugs) { - foreach my $field (@select_fields) { - my $name = $field->name; - next if !defined $bug->{$name}; - $values{$name}->{$bug->{$name}} = 1; - } - foreach my $field (qw(version target_milestone)) { - # Fix per-product bug values here, because it's easier than - # doing it during _insert_bugs. - if (!defined $bug->{$field} or trim($bug->{$field}) eq '') { - my $accessor = $field; - $accessor =~ s/^target_//; $accessor .= "s"; - my $product = Bugzilla::Product->check($bug->{product}); - $bug->{$field} = $product->$accessor->[0]->name; - next; - } - $product_values{$bug->{product}}->{$field}->{$bug->{$field}} = 1; - } - } - + my ($self, $bugs) = @_; + my @select_fields = grep($_->is_select, values %{$self->bug_fields}); + + # Get all the values in use on all the bugs we're importing. + my (%values, %product_values); + foreach my $bug (@$bugs) { foreach my $field (@select_fields) { - next if $field->is_abnormal; - my $name = $field->name; - foreach my $value (keys %{ $values{$name} }) { - next if Bugzilla::Field::Choice->type($field)->new({ name => $value }); - Bugzilla::Field::Choice->type($field)->create({ value => $value }); - print get_text('migrate_value_created', - { field => $field, value => $value }), "\n"; - } + my $name = $field->name; + next if !defined $bug->{$name}; + $values{$name}->{$bug->{$name}} = 1; } - - foreach my $product (keys %product_values) { - my $prod_obj = Bugzilla::Product->check($product); - foreach my $version (keys %{ $product_values{$product}->{version} }) { - next if new Bugzilla::Version({ product => $prod_obj, - name => $version }); - my $created = Bugzilla::Version->create({ product => $prod_obj, - value => $version }); - my $field = $self->bug_fields->{version}; - print get_text('migrate_value_created', { product => $prod_obj, - field => $field, - value => $created->name }), "\n"; - } - foreach my $milestone (keys %{ $product_values{$product}->{target_milestone} }) { - next if new Bugzilla::Milestone({ product => $prod_obj, - name => $milestone }); - my $created = Bugzilla::Milestone->create( - { product => $prod_obj, value => $milestone }); - my $field = $self->bug_fields->{target_milestone}; - print get_text('migrate_value_created', { product => $prod_obj, - field => $field, - value => $created->name }), "\n"; - - } + foreach my $field (qw(version target_milestone)) { + + # Fix per-product bug values here, because it's easier than + # doing it during _insert_bugs. + if (!defined $bug->{$field} or trim($bug->{$field}) eq '') { + my $accessor = $field; + $accessor =~ s/^target_//; + $accessor .= "s"; + my $product = Bugzilla::Product->check($bug->{product}); + $bug->{$field} = $product->$accessor->[0]->name; + next; + } + $product_values{$bug->{product}}->{$field}->{$bug->{$field}} = 1; + } + } + + foreach my $field (@select_fields) { + next if $field->is_abnormal; + my $name = $field->name; + foreach my $value (keys %{$values{$name}}) { + next if Bugzilla::Field::Choice->type($field)->new({name => $value}); + Bugzilla::Field::Choice->type($field)->create({value => $value}); + print get_text('migrate_value_created', {field => $field, value => $value}), + "\n"; + } + } + + foreach my $product (keys %product_values) { + my $prod_obj = Bugzilla::Product->check($product); + foreach my $version (keys %{$product_values{$product}->{version}}) { + next if new Bugzilla::Version({product => $prod_obj, name => $version}); + my $created + = Bugzilla::Version->create({product => $prod_obj, value => $version}); + my $field = $self->bug_fields->{version}; + print get_text('migrate_value_created', + {product => $prod_obj, field => $field, value => $created->name}), + "\n"; + } + foreach my $milestone (keys %{$product_values{$product}->{target_milestone}}) { + next if new Bugzilla::Milestone({product => $prod_obj, name => $milestone}); + my $created + = Bugzilla::Milestone->create({product => $prod_obj, value => $milestone}); + my $field = $self->bug_fields->{target_milestone}; + print get_text('migrate_value_created', + {product => $prod_obj, field => $field, value => $created->name}), + "\n"; + } - + } + } sub insert_bugs { - my ($self, $bugs) = @_; - my $dbh = Bugzilla->dbh; - say get_text('migrate_creating_bugs'); - - my $init_statuses = Bugzilla::Status->can_change_to(); - my %allowed_statuses = map { lc($_->name) => 1 } @$init_statuses; - # Bypass the question of whether or not we can file UNCONFIRMED - # in any product by simply picking a non-UNCONFIRMED status as our - # default for bugs that don't have a status specified. - my $default_status = first { $_->name ne 'UNCONFIRMED' } @$init_statuses; - # Use the first resolution that's not blank. - my $default_resolution = - first { $_->name ne '' } - @{ $self->bug_fields->{resolution}->legal_values }; - - # Set the values of any required drop-down fields that aren't set. - my @standard_drop_downs = grep { !$_->custom and $_->is_select } - (values %{ $self->bug_fields }); - # Make bug_status get set before resolution. - @standard_drop_downs = sort { $a->name cmp $b->name } @standard_drop_downs; - # Cache all statuses for setting the resolution. - my %statuses = map { lc($_->name) => $_ } Bugzilla::Status->get_all; - - my $total = scalar @$bugs; - my $count = 1; - foreach my $bug (@$bugs) { - my $comments = delete $bug->{comments}; - my $history = delete $bug->{history}; - my $attachments = delete $bug->{attachments}; - - $self->debug($bug, 3); - - foreach my $field (@standard_drop_downs) { - next if $field->is_abnormal; - my $field_name = $field->name; - if (!defined $bug->{$field_name}) { - # If there's a default value for this, then just let create() - # pick it. - next if grep($_->is_default, @{ $field->legal_values }); - # Otherwise, pick the first valid value if this is a required - # field. - if ($field_name eq 'bug_status') { - $bug->{bug_status} = $default_status; - } - elsif ($field_name eq 'resolution') { - my $status = $statuses{lc($bug->{bug_status})}; - if (!$status->is_open) { - $bug->{resolution} = $default_resolution; - } - } - else { - $bug->{$field_name} = $field->legal_values->[0]->name; - } - } - } - - my $product = Bugzilla::Product->check($bug->{product}); - - # If this isn't a legal starting status, or if the bug has a - # resolution, then those will have to be set after creating the bug. - # We make them into objects so that we can normalize their names. - my ($set_status, $set_resolution); - if (defined $bug->{resolution}) { - $set_resolution = Bugzilla::Field::Choice->type('resolution') - ->new({ name => delete $bug->{resolution} }); + my ($self, $bugs) = @_; + my $dbh = Bugzilla->dbh; + say get_text('migrate_creating_bugs'); + + my $init_statuses = Bugzilla::Status->can_change_to(); + my %allowed_statuses = map { lc($_->name) => 1 } @$init_statuses; + + # Bypass the question of whether or not we can file UNCONFIRMED + # in any product by simply picking a non-UNCONFIRMED status as our + # default for bugs that don't have a status specified. + my $default_status = first { $_->name ne 'UNCONFIRMED' } @$init_statuses; + + # Use the first resolution that's not blank. + my $default_resolution = first { $_->name ne '' } + @{$self->bug_fields->{resolution}->legal_values}; + + # Set the values of any required drop-down fields that aren't set. + my @standard_drop_downs + = grep { !$_->custom and $_->is_select } (values %{$self->bug_fields}); + + # Make bug_status get set before resolution. + @standard_drop_downs = sort { $a->name cmp $b->name } @standard_drop_downs; + + # Cache all statuses for setting the resolution. + my %statuses = map { lc($_->name) => $_ } Bugzilla::Status->get_all; + + my $total = scalar @$bugs; + my $count = 1; + foreach my $bug (@$bugs) { + my $comments = delete $bug->{comments}; + my $history = delete $bug->{history}; + my $attachments = delete $bug->{attachments}; + + $self->debug($bug, 3); + + foreach my $field (@standard_drop_downs) { + next if $field->is_abnormal; + my $field_name = $field->name; + if (!defined $bug->{$field_name}) { + + # If there's a default value for this, then just let create() + # pick it. + next if grep($_->is_default, @{$field->legal_values}); + + # Otherwise, pick the first valid value if this is a required + # field. + if ($field_name eq 'bug_status') { + $bug->{bug_status} = $default_status; } - if (!$allowed_statuses{lc($bug->{bug_status})}) { - $set_status = new Bugzilla::Status({ name => $bug->{bug_status} }); - # Set the starting status to some status that Bugzilla will - # accept. We're going to overwrite it immediately afterward. - $bug->{bug_status} = $default_status; + elsif ($field_name eq 'resolution') { + my $status = $statuses{lc($bug->{bug_status})}; + if (!$status->is_open) { + $bug->{resolution} = $default_resolution; + } } - - # If we're in dry-run mode, our custom fields haven't been created - # yet, so we shouldn't try to set them on creation. - if ($self->dry_run) { - foreach my $field (keys %{ $self->CUSTOM_FIELDS }) { - delete $bug->{$field}; - } + else { + $bug->{$field_name} = $field->legal_values->[0]->name; } - - # File the bug as the reporter. - my $super_user = Bugzilla->user; - my $reporter = Bugzilla::User->check($bug->{reporter}); - # Allow the user to file a bug in any product, no matter their current - # permissions. - $reporter->{groups} = $super_user->groups; - Bugzilla->set_user($reporter); - my $created = Bugzilla::Bug->create($bug); - $self->debug('Created bug ' . $created->id); - Bugzilla->set_user($super_user); - - if (defined $bug->{creation_ts}) { - $dbh->do('UPDATE bugs SET creation_ts = ?, delta_ts = ? + } + } + + my $product = Bugzilla::Product->check($bug->{product}); + + # If this isn't a legal starting status, or if the bug has a + # resolution, then those will have to be set after creating the bug. + # We make them into objects so that we can normalize their names. + my ($set_status, $set_resolution); + if (defined $bug->{resolution}) { + $set_resolution = Bugzilla::Field::Choice->type('resolution') + ->new({name => delete $bug->{resolution}}); + } + if (!$allowed_statuses{lc($bug->{bug_status})}) { + $set_status = new Bugzilla::Status({name => $bug->{bug_status}}); + + # Set the starting status to some status that Bugzilla will + # accept. We're going to overwrite it immediately afterward. + $bug->{bug_status} = $default_status; + } + + # If we're in dry-run mode, our custom fields haven't been created + # yet, so we shouldn't try to set them on creation. + if ($self->dry_run) { + foreach my $field (keys %{$self->CUSTOM_FIELDS}) { + delete $bug->{$field}; + } + } + + # File the bug as the reporter. + my $super_user = Bugzilla->user; + my $reporter = Bugzilla::User->check($bug->{reporter}); + + # Allow the user to file a bug in any product, no matter their current + # permissions. + $reporter->{groups} = $super_user->groups; + Bugzilla->set_user($reporter); + my $created = Bugzilla::Bug->create($bug); + $self->debug('Created bug ' . $created->id); + Bugzilla->set_user($super_user); + + if (defined $bug->{creation_ts}) { + $dbh->do( + 'UPDATE bugs SET creation_ts = ?, delta_ts = ? WHERE bug_id = ?', undef, $bug->{creation_ts}, - $bug->{creation_ts}, $created->id); - } - if (defined $bug->{delta_ts}) { - $dbh->do('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?', - undef, $bug->{delta_ts}, $created->id); - } - # We don't need to send email for imported bugs. - $dbh->do('UPDATE bugs SET lastdiffed = delta_ts WHERE bug_id = ?', - undef, $created->id); - - # We don't use set_ and update() because that would create - # a bugs_activity entry that we don't want. - if ($set_status) { - $dbh->do('UPDATE bugs SET bug_status = ? WHERE bug_id = ?', - undef, $set_status->name, $created->id); - } - if ($set_resolution) { - $dbh->do('UPDATE bugs SET resolution = ? WHERE bug_id = ?', - undef, $set_resolution->name, $created->id); - } - - $self->_insert_comments($created, $comments); - $self->_insert_history($created, $history); - $self->_insert_attachments($created, $attachments); - - # bugs_fulltext isn't transactional, so if we're in a dry-run we - # need to delete anything that we put in there. - if ($self->dry_run) { - $dbh->do('DELETE FROM bugs_fulltext WHERE bug_id = ?', - undef, $created->id); - } + $bug->{creation_ts}, $created->id + ); + } + if (defined $bug->{delta_ts}) { + $dbh->do('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?', + undef, $bug->{delta_ts}, $created->id); + } - if (!$self->verbose) { - indicate_progress({ current => $count++, every => 5, total => $total }); - } + # We don't need to send email for imported bugs. + $dbh->do('UPDATE bugs SET lastdiffed = delta_ts WHERE bug_id = ?', + undef, $created->id); + + # We don't use set_ and update() because that would create + # a bugs_activity entry that we don't want. + if ($set_status) { + $dbh->do('UPDATE bugs SET bug_status = ? WHERE bug_id = ?', + undef, $set_status->name, $created->id); + } + if ($set_resolution) { + $dbh->do('UPDATE bugs SET resolution = ? WHERE bug_id = ?', + undef, $set_resolution->name, $created->id); } + + $self->_insert_comments($created, $comments); + $self->_insert_history($created, $history); + $self->_insert_attachments($created, $attachments); + + # bugs_fulltext isn't transactional, so if we're in a dry-run we + # need to delete anything that we put in there. + if ($self->dry_run) { + $dbh->do('DELETE FROM bugs_fulltext WHERE bug_id = ?', undef, $created->id); + } + + if (!$self->verbose) { + indicate_progress({current => $count++, every => 5, total => $total}); + } + } } sub _insert_comments { - my ($self, $bug, $comments) = @_; - return if !$comments; - $self->debug(' Inserting comments:', 2); - foreach my $comment (@$comments) { - $self->debug($comment, 3); - my %copy = %$comment; - # XXX In the future, if we have a Bugzilla::Comment->create, this - # should use it. - my $who = Bugzilla::User->check(delete $copy{who}); - $copy{who} = $who->id; - $copy{bug_id} = $bug->id; - $self->_do_table_insert('longdescs', \%copy); - $self->debug(" Inserted comment from " . $who->login, 2); - } - $bug->_sync_fulltext( update_comments => 1 ); + my ($self, $bug, $comments) = @_; + return if !$comments; + $self->debug(' Inserting comments:', 2); + foreach my $comment (@$comments) { + $self->debug($comment, 3); + my %copy = %$comment; + + # XXX In the future, if we have a Bugzilla::Comment->create, this + # should use it. + my $who = Bugzilla::User->check(delete $copy{who}); + $copy{who} = $who->id; + $copy{bug_id} = $bug->id; + $self->_do_table_insert('longdescs', \%copy); + $self->debug(" Inserted comment from " . $who->login, 2); + } + $bug->_sync_fulltext(update_comments => 1); } sub _insert_history { - my ($self, $bug, $history) = @_; - return if !$history; - $self->debug(' Inserting history:', 2); - foreach my $item (@$history) { - $self->debug($item, 3); - my $who = Bugzilla::User->check($item->{who}); - LogActivityEntry($bug->id, $item->{field}, $item->{removed}, - $item->{added}, $who->id, $item->{bug_when}); - $self->debug(" $item->{field} change from " . $who->login, 2); - } + my ($self, $bug, $history) = @_; + return if !$history; + $self->debug(' Inserting history:', 2); + foreach my $item (@$history) { + $self->debug($item, 3); + my $who = Bugzilla::User->check($item->{who}); + LogActivityEntry($bug->id, $item->{field}, $item->{removed}, $item->{added}, + $who->id, $item->{bug_when}); + $self->debug(" $item->{field} change from " . $who->login, 2); + } } sub _insert_attachments { - my ($self, $bug, $attachments) = @_; - return if !$attachments; - $self->debug(' Inserting attachments:', 2); - foreach my $attachment (@$attachments) { - $self->debug($attachment, 3); - # Make sure that our pointer is at the beginning of the file, - # because usually it will be at the end, having just been fully - # written to. - if (ref $attachment->{data}) { - $attachment->{data}->seek(0, SEEK_SET); - } - - my $submitter = Bugzilla::User->check(delete $attachment->{submitter}); - my $super_user = Bugzilla->user; - # Make sure the submitter can attach this attachment no matter what. - $submitter->{groups} = $super_user->groups; - Bugzilla->set_user($submitter); - my $created = - Bugzilla::Attachment->create({ %$attachment, bug => $bug }); - $self->debug(' Attachment ' . $created->description . ' from ' - . $submitter->login, 2); - Bugzilla->set_user($super_user); + my ($self, $bug, $attachments) = @_; + return if !$attachments; + $self->debug(' Inserting attachments:', 2); + foreach my $attachment (@$attachments) { + $self->debug($attachment, 3); + + # Make sure that our pointer is at the beginning of the file, + # because usually it will be at the end, having just been fully + # written to. + if (ref $attachment->{data}) { + $attachment->{data}->seek(0, SEEK_SET); } + + my $submitter = Bugzilla::User->check(delete $attachment->{submitter}); + my $super_user = Bugzilla->user; + + # Make sure the submitter can attach this attachment no matter what. + $submitter->{groups} = $super_user->groups; + Bugzilla->set_user($submitter); + my $created = Bugzilla::Attachment->create({%$attachment, bug => $bug}); + $self->debug( + ' Attachment ' . $created->description . ' from ' . $submitter->login, 2); + Bugzilla->set_user($super_user); + } } sub _do_table_insert { - my ($self, $table, $hash) = @_; - my @fields = keys %$hash; - my @questions = ('?') x @fields; - my @values = map { $hash->{$_} } @fields; - my $field_sql = join(',', @fields); - my $question_sql = join(',', @questions); - Bugzilla->dbh->do("INSERT INTO $table ($field_sql) VALUES ($question_sql)", - undef, @values); + my ($self, $table, $hash) = @_; + my @fields = keys %$hash; + my @questions = ('?') x @fields; + my @values = map { $hash->{$_} } @fields; + my $field_sql = join(',', @fields); + my $question_sql = join(',', @questions); + Bugzilla->dbh->do("INSERT INTO $table ($field_sql) VALUES ($question_sql)", + undef, @values); } ###################### @@ -879,11 +903,11 @@ sub _do_table_insert { ###################### sub _canonical_name { - my ($module) = @_; - $module =~ s{::}{/}g; - $module = basename($module); - $module =~ s/\.pm$//g; - return $module; + my ($module) = @_; + $module =~ s{::}{/}g; + $module = basename($module); + $module =~ s/\.pm$//g; + return $module; } 1; diff --git a/Bugzilla/Migrate/Gnats.pm b/Bugzilla/Migrate/Gnats.pm index 5feda4b8d..3d51aa60d 100644 --- a/Bugzilla/Migrate/Gnats.pm +++ b/Bugzilla/Migrate/Gnats.pm @@ -25,88 +25,87 @@ use List::MoreUtils qw(firstidx); use List::Util qw(first); use constant REQUIRED_MODULES => [ - { - package => 'Email-Simple-FromHandle', - module => 'Email::Simple::FromHandle', - # This version added seekable handles. - version => 0.050, - }, + { + package => 'Email-Simple-FromHandle', + module => 'Email::Simple::FromHandle', + + # This version added seekable handles. + version => 0.050, + }, ]; use constant FIELD_MAP => { - 'Number' => 'bug_id', - 'Category' => 'product', - 'Synopsis' => 'short_desc', - 'Responsible' => 'assigned_to', - 'State' => 'bug_status', - 'Class' => 'cf_type', - 'Classification' => '', - 'Originator' => 'reporter', - 'Arrival-Date' => 'creation_ts', - 'Last-Modified' => 'delta_ts', - 'Release' => 'version', - 'Severity' => 'bug_severity', - 'Description' => 'comment', + 'Number' => 'bug_id', + 'Category' => 'product', + 'Synopsis' => 'short_desc', + 'Responsible' => 'assigned_to', + 'State' => 'bug_status', + 'Class' => 'cf_type', + 'Classification' => '', + 'Originator' => 'reporter', + 'Arrival-Date' => 'creation_ts', + 'Last-Modified' => 'delta_ts', + 'Release' => 'version', + 'Severity' => 'bug_severity', + 'Description' => 'comment', }; use constant VALUE_MAP => { - bug_severity => { - 'serious' => 'major', - 'cosmetic' => 'trivial', - 'new-feature' => 'enhancement', - 'non-critical' => 'normal', - }, - bug_status => { - 'open' => 'CONFIRMED', - 'analyzed' => 'IN_PROGRESS', - 'suspended' => 'RESOLVED', - 'feedback' => 'RESOLVED', - 'released' => 'VERIFIED', - }, - bug_status_resolution => { - 'feedback' => 'FIXED', - 'released' => 'FIXED', - 'closed' => 'FIXED', - 'suspended' => 'LATER', - }, - priority => { - 'medium' => 'Normal', - }, + bug_severity => { + 'serious' => 'major', + 'cosmetic' => 'trivial', + 'new-feature' => 'enhancement', + 'non-critical' => 'normal', + }, + bug_status => { + 'open' => 'CONFIRMED', + 'analyzed' => 'IN_PROGRESS', + 'suspended' => 'RESOLVED', + 'feedback' => 'RESOLVED', + 'released' => 'VERIFIED', + }, + bug_status_resolution => { + 'feedback' => 'FIXED', + 'released' => 'FIXED', + 'closed' => 'FIXED', + 'suspended' => 'LATER', + }, + priority => {'medium' => 'Normal',}, }; use constant GNATS_CONFIG_VARS => ( - { - name => 'gnats_path', - default => '/var/lib/gnats', - desc => < 'gnats_path', + default => '/var/lib/gnats', + desc => < 'default_email_domain', - default => 'example.com', - desc => <<'END', + }, + { + name => 'default_email_domain', + default => 'example.com', + desc => <<'END', # Some GNATS users do not have full email addresses, but Bugzilla requires # every user to have an email address. What domain should be appended to # usernames that don't have emails, to make them into email addresses? # (For example, if you leave this at the default, "unknown" would become # "unknown@example.com".) END - }, - { - name => 'component_name', - default => 'General', - desc => <<'END', + }, + { + name => 'component_name', + default => 'General', + desc => <<'END', # GNATS has only "Category" to classify bugs. However, Bugzilla has a # multi-level system of Products that contain Components. When importing # GNATS categories, they become a Product with one Component. What should # the name of that Component be? END - }, - { - name => 'version_regex', - default => '', - desc => <<'END', + }, + { + name => 'version_regex', + default => '', + desc => <<'END', # In GNATS, the "version" field can contain almost anything. However, in # Bugzilla, it's a drop-down, so you don't want too many choices in there. # If you specify a regular expression here, versions will be tested against @@ -115,43 +114,43 @@ END # as the version value for the bug instead of the full version value specified # in GNATS. END - }, - { - name => 'default_originator', - default => 'gnats-admin', - desc => <<'END', + }, + { + name => 'default_originator', + default => 'gnats-admin', + desc => <<'END', # Sometimes, a PR has no valid Originator, so we fall back to the From # header of the email. If the From header also isn't a valid username # (is just a name with spaces in it--we can't convert that to an email # address) then this username (which can either be a GNATS username or an # email address) will be considered to be the Originator of the PR. END - } + } ); sub CONFIG_VARS { - my $self = shift; - my @vars = (GNATS_CONFIG_VARS, $self->SUPER::CONFIG_VARS); - my $field_map = first { $_->{name} eq 'translate_fields' } @vars; - $field_map->{default} = FIELD_MAP; - my $value_map = first { $_->{name} eq 'translate_values' } @vars; - $value_map->{default} = VALUE_MAP; - return @vars; + my $self = shift; + my @vars = (GNATS_CONFIG_VARS, $self->SUPER::CONFIG_VARS); + my $field_map = first { $_->{name} eq 'translate_fields' } @vars; + $field_map->{default} = FIELD_MAP; + my $value_map = first { $_->{name} eq 'translate_values' } @vars; + $value_map->{default} = VALUE_MAP; + return @vars; } # Directories that aren't projects, or that we shouldn't be parsing use constant SKIP_DIRECTORIES => qw( - gnats-adm - gnats-queue - pending + gnats-adm + gnats-queue + pending ); use constant NON_COMMENT_FIELDS => qw( - Audit-Trail - Closed-Date - Confidential - Unformatted - attachments + Audit-Trail + Closed-Date + Confidential + Unformatted + attachments ); # Certain fields can contain things that look like fields in them, @@ -160,20 +159,16 @@ use constant NON_COMMENT_FIELDS => qw( # and wait for the next field to consider that we actually have # a field to parse. use constant END_FIELD_ORDER => qw( - Description - How-To-Repeat - Fix - Release-Note - Audit-Trail - Unformatted + Description + How-To-Repeat + Fix + Release-Note + Audit-Trail + Unformatted ); -use constant CUSTOM_FIELDS => { - cf_type => { - type => FIELD_TYPE_SINGLE_SELECT, - description => 'Type', - }, -}; +use constant CUSTOM_FIELDS => + {cf_type => {type => FIELD_TYPE_SINGLE_SELECT, description => 'Type',},}; use constant FIELD_REGEX => qr/^>(\S+):\s*(.*)$/; @@ -192,24 +187,24 @@ use constant LONG_VERSION_LENGTH => 32; ######### sub before_insert { - my $self = shift; - - # gnats_id isn't a valid User::create field, and we don't need it - # anymore now. - delete $_->{gnats_id} foreach @{ $self->users }; - - # Grab a version out of a bug for each product, so that there is a - # valid "version" argument for Bugzilla::Product->create. - foreach my $product (@{ $self->products }) { - my $bug = first { $_->{product} eq $product->{name} and $_->{version} } - @{ $self->bugs }; - if (defined $bug) { - $product->{version} = $bug->{version}; - } - else { - $product->{version} = 'unspecified'; - } + my $self = shift; + + # gnats_id isn't a valid User::create field, and we don't need it + # anymore now. + delete $_->{gnats_id} foreach @{$self->users}; + + # Grab a version out of a bug for each product, so that there is a + # valid "version" argument for Bugzilla::Product->create. + foreach my $product (@{$self->products}) { + my $bug = first { $_->{product} eq $product->{name} and $_->{version} } + @{$self->bugs}; + if (defined $bug) { + $product->{version} = $bug->{version}; + } + else { + $product->{version} = 'unspecified'; } + } } ######### @@ -217,53 +212,53 @@ sub before_insert { ######### sub _read_users { - my $self = shift; - my $path = $self->config('gnats_path'); - my $file = "$path/gnats-adm/responsible"; - $self->debug("Reading users from $file"); - my $default_domain = $self->config('default_email_domain'); - open(my $users_fh, '<', $file) || die "$file: $!"; - my @users; - foreach my $line (<$users_fh>) { - $line = trim($line); - next if $line =~ /^#/; - my ($id, $name, $email) = split(':', $line, 3); - $email ||= "$id\@$default_domain"; - # We can't call our own translate_value, because that depends on - # the existence of user_map, which doesn't exist until after - # this method. However, we still want to translate any users found. - $email = $self->SUPER::translate_value('user', $email); - push(@users, { realname => $name, login_name => $email, - gnats_id => $id }); - } - close($users_fh); - return \@users; + my $self = shift; + my $path = $self->config('gnats_path'); + my $file = "$path/gnats-adm/responsible"; + $self->debug("Reading users from $file"); + my $default_domain = $self->config('default_email_domain'); + open(my $users_fh, '<', $file) || die "$file: $!"; + my @users; + foreach my $line (<$users_fh>) { + $line = trim($line); + next if $line =~ /^#/; + my ($id, $name, $email) = split(':', $line, 3); + $email ||= "$id\@$default_domain"; + + # We can't call our own translate_value, because that depends on + # the existence of user_map, which doesn't exist until after + # this method. However, we still want to translate any users found. + $email = $self->SUPER::translate_value('user', $email); + push(@users, {realname => $name, login_name => $email, gnats_id => $id}); + } + close($users_fh); + return \@users; } sub user_map { - my $self = shift; - $self->{user_map} ||= { map { $_->{gnats_id} => $_->{login_name} } - @{ $self->users } }; - return $self->{user_map}; + my $self = shift; + $self->{user_map} + ||= {map { $_->{gnats_id} => $_->{login_name} } @{$self->users}}; + return $self->{user_map}; } sub add_user { - my ($self, $id, $email) = @_; - return if defined $self->user_map->{$id}; - $self->user_map->{$id} = $email; - push(@{ $self->users }, { login_name => $email, gnats_id => $id }); + my ($self, $id, $email) = @_; + return if defined $self->user_map->{$id}; + $self->user_map->{$id} = $email; + push(@{$self->users}, {login_name => $email, gnats_id => $id}); } sub user_to_email { - my ($self, $value) = @_; - if (defined $self->user_map->{$value}) { - $value = $self->user_map->{$value}; - } - elsif ($value !~ /@/) { - my $domain = $self->config('default_email_domain'); - $value = "$value\@$domain"; - } - return $value; + my ($self, $value) = @_; + if (defined $self->user_map->{$value}) { + $value = $self->user_map->{$value}; + } + elsif ($value !~ /@/) { + my $domain = $self->config('default_email_domain'); + $value = "$value\@$domain"; + } + return $value; } ############ @@ -271,31 +266,33 @@ sub user_to_email { ############ sub _read_products { - my $self = shift; - my $path = $self->config('gnats_path'); - my $file = "$path/gnats-adm/categories"; - $self->debug("Reading categories from $file"); - - open(my $categories_fh, '<', $file) || die "$file: $!"; - my @products; - foreach my $line (<$categories_fh>) { - $line = trim($line); - next if $line =~ /^#/; - my ($name, $description, $assigned_to, $cc) = split(':', $line, 4); - my %product = ( name => $name, description => $description ); - - my @initial_cc = split(',', $cc); - @initial_cc = @{ $self->translate_value('user', \@initial_cc) }; - $assigned_to = $self->translate_value('user', $assigned_to); - my %component = ( name => $self->config('component_name'), - description => $description, - initialowner => $assigned_to, - initial_cc => \@initial_cc ); - $product{components} = [\%component]; - push(@products, \%product); - } - close($categories_fh); - return \@products; + my $self = shift; + my $path = $self->config('gnats_path'); + my $file = "$path/gnats-adm/categories"; + $self->debug("Reading categories from $file"); + + open(my $categories_fh, '<', $file) || die "$file: $!"; + my @products; + foreach my $line (<$categories_fh>) { + $line = trim($line); + next if $line =~ /^#/; + my ($name, $description, $assigned_to, $cc) = split(':', $line, 4); + my %product = (name => $name, description => $description); + + my @initial_cc = split(',', $cc); + @initial_cc = @{$self->translate_value('user', \@initial_cc)}; + $assigned_to = $self->translate_value('user', $assigned_to); + my %component = ( + name => $self->config('component_name'), + description => $description, + initialowner => $assigned_to, + initial_cc => \@initial_cc + ); + $product{components} = [\%component]; + push(@products, \%product); + } + close($categories_fh); + return \@products; } ################ @@ -303,128 +300,131 @@ sub _read_products { ################ sub _read_bugs { - my $self = shift; - my $path = $self->config('gnats_path'); - my @directories = glob("$path/*"); - my @bugs; - foreach my $directory (@directories) { - next if !-d $directory; - my $name = basename($directory); - next if grep($_ eq $name, SKIP_DIRECTORIES); - push(@bugs, @{ $self->_parse_project($directory) }); - } - @bugs = sort { $a->{Number} <=> $b->{Number} } @bugs; - return \@bugs; + my $self = shift; + my $path = $self->config('gnats_path'); + my @directories = glob("$path/*"); + my @bugs; + foreach my $directory (@directories) { + next if !-d $directory; + my $name = basename($directory); + next if grep($_ eq $name, SKIP_DIRECTORIES); + push(@bugs, @{$self->_parse_project($directory)}); + } + @bugs = sort { $a->{Number} <=> $b->{Number} } @bugs; + return \@bugs; } sub _parse_project { - my ($self, $directory) = @_; - my @files = glob("$directory/*"); - - $self->debug("Reading Project: $directory"); - # Sometimes other files get into gnats directories. - @files = grep { basename($_) =~ /^\d+$/ } @files; - my @bugs; - my $count = 1; - my $total = scalar @files; - print basename($directory) . ":\n"; - foreach my $file (@files) { - push(@bugs, $self->_parse_bug_file($file)); - if (!$self->verbose) { - indicate_progress({ current => $count++, every => 5, - total => $total }); - } + my ($self, $directory) = @_; + my @files = glob("$directory/*"); + + $self->debug("Reading Project: $directory"); + + # Sometimes other files get into gnats directories. + @files = grep { basename($_) =~ /^\d+$/ } @files; + my @bugs; + my $count = 1; + my $total = scalar @files; + print basename($directory) . ":\n"; + foreach my $file (@files) { + push(@bugs, $self->_parse_bug_file($file)); + if (!$self->verbose) { + indicate_progress({current => $count++, every => 5, total => $total}); } - return \@bugs; + } + return \@bugs; } sub _parse_bug_file { - my ($self, $file) = @_; - $self->debug("Reading $file"); - open(my $fh, "<", $file) || die "$file: $!"; - my $email = Email::Simple::FromHandle->new($fh); - my $fields = $self->_get_gnats_field_data($email); - # We parse attachments here instead of during translate_bug, - # because otherwise we'd be taking up huge amounts of memory storing - # all the raw attachment data in memory. - $fields->{attachments} = $self->_parse_attachments($fields); - close($fh); - return $fields; + my ($self, $file) = @_; + $self->debug("Reading $file"); + open(my $fh, "<", $file) || die "$file: $!"; + my $email = Email::Simple::FromHandle->new($fh); + my $fields = $self->_get_gnats_field_data($email); + + # We parse attachments here instead of during translate_bug, + # because otherwise we'd be taking up huge amounts of memory storing + # all the raw attachment data in memory. + $fields->{attachments} = $self->_parse_attachments($fields); + close($fh); + return $fields; } sub _get_gnats_field_data { - my ($self, $email) = @_; - my ($current_field, @value_lines, %fields); - $email->reset_handle(); - my $handle = $email->handle; - foreach my $line (<$handle>) { - # If this line starts a field name - if ($line =~ FIELD_REGEX) { - my ($new_field, $rest_of_line) = ($1, $2); - - # If this is one of the last few PR fields, then make sure - # that we're getting our fields in the right order. - my $new_field_valid = 1; - my $search_for = $current_field || ''; - my $current_field_pos = firstidx { $_ eq $search_for } - END_FIELD_ORDER; - if ($current_field_pos > -1) { - my $new_field_pos = firstidx { $_ eq $new_field } - END_FIELD_ORDER; - # We accept any field, as long as it's later than this one. - $new_field_valid = $new_field_pos > $current_field_pos ? 1 : 0; - } - - if ($new_field_valid) { - if ($current_field) { - $fields{$current_field} = _handle_lines(\@value_lines); - @value_lines = (); - } - $current_field = $new_field; - $line = $rest_of_line; - } + my ($self, $email) = @_; + my ($current_field, @value_lines, %fields); + $email->reset_handle(); + my $handle = $email->handle; + foreach my $line (<$handle>) { + + # If this line starts a field name + if ($line =~ FIELD_REGEX) { + my ($new_field, $rest_of_line) = ($1, $2); + + # If this is one of the last few PR fields, then make sure + # that we're getting our fields in the right order. + my $new_field_valid = 1; + my $search_for = $current_field || ''; + my $current_field_pos = firstidx { $_ eq $search_for } + END_FIELD_ORDER; + if ($current_field_pos > -1) { + my $new_field_pos = firstidx { $_ eq $new_field } + END_FIELD_ORDER; + + # We accept any field, as long as it's later than this one. + $new_field_valid = $new_field_pos > $current_field_pos ? 1 : 0; + } + + if ($new_field_valid) { + if ($current_field) { + $fields{$current_field} = _handle_lines(\@value_lines); + @value_lines = (); } - push(@value_lines, $line) if defined $line; + $current_field = $new_field; + $line = $rest_of_line; + } } - $fields{$current_field} = _handle_lines(\@value_lines); - $fields{cc} = [$email->header('Cc')] if $email->header('Cc'); - - # If the Originator is invalid and we don't have a translation for it, - # use the From header instead. - my $originator = $self->translate_value('reporter', $fields{Originator}, - { check_only => 1 }); - if ($originator !~ Bugzilla->params->{emailregexp}) { - # We use the raw header sometimes, because it looks like "From: user" - # which Email::Address won't parse but we can still use. - my $address = $email->header('From'); - my ($parsed) = Email::Address->parse($address); - if ($parsed) { - $address = $parsed->address; - } - if ($address) { - $self->debug( - "PR $fields{Number} had an Originator that was not a valid" - . " user ($fields{Originator}). Using From ($address)" - . " instead.\n"); - my $address_email = $self->translate_value('reporter', $address, - { check_only => 1 }); - if ($address_email !~ Bugzilla->params->{emailregexp}) { - $self->debug(" From was also invalid, using default_originator.\n"); - $address = $self->config('default_originator'); - } - $fields{Originator} = $address; - } + push(@value_lines, $line) if defined $line; + } + $fields{$current_field} = _handle_lines(\@value_lines); + $fields{cc} = [$email->header('Cc')] if $email->header('Cc'); + + # If the Originator is invalid and we don't have a translation for it, + # use the From header instead. + my $originator + = $self->translate_value('reporter', $fields{Originator}, {check_only => 1}); + if ($originator !~ Bugzilla->params->{emailregexp}) { + + # We use the raw header sometimes, because it looks like "From: user" + # which Email::Address won't parse but we can still use. + my $address = $email->header('From'); + my ($parsed) = Email::Address->parse($address); + if ($parsed) { + $address = $parsed->address; + } + if ($address) { + $self->debug("PR $fields{Number} had an Originator that was not a valid" + . " user ($fields{Originator}). Using From ($address)" + . " instead.\n"); + my $address_email + = $self->translate_value('reporter', $address, {check_only => 1}); + if ($address_email !~ Bugzilla->params->{emailregexp}) { + $self->debug(" From was also invalid, using default_originator.\n"); + $address = $self->config('default_originator'); + } + $fields{Originator} = $address; } + } - $self->debug(\%fields, 3); - return \%fields; + $self->debug(\%fields, 3); + return \%fields; } sub _handle_lines { - my ($lines) = @_; - my $value = join('', @$lines); - $value =~ s/\s+$//; - return $value; + my ($lines) = @_; + my $value = join('', @$lines); + $value =~ s/\s+$//; + return $value; } #################### @@ -432,169 +432,188 @@ sub _handle_lines { #################### sub translate_bug { - my ($self, $fields) = @_; + my ($self, $fields) = @_; - my ($bug, $other_fields) = $self->SUPER::translate_bug($fields); + my ($bug, $other_fields) = $self->SUPER::translate_bug($fields); - $bug->{attachments} = delete $other_fields->{attachments}; + $bug->{attachments} = delete $other_fields->{attachments}; - if (defined $other_fields->{_add_to_comment}) { - $bug->{comment} .= delete $other_fields->{_add_to_comment}; - } + if (defined $other_fields->{_add_to_comment}) { + $bug->{comment} .= delete $other_fields->{_add_to_comment}; + } - my ($changes, $extra_comment) = - $self->_parse_audit_trail($bug, $other_fields->{'Audit-Trail'}); - - my @comments; - foreach my $change (@$changes) { - if (exists $change->{comment}) { - push(@comments, { - thetext => $change->{comment}, - who => $change->{who}, - bug_when => $change->{bug_when} }); - delete $change->{comment}; - } - } - $bug->{history} = $changes; + my ($changes, $extra_comment) + = $self->_parse_audit_trail($bug, $other_fields->{'Audit-Trail'}); - if (trim($extra_comment)) { - push(@comments, { thetext => $extra_comment, who => $bug->{reporter}, - bug_when => $bug->{delta_ts} || $bug->{creation_ts} }); - } - $bug->{comments} = \@comments; - - $bug->{component} = $self->config('component_name'); - if (!$bug->{short_desc}) { - $bug->{short_desc} = NO_SUBJECT; - } - - foreach my $attachment (@{ $bug->{attachments} || [] }) { - $attachment->{submitter} = $bug->{reporter}; - $attachment->{creation_ts} = $bug->{creation_ts}; + my @comments; + foreach my $change (@$changes) { + if (exists $change->{comment}) { + push( + @comments, + { + thetext => $change->{comment}, + who => $change->{who}, + bug_when => $change->{bug_when} + } + ); + delete $change->{comment}; } - - $self->debug($bug, 3); - return $bug; + } + $bug->{history} = $changes; + + if (trim($extra_comment)) { + push( + @comments, + { + thetext => $extra_comment, + who => $bug->{reporter}, + bug_when => $bug->{delta_ts} || $bug->{creation_ts} + } + ); + } + $bug->{comments} = \@comments; + + $bug->{component} = $self->config('component_name'); + if (!$bug->{short_desc}) { + $bug->{short_desc} = NO_SUBJECT; + } + + foreach my $attachment (@{$bug->{attachments} || []}) { + $attachment->{submitter} = $bug->{reporter}; + $attachment->{creation_ts} = $bug->{creation_ts}; + } + + $self->debug($bug, 3); + return $bug; } sub _parse_audit_trail { - my ($self, $bug, $audit_trail) = @_; - return [] if !trim($audit_trail); - $self->debug(" Parsing audit trail...", 2); - - if ($audit_trail !~ /^\S+-Changed-\S+:/ms) { - # This is just a comment from the bug's creator. - $self->debug(" Audit trail is just a comment.", 2); - return ([], $audit_trail); - } - - my (@changes, %current_data, $current_column, $on_why); - my $extra_comment = ''; - my $current_field; - my @all_lines = split("\n", $audit_trail); - foreach my $line (@all_lines) { - # GNATS history looks like: - # Status-Changed-From-To: open->closed - # Status-Changed-By: jack - # Status-Changed-When: Mon May 12 14:46:59 2003 - # Status-Changed-Why: - # This is some comment here about the change. - if ($line =~ /^(\S+)-Changed-(\S+):(.*)/) { - my ($field, $column, $value) = ($1, $2, $3); - my $bz_field = $self->translate_field($field); - # If it's not a field we're importing, we don't care about - # its history. - next if !$bz_field; - # GNATS doesn't track values for description changes, - # unfortunately, and that's the only information we'd be able to - # use in Bugzilla for the audit trail on that field. - next if $bz_field eq 'comment'; - $current_field = $bz_field if !$current_field; - if ($bz_field ne $current_field) { - $self->_store_audit_change( - \@changes, $current_field, \%current_data); - %current_data = (); - $current_field = $bz_field; - } - $value = trim($value); - $self->debug(" $bz_field $column: $value", 3); - if ($column eq 'From-To') { - my ($from, $to) = split('->', $value, 2); - # Sometimes there's just a - instead of a -> between the values. - if (!defined($to)) { - ($from, $to) = split('-', $value, 2); - } - $current_data{added} = $to; - $current_data{removed} = $from; - } - elsif ($column eq 'By') { - my $email = $self->translate_value('user', $value); - # Sometimes we hit users in the audit trail that we haven't - # seen anywhere else. - $current_data{who} = $email; - } - elsif ($column eq 'When') { - $current_data{bug_when} = $self->parse_date($value); - } - if ($column eq 'Why') { - $value = '' if !defined $value; - $current_data{comment} = $value; - $on_why = 1; - } - else { - $on_why = 0; - } - } - elsif ($on_why) { - # "Why" lines are indented four characters. - $line =~ s/^\s{4}//; - $current_data{comment} .= "$line\n"; - } - else { - $self->debug( - "Extra Audit-Trail line on $bug->{product} $bug->{bug_id}:" - . " $line\n", 2); - $extra_comment .= "$line\n"; + my ($self, $bug, $audit_trail) = @_; + return [] if !trim($audit_trail); + $self->debug(" Parsing audit trail...", 2); + + if ($audit_trail !~ /^\S+-Changed-\S+:/ms) { + + # This is just a comment from the bug's creator. + $self->debug(" Audit trail is just a comment.", 2); + return ([], $audit_trail); + } + + my (@changes, %current_data, $current_column, $on_why); + my $extra_comment = ''; + my $current_field; + my @all_lines = split("\n", $audit_trail); + foreach my $line (@all_lines) { + + # GNATS history looks like: + # Status-Changed-From-To: open->closed + # Status-Changed-By: jack + # Status-Changed-When: Mon May 12 14:46:59 2003 + # Status-Changed-Why: + # This is some comment here about the change. + if ($line =~ /^(\S+)-Changed-(\S+):(.*)/) { + my ($field, $column, $value) = ($1, $2, $3); + my $bz_field = $self->translate_field($field); + + # If it's not a field we're importing, we don't care about + # its history. + next if !$bz_field; + + # GNATS doesn't track values for description changes, + # unfortunately, and that's the only information we'd be able to + # use in Bugzilla for the audit trail on that field. + next if $bz_field eq 'comment'; + $current_field = $bz_field if !$current_field; + if ($bz_field ne $current_field) { + $self->_store_audit_change(\@changes, $current_field, \%current_data); + %current_data = (); + $current_field = $bz_field; + } + $value = trim($value); + $self->debug(" $bz_field $column: $value", 3); + if ($column eq 'From-To') { + my ($from, $to) = split('->', $value, 2); + + # Sometimes there's just a - instead of a -> between the values. + if (!defined($to)) { + ($from, $to) = split('-', $value, 2); } + $current_data{added} = $to; + $current_data{removed} = $from; + } + elsif ($column eq 'By') { + my $email = $self->translate_value('user', $value); + + # Sometimes we hit users in the audit trail that we haven't + # seen anywhere else. + $current_data{who} = $email; + } + elsif ($column eq 'When') { + $current_data{bug_when} = $self->parse_date($value); + } + if ($column eq 'Why') { + $value = '' if !defined $value; + $current_data{comment} = $value; + $on_why = 1; + } + else { + $on_why = 0; + } + } + elsif ($on_why) { + + # "Why" lines are indented four characters. + $line =~ s/^\s{4}//; + $current_data{comment} .= "$line\n"; + } + else { + $self->debug( + "Extra Audit-Trail line on $bug->{product} $bug->{bug_id}:" . " $line\n", 2); + $extra_comment .= "$line\n"; } - $self->_store_audit_change(\@changes, $current_field, \%current_data); - return (\@changes, $extra_comment); + } + $self->_store_audit_change(\@changes, $current_field, \%current_data); + return (\@changes, $extra_comment); } sub _store_audit_change { - my ($self, $changes, $old_field, $current_data) = @_; - - $current_data->{field} = $old_field; - $current_data->{removed} = - $self->translate_value($old_field, $current_data->{removed}); - $current_data->{added} = - $self->translate_value($old_field, $current_data->{added}); - push(@$changes, { %$current_data }); + my ($self, $changes, $old_field, $current_data) = @_; + + $current_data->{field} = $old_field; + $current_data->{removed} + = $self->translate_value($old_field, $current_data->{removed}); + $current_data->{added} + = $self->translate_value($old_field, $current_data->{added}); + push(@$changes, {%$current_data}); } sub _parse_attachments { - my ($self, $fields) = @_; - my $unformatted = delete $fields->{'Unformatted'}; - my $gnats_boundary = GNATS_BOUNDARY; - # A sanity checker to make sure that we're parsing attachments right. - my $num_attachments = 0; - $num_attachments++ while ($unformatted =~ /\Q$gnats_boundary\E/g); - # Sometimes there's a GNATS_BOUNDARY that is on the same line as other data. - $unformatted =~ s/(\S\s*)\Q$gnats_boundary\E$/$1\n$gnats_boundary/mg; - # Often the "Unformatted" section starts with stuff before - # ----gnatsweb-attachment---- that isn't necessary. - $unformatted =~ s/^\s*From:.+?Reply-to:[^\n]+//s; - $unformatted = trim($unformatted); - return [] if !$unformatted; - $self->debug('Reading attachments...', 2); - my $boundary = generate_random_password(48); - $unformatted =~ s/\Q$gnats_boundary\E/--$boundary/g; - # Sometimes the whole Unformatted section is indented by exactly - # one space, and needs to be fixed. - if ($unformatted =~ /--\Q$boundary\E\n /) { - $unformatted =~ s/^ //mg; - } - $unformatted = <{'Unformatted'}; + my $gnats_boundary = GNATS_BOUNDARY; + + # A sanity checker to make sure that we're parsing attachments right. + my $num_attachments = 0; + $num_attachments++ while ($unformatted =~ /\Q$gnats_boundary\E/g); + + # Sometimes there's a GNATS_BOUNDARY that is on the same line as other data. + $unformatted =~ s/(\S\s*)\Q$gnats_boundary\E$/$1\n$gnats_boundary/mg; + + # Often the "Unformatted" section starts with stuff before + # ----gnatsweb-attachment---- that isn't necessary. + $unformatted =~ s/^\s*From:.+?Reply-to:[^\n]+//s; + $unformatted = trim($unformatted); + return [] if !$unformatted; + $self->debug('Reading attachments...', 2); + my $boundary = generate_random_password(48); + $unformatted =~ s/\Q$gnats_boundary\E/--$boundary/g; + + # Sometimes the whole Unformatted section is indented by exactly + # one space, and needs to be fixed. + if ($unformatted =~ /--\Q$boundary\E\n /) { + $unformatted =~ s/^ //mg; + } + $unformatted = <parts; - # Remove the fake body. - my $part1 = shift @parts; - if ($part1->body) { - $self->debug(" Additional Unformatted data found on " - . $fields->{Category} . " bug " . $fields->{Number}); - $self->debug($part1->body, 3); - $fields->{_add_comment} .= "\n\nUnformatted:\n" . $part1->body; - } + my $email = new Email::MIME(\$unformatted); + my @parts = $email->parts; + + # Remove the fake body. + my $part1 = shift @parts; + if ($part1->body) { + $self->debug(" Additional Unformatted data found on " + . $fields->{Category} . " bug " + . $fields->{Number}); + $self->debug($part1->body, 3); + $fields->{_add_comment} .= "\n\nUnformatted:\n" . $part1->body; + } + + my @attachments; + foreach my $part (@parts) { + $self->debug(' Parsing attachment: ' . $part->filename); + my $temp_fh = IO::File->new_tmpfile or die("Can't create tempfile: $!"); + $temp_fh->binmode; + print $temp_fh $part->body; + my $content_type = $part->content_type; + $content_type =~ s/; name=.+$//; + my $attachment = { + filename => $part->filename, + description => $part->filename, + mimetype => $content_type, + data => $temp_fh + }; + $self->debug($attachment, 3); + push(@attachments, $attachment); + } + + if (scalar(@attachments) ne $num_attachments) { + warn "WARNING: Expected $num_attachments attachments but got " + . scalar(@attachments) . "\n"; + $self->debug($unformatted, 3); + } + return \@attachments; +} - my @attachments; - foreach my $part (@parts) { - $self->debug(' Parsing attachment: ' . $part->filename); - my $temp_fh = IO::File->new_tmpfile or die ("Can't create tempfile: $!"); - $temp_fh->binmode; - print $temp_fh $part->body; - my $content_type = $part->content_type; - $content_type =~ s/; name=.+$//; - my $attachment = { filename => $part->filename, - description => $part->filename, - mimetype => $content_type, - data => $temp_fh }; - $self->debug($attachment, 3); - push(@attachments, $attachment); +sub translate_value { + my $self = shift; + my ($field, $value, $options) = @_; + my $original_value = $value; + $options ||= {}; + + if (!ref($value) and grep($_ eq $field, $self->USER_FIELDS)) { + if ($value =~ /(\S+\@\S+)/) { + $value = $1; + $value =~ s/^$//; } - - if (scalar(@attachments) ne $num_attachments) { - warn "WARNING: Expected $num_attachments attachments but got " - . scalar(@attachments) . "\n" ; - $self->debug($unformatted, 3); + else { + # Sometimes names have extra stuff on the end like "(Somebody's Name)" + $value =~ s/\s+\(.+\)$//; + + # Sometimes user fields look like "(user)" instead of just "user". + $value =~ s/^\((.+)\)$/$1/; + $value = trim($value); } - return \@attachments; -} + } -sub translate_value { - my $self = shift; - my ($field, $value, $options) = @_; - my $original_value = $value; - $options ||= {}; - - if (!ref($value) and grep($_ eq $field, $self->USER_FIELDS)) { - if ($value =~ /(\S+\@\S+)/) { - $value = $1; - $value =~ s/^$//; - } - else { - # Sometimes names have extra stuff on the end like "(Somebody's Name)" - $value =~ s/\s+\(.+\)$//; - # Sometimes user fields look like "(user)" instead of just "user". - $value =~ s/^\((.+)\)$/$1/; - $value = trim($value); - } + if ($field eq 'version' and $value ne '') { + my $version_re = $self->config('version_regex'); + if ($version_re and $value =~ $version_re) { + $value = $1; } - if ($field eq 'version' and $value ne '') { - my $version_re = $self->config('version_regex'); - if ($version_re and $value =~ $version_re) { - $value = $1; - } - # In the GNATS that I tested this with, there were many extremely long - # values for "version" that caused some import problems (they were - # longer than the max allowed version value). So if the version value - # is longer than 32 characters, pull out the first thing that looks - # like a version number. - elsif (length($value) > LONG_VERSION_LENGTH) { - $value =~ s/^.+?\b(\d[\w\.]+)\b.+$/$1/; - } + # In the GNATS that I tested this with, there were many extremely long + # values for "version" that caused some import problems (they were + # longer than the max allowed version value). So if the version value + # is longer than 32 characters, pull out the first thing that looks + # like a version number. + elsif (length($value) > LONG_VERSION_LENGTH) { + $value =~ s/^.+?\b(\d[\w\.]+)\b.+$/$1/; } - - my @args = @_; + } + + my @args = @_; + $args[1] = $value; + + $value = $self->SUPER::translate_value(@args); + return $value if ref $value; + + if (grep($_ eq $field, $self->USER_FIELDS)) { + my $from_value = $value; + $value = $self->user_to_email($value); $args[1] = $value; - + + # If we got something new from user_to_email, do any necessary + # translation of it. $value = $self->SUPER::translate_value(@args); - return $value if ref $value; - - if (grep($_ eq $field, $self->USER_FIELDS)) { - my $from_value = $value; - $value = $self->user_to_email($value); - $args[1] = $value; - # If we got something new from user_to_email, do any necessary - # translation of it. - $value = $self->SUPER::translate_value(@args); - if (!$options->{check_only}) { - $self->add_user($from_value, $value); - } + if (!$options->{check_only}) { + $self->add_user($from_value, $value); } - - return $value; + } + + return $value; } 1; diff --git a/Bugzilla/Milestone.pm b/Bugzilla/Milestone.pm index cf7e3e35f..be49df536 100644 --- a/Bugzilla/Milestone.pm +++ b/Bugzilla/Milestone.pm @@ -25,140 +25,140 @@ use Scalar::Util qw(blessed); use constant DEFAULT_SORTKEY => 0; -use constant DB_TABLE => 'milestones'; +use constant DB_TABLE => 'milestones'; use constant NAME_FIELD => 'value'; use constant LIST_ORDER => 'sortkey, value'; use constant DB_COLUMNS => qw( - id - value - product_id - sortkey - isactive + id + value + product_id + sortkey + isactive ); -use constant REQUIRED_FIELD_MAP => { - product_id => 'product', -}; +use constant REQUIRED_FIELD_MAP => {product_id => 'product',}; use constant UPDATE_COLUMNS => qw( - value - sortkey - isactive + value + sortkey + isactive ); use constant VALIDATORS => { - product => \&_check_product, - sortkey => \&_check_sortkey, - value => \&_check_value, - isactive => \&Bugzilla::Object::check_boolean, + product => \&_check_product, + sortkey => \&_check_sortkey, + value => \&_check_value, + isactive => \&Bugzilla::Object::check_boolean, }; -use constant VALIDATOR_DEPENDENCIES => { - value => ['product'], -}; +use constant VALIDATOR_DEPENDENCIES => {value => ['product'],}; ################################ sub new { - my $class = shift; - my $param = shift; - my $dbh = Bugzilla->dbh; - - my $product; - if (ref $param and !defined $param->{id}) { - $product = $param->{product}; - my $name = $param->{name}; - if (!defined $product) { - ThrowCodeError('bad_arg', - {argument => 'product', - function => "${class}::new"}); - } - if (!defined $name) { - ThrowCodeError('bad_arg', - {argument => 'name', - function => "${class}::new"}); - } - - my $condition = 'product_id = ? AND value = ?'; - my @values = ($product->id, $name); - $param = { condition => $condition, values => \@values }; + my $class = shift; + my $param = shift; + my $dbh = Bugzilla->dbh; + + my $product; + if (ref $param and !defined $param->{id}) { + $product = $param->{product}; + my $name = $param->{name}; + if (!defined $product) { + ThrowCodeError('bad_arg', {argument => 'product', function => "${class}::new"}); } + if (!defined $name) { + ThrowCodeError('bad_arg', {argument => 'name', function => "${class}::new"}); + } + + my $condition = 'product_id = ? AND value = ?'; + my @values = ($product->id, $name); + $param = {condition => $condition, values => \@values}; + } - unshift @_, $param; - return $class->SUPER::new(@_); + unshift @_, $param; + return $class->SUPER::new(@_); } sub run_create_validators { - my $class = shift; - my $params = $class->SUPER::run_create_validators(@_); - my $product = delete $params->{product}; - $params->{product_id} = $product->id; - return $params; + my $class = shift; + my $params = $class->SUPER::run_create_validators(@_); + my $product = delete $params->{product}; + $params->{product_id} = $product->id; + return $params; } sub update { - my $self = shift; - my $dbh = Bugzilla->dbh; - - $dbh->bz_start_transaction(); - my $changes = $self->SUPER::update(@_); - - if (exists $changes->{value}) { - # 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 = ?', - undef, ($self->name, $changes->{value}->[0], $self->product_id)); - - # The default milestone also stores the value instead of the ID. - $dbh->do('UPDATE products SET defaultmilestone = ? - WHERE id = ? AND defaultmilestone = ?', - undef, ($self->name, $self->product_id, $changes->{value}->[0])); - Bugzilla->memcached->clear({ table => 'products', id => $self->product_id }); - } - $dbh->bz_commit_transaction(); - Bugzilla->memcached->clear_config(); - - return $changes; + my $self = shift; + my $dbh = Bugzilla->dbh; + + $dbh->bz_start_transaction(); + my $changes = $self->SUPER::update(@_); + + if (exists $changes->{value}) { + + # 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 = ?', undef, + ($self->name, $changes->{value}->[0], $self->product_id) + ); + + # The default milestone also stores the value instead of the ID. + $dbh->do( + 'UPDATE products SET defaultmilestone = ? + WHERE id = ? AND defaultmilestone = ?', undef, + ($self->name, $self->product_id, $changes->{value}->[0]) + ); + Bugzilla->memcached->clear({table => 'products', id => $self->product_id}); + } + $dbh->bz_commit_transaction(); + Bugzilla->memcached->clear_config(); + + return $changes; } sub remove_from_db { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - $dbh->bz_start_transaction(); + $dbh->bz_start_transaction(); - # The default milestone cannot be deleted. - if ($self->name eq $self->product->default_milestone) { - ThrowUserError('milestone_is_default', { milestone => $self }); - } + # The default milestone cannot be deleted. + if ($self->name eq $self->product->default_milestone) { + ThrowUserError('milestone_is_default', {milestone => $self}); + } + + if ($self->bug_count) { - if ($self->bug_count) { - # We don't want to delete bugs when deleting a milestone. - # Bugs concerned are reassigned to the default milestone. - my $bug_ids = - $dbh->selectcol_arrayref('SELECT bug_id FROM bugs + # We don't want to delete bugs when deleting a milestone. + # Bugs concerned are reassigned to the default milestone. + my $bug_ids = $dbh->selectcol_arrayref( + 'SELECT bug_id FROM bugs WHERE product_id = ? AND target_milestone = ?', - undef, ($self->product->id, $self->name)); - - my $timestamp = $dbh->selectrow_array('SELECT NOW()'); - - $dbh->do('UPDATE bugs SET target_milestone = ?, delta_ts = ? - WHERE ' . $dbh->sql_in('bug_id', $bug_ids), - undef, ($self->product->default_milestone, $timestamp)); - - require Bugzilla::Bug; - import Bugzilla::Bug qw(LogActivityEntry); - foreach my $bug_id (@$bug_ids) { - LogActivityEntry($bug_id, 'target_milestone', - $self->name, - $self->product->default_milestone, - Bugzilla->user->id, $timestamp); - } + undef, ($self->product->id, $self->name) + ); + + my $timestamp = $dbh->selectrow_array('SELECT NOW()'); + + $dbh->do( + 'UPDATE bugs SET target_milestone = ?, delta_ts = ? + WHERE ' . $dbh->sql_in('bug_id', $bug_ids), undef, + ($self->product->default_milestone, $timestamp) + ); + + require Bugzilla::Bug; + import Bugzilla::Bug qw(LogActivityEntry); + foreach my $bug_id (@$bug_ids) { + LogActivityEntry($bug_id, 'target_milestone', $self->name, + $self->product->default_milestone, + Bugzilla->user->id, $timestamp); } - $self->SUPER::remove_from_db(); + } + $self->SUPER::remove_from_db(); - $dbh->bz_commit_transaction(); + $dbh->bz_commit_transaction(); } ################################ @@ -166,78 +166,84 @@ sub remove_from_db { ################################ sub _check_value { - my ($invocant, $name, undef, $params) = @_; - my $product = blessed($invocant) ? $invocant->product : $params->{product}; - - $name = trim($name); - $name || ThrowUserError('milestone_blank_name'); - if (length($name) > MAX_MILESTONE_SIZE) { - ThrowUserError('milestone_name_too_long', {name => $name}); - } - - my $milestone = new Bugzilla::Milestone({product => $product, name => $name}); - if ($milestone && (!ref $invocant || $milestone->id != $invocant->id)) { - ThrowUserError('milestone_already_exists', { name => $milestone->name, - product => $product->name }); - } - return $name; + my ($invocant, $name, undef, $params) = @_; + my $product = blessed($invocant) ? $invocant->product : $params->{product}; + + $name = trim($name); + $name || ThrowUserError('milestone_blank_name'); + if (length($name) > MAX_MILESTONE_SIZE) { + ThrowUserError('milestone_name_too_long', {name => $name}); + } + + my $milestone = new Bugzilla::Milestone({product => $product, name => $name}); + if ($milestone && (!ref $invocant || $milestone->id != $invocant->id)) { + ThrowUserError('milestone_already_exists', + {name => $milestone->name, product => $product->name}); + } + return $name; } sub _check_sortkey { - my ($invocant, $sortkey) = @_; - - # Keep a copy in case detaint_signed() clears the sortkey - my $stored_sortkey = $sortkey; - - if (!detaint_signed($sortkey) || $sortkey < MIN_SMALLINT || $sortkey > MAX_SMALLINT) { - ThrowUserError('milestone_sortkey_invalid', {sortkey => $stored_sortkey}); - } - return $sortkey; + my ($invocant, $sortkey) = @_; + + # Keep a copy in case detaint_signed() clears the sortkey + my $stored_sortkey = $sortkey; + + if ( !detaint_signed($sortkey) + || $sortkey < MIN_SMALLINT + || $sortkey > MAX_SMALLINT) + { + ThrowUserError('milestone_sortkey_invalid', {sortkey => $stored_sortkey}); + } + return $sortkey; } sub _check_product { - my ($invocant, $product) = @_; - $product || ThrowCodeError('param_required', - { function => "$invocant->create", param => "product" }); - return Bugzilla->user->check_can_admin_product($product->name); + my ($invocant, $product) = @_; + $product + || ThrowCodeError('param_required', + {function => "$invocant->create", param => "product"}); + return Bugzilla->user->check_can_admin_product($product->name); } ################################ # Methods ################################ -sub set_name { $_[0]->set('value', $_[1]); } -sub set_sortkey { $_[0]->set('sortkey', $_[1]); } +sub set_name { $_[0]->set('value', $_[1]); } +sub set_sortkey { $_[0]->set('sortkey', $_[1]); } sub set_is_active { $_[0]->set('isactive', $_[1]); } sub bug_count { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - if (!defined $self->{'bug_count'}) { - $self->{'bug_count'} = $dbh->selectrow_array(q{ + if (!defined $self->{'bug_count'}) { + $self->{'bug_count'} = $dbh->selectrow_array( + q{ SELECT COUNT(*) FROM bugs - WHERE product_id = ? AND target_milestone = ?}, - undef, $self->product_id, $self->name) || 0; - } - return $self->{'bug_count'}; + WHERE product_id = ? AND target_milestone = ?}, undef, $self->product_id, + $self->name + ) || 0; + } + return $self->{'bug_count'}; } ################################ ##### Accessors ###### ################################ -sub name { return $_[0]->{'value'}; } +sub name { return $_[0]->{'value'}; } sub product_id { return $_[0]->{'product_id'}; } -sub sortkey { return $_[0]->{'sortkey'}; } -sub is_active { return $_[0]->{'isactive'}; } +sub sortkey { return $_[0]->{'sortkey'}; } +sub is_active { return $_[0]->{'isactive'}; } sub product { - my $self = shift; + my $self = shift; - require Bugzilla::Product; - $self->{'product'} ||= new Bugzilla::Product($self->product_id); - return $self->{'product'}; + require Bugzilla::Product; + $self->{'product'} ||= new Bugzilla::Product($self->product_id); + return $self->{'product'}; } 1; diff --git a/Bugzilla/Object.pm b/Bugzilla/Object.pm index d43c8ca34..863eabc00 100644 --- a/Bugzilla/Object.pm +++ b/Bugzilla/Object.pm @@ -24,16 +24,17 @@ use constant NAME_FIELD => 'name'; use constant ID_FIELD => 'id'; use constant LIST_ORDER => NAME_FIELD; -use constant UPDATE_VALIDATORS => {}; -use constant NUMERIC_COLUMNS => (); -use constant DATE_COLUMNS => (); +use constant UPDATE_VALIDATORS => {}; +use constant NUMERIC_COLUMNS => (); +use constant DATE_COLUMNS => (); use constant VALIDATOR_DEPENDENCIES => {}; + # XXX At some point, this will be joined with FIELD_MAP. -use constant REQUIRED_FIELD_MAP => {}; +use constant REQUIRED_FIELD_MAP => {}; use constant EXTRA_REQUIRED_FIELDS => (); -use constant AUDIT_CREATES => 1; -use constant AUDIT_UPDATES => 1; -use constant AUDIT_REMOVES => 1; +use constant AUDIT_CREATES => 1; +use constant AUDIT_UPDATES => 1; +use constant AUDIT_REMOVES => 1; # When USE_MEMCACHED is true, the class is suitable for serialisation to # Memcached. See documentation in Bugzilla::Memcached for more information. @@ -47,54 +48,52 @@ use constant IS_CONFIG => 0; # This allows the JSON-RPC interface to return Bugzilla::Object instances # as though they were hashes. In the future, this may be modified to return # less information. -sub TO_JSON { return { %{ $_[0] } }; } +sub TO_JSON { return {%{$_[0]}}; } ############################### #### Initialization #### ############################### sub new { - my $invocant = shift; - my $class = ref($invocant) || $invocant; - my $param = shift; - - my $object = $class->_object_cache_get($param); - return $object if $object; - - my ($data, $set_memcached); - if (Bugzilla->memcached->enabled - && $class->USE_MEMCACHED - && ref($param) eq 'HASH' && $param->{cache}) - { - if (defined $param->{id}) { - $data = Bugzilla->memcached->get({ - table => $class->DB_TABLE, - id => $param->{id}, - }); - } - elsif (defined $param->{name}) { - $data = Bugzilla->memcached->get({ - table => $class->DB_TABLE, - name => $param->{name}, - }); - } - $set_memcached = $data ? 0 : 1; + my $invocant = shift; + my $class = ref($invocant) || $invocant; + my $param = shift; + + my $object = $class->_object_cache_get($param); + return $object if $object; + + my ($data, $set_memcached); + if ( Bugzilla->memcached->enabled + && $class->USE_MEMCACHED + && ref($param) eq 'HASH' + && $param->{cache}) + { + if (defined $param->{id}) { + $data + = Bugzilla->memcached->get({table => $class->DB_TABLE, id => $param->{id},}); } - $data ||= $class->_load_from_db($param); - - if ($data && $set_memcached) { - Bugzilla->memcached->set({ - table => $class->DB_TABLE, - id => $data->{$class->ID_FIELD}, - name => $data->{$class->NAME_FIELD}, - data => $data, + elsif (defined $param->{name}) { + $data + = Bugzilla->memcached->get({table => $class->DB_TABLE, name => $param->{name}, }); } - - $object = $class->new_from_hash($data); - $class->_object_cache_set($param, $object); - - return $object; + $set_memcached = $data ? 0 : 1; + } + $data ||= $class->_load_from_db($param); + + if ($data && $set_memcached) { + Bugzilla->memcached->set({ + table => $class->DB_TABLE, + id => $data->{$class->ID_FIELD}, + name => $data->{$class->NAME_FIELD}, + data => $data, + }); + } + + $object = $class->new_from_hash($data); + $class->_object_cache_set($param, $object); + + return $object; } # Note: Because this uses sql_istrcmp, if you make a new object use @@ -102,326 +101,324 @@ sub new { # in Bugzilla::DB::Pg appropriately, to add the right LOWER # index. You can see examples already there. sub _load_from_db { - my $class = shift; - my ($param) = @_; - my $dbh = Bugzilla->dbh; - my $columns = join(',', $class->_get_db_columns); - my $table = $class->DB_TABLE; - my $name_field = $class->NAME_FIELD; - my $id_field = $class->ID_FIELD; - - my $id = $param; - if (ref $param eq 'HASH') { - $id = $param->{id}; + my $class = shift; + my ($param) = @_; + my $dbh = Bugzilla->dbh; + my $columns = join(',', $class->_get_db_columns); + my $table = $class->DB_TABLE; + my $name_field = $class->NAME_FIELD; + my $id_field = $class->ID_FIELD; + + my $id = $param; + if (ref $param eq 'HASH') { + $id = $param->{id}; + } + + my $object_data; + if (defined $id) { + + # We special-case if somebody specifies an ID, so that we can + # validate it as numeric. + detaint_natural($id) + || ThrowCodeError('param_must_be_numeric', + {function => $class . '::_load_from_db'}); + + # Too large integers make PostgreSQL crash. + return if $id > MAX_INT_32; + + $object_data = $dbh->selectrow_hashref( + qq{ + SELECT $columns FROM $table + WHERE $id_field = ?}, undef, $id + ); + } + else { + unless (defined $param->{name} + || (defined $param->{'condition'} && defined $param->{'values'})) + { + ThrowCodeError('bad_arg', {argument => 'param', function => $class . '::new'}); } - my $object_data; - if (defined $id) { - # We special-case if somebody specifies an ID, so that we can - # validate it as numeric. - detaint_natural($id) - || ThrowCodeError('param_must_be_numeric', - {function => $class . '::_load_from_db'}); - - # Too large integers make PostgreSQL crash. - return if $id > MAX_INT_32; - - $object_data = $dbh->selectrow_hashref(qq{ - SELECT $columns FROM $table - WHERE $id_field = ?}, undef, $id); - } else { - unless (defined $param->{name} || (defined $param->{'condition'} - && defined $param->{'values'})) + my ($condition, @values); + if (defined $param->{name}) { + $condition = $dbh->sql_istrcmp($name_field, '?'); + push(@values, $param->{name}); + } + elsif (defined $param->{'condition'} && defined $param->{'values'}) { + caller->isa('Bugzilla::Object') || ThrowCodeError( + 'protection_violation', { - ThrowCodeError('bad_arg', { argument => 'param', - function => $class . '::new' }); - } - - my ($condition, @values); - if (defined $param->{name}) { - $condition = $dbh->sql_istrcmp($name_field, '?'); - push(@values, $param->{name}); - } - elsif (defined $param->{'condition'} && defined $param->{'values'}) { - caller->isa('Bugzilla::Object') - || ThrowCodeError('protection_violation', - { caller => caller, - function => $class . '::new', - argument => 'condition/values' }); - $condition = $param->{'condition'}; - push(@values, @{$param->{'values'}}); + caller => caller, + function => $class . '::new', + argument => 'condition/values' } - - map { trick_taint($_) } @values; - $object_data = $dbh->selectrow_hashref( - "SELECT $columns FROM $table WHERE $condition", undef, @values); + ); + $condition = $param->{'condition'}; + push(@values, @{$param->{'values'}}); } - return $object_data; + + map { trick_taint($_) } @values; + $object_data + = $dbh->selectrow_hashref("SELECT $columns FROM $table WHERE $condition", + undef, @values); + } + return $object_data; } sub new_from_list { - my $invocant = shift; - my $class = ref($invocant) || $invocant; - my ($id_list) = @_; - my $id_field = $class->ID_FIELD; - - my @detainted_ids; - foreach my $id (@$id_list) { - detaint_natural($id) || - ThrowCodeError('param_must_be_numeric', - {function => $class . '::new_from_list'}); - # Too large integers make PostgreSQL crash. - next if $id > MAX_INT_32; - push(@detainted_ids, $id); - } - - # We don't do $invocant->match because some classes have - # their own implementation of match which is not compatible - # with this one. However, match() still needs to have the right $invocant - # in order to do $class->DB_TABLE and so on. - return match($invocant, { $id_field => \@detainted_ids }); + my $invocant = shift; + my $class = ref($invocant) || $invocant; + my ($id_list) = @_; + my $id_field = $class->ID_FIELD; + + my @detainted_ids; + foreach my $id (@$id_list) { + detaint_natural($id) + || ThrowCodeError('param_must_be_numeric', + {function => $class . '::new_from_list'}); + + # Too large integers make PostgreSQL crash. + next if $id > MAX_INT_32; + push(@detainted_ids, $id); + } + + # We don't do $invocant->match because some classes have + # their own implementation of match which is not compatible + # with this one. However, match() still needs to have the right $invocant + # in order to do $class->DB_TABLE and so on. + return match($invocant, {$id_field => \@detainted_ids}); } sub new_from_hash { - my $invocant = shift; - my $class = ref($invocant) || $invocant; - my $object_data = shift || return; - $class->_serialisation_keys($object_data); - bless($object_data, $class); - $object_data->initialize(); - return $object_data; + my $invocant = shift; + my $class = ref($invocant) || $invocant; + my $object_data = shift || return; + $class->_serialisation_keys($object_data); + bless($object_data, $class); + $object_data->initialize(); + return $object_data; } sub initialize { - # abstract + + # abstract } # Provides a mechanism for objects to be cached in the request_cache sub object_cache_get { - my ($class, $id) = @_; - return $class->_object_cache_get( - { id => $id, cache => 1}, - $class - ); + my ($class, $id) = @_; + return $class->_object_cache_get({id => $id, cache => 1}, $class); } sub object_cache_set { - my $self = shift; - return $self->_object_cache_set( - { id => $self->id, cache => 1 }, - $self - ); + my $self = shift; + return $self->_object_cache_set({id => $self->id, cache => 1}, $self); } sub _object_cache_get { - my $class = shift; - my ($param) = @_; - my $cache_key = $class->object_cache_key($param) - || return; - return Bugzilla->request_cache->{$cache_key}; + my $class = shift; + my ($param) = @_; + my $cache_key = $class->object_cache_key($param) || return; + return Bugzilla->request_cache->{$cache_key}; } sub _object_cache_set { - my $class = shift; - my ($param, $object) = @_; - my $cache_key = $class->object_cache_key($param) - || return; - Bugzilla->request_cache->{$cache_key} = $object; + my $class = shift; + my ($param, $object) = @_; + my $cache_key = $class->object_cache_key($param) || return; + Bugzilla->request_cache->{$cache_key} = $object; } sub _object_cache_remove { - my $class = shift; - my ($param) = @_; - $param->{cache} = 1; - my $cache_key = $class->object_cache_key($param) - || return; - delete Bugzilla->request_cache->{$cache_key}; + my $class = shift; + my ($param) = @_; + $param->{cache} = 1; + my $cache_key = $class->object_cache_key($param) || return; + delete Bugzilla->request_cache->{$cache_key}; } sub object_cache_key { - my $class = shift; - my ($param) = @_; - if (ref($param) && $param->{cache} && ($param->{id} || $param->{name})) { - $class = blessed($class) if blessed($class); - return $class . ',' . ($param->{id} || $param->{name}); - } else { - return; - } + my $class = shift; + my ($param) = @_; + if (ref($param) && $param->{cache} && ($param->{id} || $param->{name})) { + $class = blessed($class) if blessed($class); + return $class . ',' . ($param->{id} || $param->{name}); + } + else { + return; + } } # To support serialisation, we need to capture the keys in an object's default # hashref. sub _serialisation_keys { - my ($class, $object) = @_; - my $cache = Bugzilla->request_cache->{serialisation_keys} ||= {}; - $cache->{$class} = [ keys %$object ] if $object && !exists $cache->{$class}; - return @{ $cache->{$class} }; + my ($class, $object) = @_; + my $cache = Bugzilla->request_cache->{serialisation_keys} ||= {}; + $cache->{$class} = [keys %$object] if $object && !exists $cache->{$class}; + return @{$cache->{$class}}; } sub check { - my ($invocant, $param) = @_; - my $class = ref($invocant) || $invocant; - # If we were just passed a name, then just use the name. - if (!ref $param) { - $param = { name => $param }; + my ($invocant, $param) = @_; + my $class = ref($invocant) || $invocant; + + # If we were just passed a name, then just use the name. + if (!ref $param) { + $param = {name => $param}; + } + + # Don't allow empty names or ids. + my $check_param = exists $param->{id} ? 'id' : 'name'; + $param->{$check_param} = trim($param->{$check_param}); + + # If somebody passes us "0", we want to throw an error like + # "there is no X with the name 0". This is true even for ids. So here, + # we only check if the parameter is undefined or empty. + if (!defined $param->{$check_param} or $param->{$check_param} eq '') { + ThrowUserError('object_not_specified', {class => $class}); + } + + my $obj = $class->new($param); + if (!$obj) { + + # We don't want to override the normal template "user" object if + # "user" is one of the params. + delete $param->{user}; + if (my $error = delete $param->{_error}) { + ThrowUserError($error, {%$param, class => $class}); } - - # Don't allow empty names or ids. - my $check_param = exists $param->{id} ? 'id' : 'name'; - $param->{$check_param} = trim($param->{$check_param}); - # If somebody passes us "0", we want to throw an error like - # "there is no X with the name 0". This is true even for ids. So here, - # we only check if the parameter is undefined or empty. - if (!defined $param->{$check_param} or $param->{$check_param} eq '') { - ThrowUserError('object_not_specified', { class => $class }); - } - - my $obj = $class->new($param); - if (!$obj) { - # We don't want to override the normal template "user" object if - # "user" is one of the params. - delete $param->{user}; - if (my $error = delete $param->{_error}) { - ThrowUserError($error, { %$param, class => $class }); - } - else { - ThrowUserError('object_does_not_exist', { %$param, class => $class }); - } + else { + ThrowUserError('object_does_not_exist', {%$param, class => $class}); } - return $obj; + } + return $obj; } # Note: Future extensions to this could be: # * Add a MATCH_JOIN constant so that we can join against # certain other tables for the WHERE criteria. sub match { - my ($invocant, $criteria) = @_; - my $class = ref($invocant) || $invocant; - my $dbh = Bugzilla->dbh; - - return [$class->get_all] if !$criteria; - - my (@terms, @values, $postamble); - foreach my $field (keys %$criteria) { - my $value = $criteria->{$field}; - - # allow for LIMIT and OFFSET expressions via the criteria. - next if $field eq 'OFFSET'; - if ( $field eq 'LIMIT' ) { - next unless defined $value; - detaint_natural($value) - or ThrowCodeError('param_must_be_numeric', - { param => 'LIMIT', - function => "${class}::match" }); - my $offset; - if (defined $criteria->{OFFSET}) { - $offset = $criteria->{OFFSET}; - detaint_signed($offset) - or ThrowCodeError('param_must_be_numeric', - { param => 'OFFSET', - function => "${class}::match" }); - } - $postamble = $dbh->sql_limit($value, $offset); - next; - } - elsif ( $field eq 'WHERE' ) { - # the WHERE value is a hashref where the keys are - # "column_name operator ?" and values are the placeholder's - # value (either a scalar or an array of values). - foreach my $k (keys %$value) { - push(@terms, $k); - my @this_value = ref($value->{$k}) ? @{ $value->{$k} } - : ($value->{$k}); - push(@values, @this_value); - } - next; - } + my ($invocant, $criteria) = @_; + my $class = ref($invocant) || $invocant; + my $dbh = Bugzilla->dbh; + + return [$class->get_all] if !$criteria; + + my (@terms, @values, $postamble); + foreach my $field (keys %$criteria) { + my $value = $criteria->{$field}; + + # allow for LIMIT and OFFSET expressions via the criteria. + next if $field eq 'OFFSET'; + if ($field eq 'LIMIT') { + next unless defined $value; + detaint_natural($value) + or ThrowCodeError('param_must_be_numeric', + {param => 'LIMIT', function => "${class}::match"}); + my $offset; + if (defined $criteria->{OFFSET}) { + $offset = $criteria->{OFFSET}; + detaint_signed($offset) + or ThrowCodeError('param_must_be_numeric', + {param => 'OFFSET', function => "${class}::match"}); + } + $postamble = $dbh->sql_limit($value, $offset); + next; + } + elsif ($field eq 'WHERE') { + + # the WHERE value is a hashref where the keys are + # "column_name operator ?" and values are the placeholder's + # value (either a scalar or an array of values). + foreach my $k (keys %$value) { + push(@terms, $k); + my @this_value = ref($value->{$k}) ? @{$value->{$k}} : ($value->{$k}); + push(@values, @this_value); + } + next; + } - # It's always safe to use the field defined by classes as being - # their ID field. In particular, this means that new_from_list() - # is exempted from this check. - $class->_check_field($field, 'match') unless $field eq $class->ID_FIELD; + # It's always safe to use the field defined by classes as being + # their ID field. In particular, this means that new_from_list() + # is exempted from this check. + $class->_check_field($field, 'match') unless $field eq $class->ID_FIELD; - if (ref $value eq 'ARRAY') { - # IN () is invalid SQL, and if we have an empty list - # to match against, we're just returning an empty - # array anyhow. - return [] if !scalar @$value; + if (ref $value eq 'ARRAY') { - my @qmarks = ("?") x @$value; - push(@terms, $dbh->sql_in($field, \@qmarks)); - push(@values, @$value); - } - elsif ($value eq NOT_NULL) { - push(@terms, "$field IS NOT NULL"); - } - elsif ($value eq IS_NULL) { - push(@terms, "$field IS NULL"); - } - else { - push(@terms, "$field = ?"); - push(@values, $value); - } + # IN () is invalid SQL, and if we have an empty list + # to match against, we're just returning an empty + # array anyhow. + return [] if !scalar @$value; + + my @qmarks = ("?") x @$value; + push(@terms, $dbh->sql_in($field, \@qmarks)); + push(@values, @$value); + } + elsif ($value eq NOT_NULL) { + push(@terms, "$field IS NOT NULL"); } + elsif ($value eq IS_NULL) { + push(@terms, "$field IS NULL"); + } + else { + push(@terms, "$field = ?"); + push(@values, $value); + } + } - my $where = join(' AND ', @terms) if scalar @terms; - return $class->_do_list_select($where, \@values, $postamble); + my $where = join(' AND ', @terms) if scalar @terms; + return $class->_do_list_select($where, \@values, $postamble); } sub _do_list_select { - my ($class, $where, $values, $postamble) = @_; - my $table = $class->DB_TABLE; - my $cols = join(',', $class->_get_db_columns); - my $order = $class->LIST_ORDER; - - # Unconditional requests for configuration data are cacheable. - my ($objects, $set_memcached, $memcached_key); - if (!defined $where - && Bugzilla->memcached->enabled - && $class->IS_CONFIG) - { - $memcached_key = "$class:get_all"; - $objects = Bugzilla->memcached->get_config({ key => $memcached_key }); - $set_memcached = $objects ? 0 : 1; + my ($class, $where, $values, $postamble) = @_; + my $table = $class->DB_TABLE; + my $cols = join(',', $class->_get_db_columns); + my $order = $class->LIST_ORDER; + + # Unconditional requests for configuration data are cacheable. + my ($objects, $set_memcached, $memcached_key); + if (!defined $where && Bugzilla->memcached->enabled && $class->IS_CONFIG) { + $memcached_key = "$class:get_all"; + $objects = Bugzilla->memcached->get_config({key => $memcached_key}); + $set_memcached = $objects ? 0 : 1; + } + + if (!$objects) { + my $sql = "SELECT $cols FROM $table"; + if (defined $where) { + $sql .= " WHERE $where "; } + $sql .= " ORDER BY $order"; + $sql .= " $postamble" if $postamble; - if (!$objects) { - my $sql = "SELECT $cols FROM $table"; - if (defined $where) { - $sql .= " WHERE $where "; - } - $sql .= " ORDER BY $order"; - $sql .= " $postamble" if $postamble; - - my $dbh = Bugzilla->dbh; - # Sometimes the values are tainted, but we don't want to untaint them - # for the caller. So we copy the array. It's safe to untaint because - # they're only used in placeholders here. - my @untainted = @{ $values || [] }; - trick_taint($_) foreach @untainted; - $objects = $dbh->selectall_arrayref($sql, {Slice=>{}}, @untainted); - $class->_serialisation_keys($objects->[0]) if @$objects; - } - - if ($objects && $set_memcached) { - Bugzilla->memcached->set_config({ - key => $memcached_key, - data => $objects - }); - } + my $dbh = Bugzilla->dbh; - foreach my $object (@$objects) { - $object = $class->new_from_hash($object); - } - return $objects; + # Sometimes the values are tainted, but we don't want to untaint them + # for the caller. So we copy the array. It's safe to untaint because + # they're only used in placeholders here. + my @untainted = @{$values || []}; + trick_taint($_) foreach @untainted; + $objects = $dbh->selectall_arrayref($sql, {Slice => {}}, @untainted); + $class->_serialisation_keys($objects->[0]) if @$objects; + } + + if ($objects && $set_memcached) { + Bugzilla->memcached->set_config({key => $memcached_key, data => $objects}); + } + + foreach my $object (@$objects) { + $object = $class->new_from_hash($object); + } + return $objects; } ############################### #### Accessors ###### ############################### -sub id { return $_[0]->{$_[0]->ID_FIELD}; } +sub id { return $_[0]->{$_[0]->ID_FIELD}; } sub name { return $_[0]->{$_[0]->NAME_FIELD}; } ############################### @@ -429,204 +426,214 @@ sub name { return $_[0]->{$_[0]->NAME_FIELD}; } ############################### sub set { - my ($self, $field, $value) = @_; - - # This method is protected. It's used to help implement set_ functions. - my $caller = caller; - $caller->isa('Bugzilla::Object') || $caller->isa('Bugzilla::Extension') - || ThrowCodeError('protection_violation', - { caller => caller, - superclass => __PACKAGE__, - function => 'Bugzilla::Object->set' }); - - Bugzilla::Hook::process('object_before_set', - { object => $self, field => $field, - value => $value }); - - my %validators = (%{$self->_get_validators}, %{$self->UPDATE_VALIDATORS}); - if (exists $validators{$field}) { - my $validator = $validators{$field}; - $value = $self->$validator($value, $field); - trick_taint($value) if (defined $value && !ref($value)); - - if ($self->can('_set_global_validator')) { - $self->_set_global_validator($value, $field); - } + my ($self, $field, $value) = @_; + + # This method is protected. It's used to help implement set_ functions. + my $caller = caller; + $caller->isa('Bugzilla::Object') + || $caller->isa('Bugzilla::Extension') + || ThrowCodeError( + 'protection_violation', + { + caller => caller, + superclass => __PACKAGE__, + function => 'Bugzilla::Object->set' } + ); - $self->{$field} = $value; + Bugzilla::Hook::process('object_before_set', + {object => $self, field => $field, value => $value}); - Bugzilla::Hook::process('object_end_of_set', - { object => $self, field => $field }); + my %validators = (%{$self->_get_validators}, %{$self->UPDATE_VALIDATORS}); + if (exists $validators{$field}) { + my $validator = $validators{$field}; + $value = $self->$validator($value, $field); + trick_taint($value) if (defined $value && !ref($value)); + + if ($self->can('_set_global_validator')) { + $self->_set_global_validator($value, $field); + } + } + + $self->{$field} = $value; + + Bugzilla::Hook::process('object_end_of_set', + {object => $self, field => $field}); } sub set_all { - my ($self, $params) = @_; - - # Don't let setters modify the values in $params for the caller. - my %field_values = %$params; - - my @sorted_names = $self->_sort_by_dep(keys %field_values); - - foreach my $key (@sorted_names) { - # It's possible for one set_ method to delete a key from $params - # for another set method, so if that's happened, we don't call the - # other set method. - next if !exists $field_values{$key}; - my $method = "set_$key"; - if (!$self->can($method)) { - my $class = ref($self) || $self; - ThrowCodeError("unknown_method", { method => "${class}::${method}" }); - } - $self->$method($field_values{$key}, \%field_values); + my ($self, $params) = @_; + + # Don't let setters modify the values in $params for the caller. + my %field_values = %$params; + + my @sorted_names = $self->_sort_by_dep(keys %field_values); + + foreach my $key (@sorted_names) { + + # It's possible for one set_ method to delete a key from $params + # for another set method, so if that's happened, we don't call the + # other set method. + next if !exists $field_values{$key}; + my $method = "set_$key"; + if (!$self->can($method)) { + my $class = ref($self) || $self; + ThrowCodeError("unknown_method", {method => "${class}::${method}"}); } - Bugzilla::Hook::process('object_end_of_set_all', - { object => $self, params => \%field_values }); + $self->$method($field_values{$key}, \%field_values); + } + Bugzilla::Hook::process('object_end_of_set_all', + {object => $self, params => \%field_values}); } sub update { - my $self = shift; - - my $dbh = Bugzilla->dbh; - my $table = $self->DB_TABLE; - my $id_field = $self->ID_FIELD; - - $dbh->bz_start_transaction(); - - my $old_self = $self->new($self->id); - - my @all_columns = $self->UPDATE_COLUMNS; - my @hook_columns; - Bugzilla::Hook::process('object_update_columns', - { object => $self, columns => \@hook_columns }); - push(@all_columns, @hook_columns); - - my %numeric = map { $_ => 1 } $self->NUMERIC_COLUMNS; - my %date = map { $_ => 1 } $self->DATE_COLUMNS; - my (@update_columns, @values, %changes); - foreach my $column (@all_columns) { - my ($old, $new) = ($old_self->{$column}, $self->{$column}); - # This has to be written this way in order to allow us to set a field - # from undef or to undef, and avoid warnings about comparing an undef - # with the "eq" operator. - if (!defined $new || !defined $old) { - next if !defined $new && !defined $old; - } - elsif ( ($numeric{$column} && $old == $new) - || ($date{$column} && str2time($old) == str2time($new)) - || $old eq $new ) { - next; - } + my $self = shift; - trick_taint($new) if defined $new; - push(@values, $new); - push(@update_columns, $column); - # We don't use $new because we don't want to detaint this for - # the caller. - $changes{$column} = [$old, $self->{$column}]; - } + my $dbh = Bugzilla->dbh; + my $table = $self->DB_TABLE; + my $id_field = $self->ID_FIELD; - my $columns = join(', ', map {"$_ = ?"} @update_columns); + $dbh->bz_start_transaction(); - $dbh->do("UPDATE $table SET $columns WHERE $id_field = ?", undef, - @values, $self->id) if @values; + my $old_self = $self->new($self->id); - Bugzilla::Hook::process('object_end_of_update', - { object => $self, old_object => $old_self, - changes => \%changes }); + my @all_columns = $self->UPDATE_COLUMNS; + my @hook_columns; + Bugzilla::Hook::process('object_update_columns', + {object => $self, columns => \@hook_columns}); + push(@all_columns, @hook_columns); - $self->audit_log(\%changes) if $self->AUDIT_UPDATES; + my %numeric = map { $_ => 1 } $self->NUMERIC_COLUMNS; + my %date = map { $_ => 1 } $self->DATE_COLUMNS; + my (@update_columns, @values, %changes); + foreach my $column (@all_columns) { + my ($old, $new) = ($old_self->{$column}, $self->{$column}); - $dbh->bz_commit_transaction(); - if ($self->USE_MEMCACHED && @values) { - Bugzilla->memcached->clear({ table => $table, id => $self->id }); - Bugzilla->memcached->clear_config() - if $self->IS_CONFIG; + # This has to be written this way in order to allow us to set a field + # from undef or to undef, and avoid warnings about comparing an undef + # with the "eq" operator. + if (!defined $new || !defined $old) { + next if !defined $new && !defined $old; } - $self->_object_cache_remove({ id => $self->id }); - $self->_object_cache_remove({ name => $self->name }) if $self->name; - - if (wantarray) { - return (\%changes, $old_self); + elsif (($numeric{$column} && $old == $new) + || ($date{$column} && str2time($old) == str2time($new)) + || $old eq $new) + { + next; } - return \%changes; + trick_taint($new) if defined $new; + push(@values, $new); + push(@update_columns, $column); + + # We don't use $new because we don't want to detaint this for + # the caller. + $changes{$column} = [$old, $self->{$column}]; + } + + my $columns = join(', ', map {"$_ = ?"} @update_columns); + + $dbh->do("UPDATE $table SET $columns WHERE $id_field = ?", + undef, @values, $self->id) + if @values; + + Bugzilla::Hook::process('object_end_of_update', + {object => $self, old_object => $old_self, changes => \%changes}); + + $self->audit_log(\%changes) if $self->AUDIT_UPDATES; + + $dbh->bz_commit_transaction(); + if ($self->USE_MEMCACHED && @values) { + Bugzilla->memcached->clear({table => $table, id => $self->id}); + Bugzilla->memcached->clear_config() if $self->IS_CONFIG; + } + $self->_object_cache_remove({id => $self->id}); + $self->_object_cache_remove({name => $self->name}) if $self->name; + + if (wantarray) { + return (\%changes, $old_self); + } + + return \%changes; } sub remove_from_db { - my $self = shift; - Bugzilla::Hook::process('object_before_delete', { object => $self }); - my $table = $self->DB_TABLE; - my $id_field = $self->ID_FIELD; - my $dbh = Bugzilla->dbh; - $dbh->bz_start_transaction(); - $self->audit_log(AUDIT_REMOVE) if $self->AUDIT_REMOVES; - $dbh->do("DELETE FROM $table WHERE $id_field = ?", undef, $self->id); - $dbh->bz_commit_transaction(); - if ($self->USE_MEMCACHED) { - Bugzilla->memcached->clear({ table => $table, id => $self->id }); - Bugzilla->memcached->clear_config() - if $self->IS_CONFIG; - } - $self->_object_cache_remove({ id => $self->id }); - $self->_object_cache_remove({ name => $self->name }) if $self->name; - undef $self; + my $self = shift; + Bugzilla::Hook::process('object_before_delete', {object => $self}); + my $table = $self->DB_TABLE; + my $id_field = $self->ID_FIELD; + my $dbh = Bugzilla->dbh; + $dbh->bz_start_transaction(); + $self->audit_log(AUDIT_REMOVE) if $self->AUDIT_REMOVES; + $dbh->do("DELETE FROM $table WHERE $id_field = ?", undef, $self->id); + $dbh->bz_commit_transaction(); + + if ($self->USE_MEMCACHED) { + Bugzilla->memcached->clear({table => $table, id => $self->id}); + Bugzilla->memcached->clear_config() if $self->IS_CONFIG; + } + $self->_object_cache_remove({id => $self->id}); + $self->_object_cache_remove({name => $self->name}) if $self->name; + undef $self; } sub audit_log { - my ($self, $changes) = @_; - my $class = ref $self; - my $dbh = Bugzilla->dbh; - my $user_id = Bugzilla->user->id || undef; - my $sth = $dbh->prepare( - 'INSERT INTO audit_log (user_id, class, object_id, field, + my ($self, $changes) = @_; + my $class = ref $self; + my $dbh = Bugzilla->dbh; + my $user_id = Bugzilla->user->id || undef; + my $sth = $dbh->prepare( + 'INSERT INTO audit_log (user_id, class, object_id, field, removed, added, at_time) - VALUES (?,?,?,?,?,?,LOCALTIMESTAMP(0))'); - # During creation or removal, $changes is actually just a string - # indicating whether we're creating or removing the object. - if ($changes eq AUDIT_CREATE or $changes eq AUDIT_REMOVE) { - # We put the object's name in the "added" or "removed" field. - # We do this thing with NAME_FIELD because $self->name returns - # the wrong thing for Bugzilla::User. - my $name = $self->{$self->NAME_FIELD}; - my @added_removed = $changes eq AUDIT_CREATE ? (undef, $name) - : ($name, undef); - $sth->execute($user_id, $class, $self->id, $changes, @added_removed); - return; - } - - # During update, it's the actual %changes hash produced by update(). - foreach my $field (keys %$changes) { - # Skip private changes. - next if $field =~ /^_/; - my ($from, $to) = $self->_sanitize_audit_log($field, $changes->{$field}); - $sth->execute($user_id, $class, $self->id, $field, $from, $to); - } + VALUES (?,?,?,?,?,?,LOCALTIMESTAMP(0))' + ); + + # During creation or removal, $changes is actually just a string + # indicating whether we're creating or removing the object. + if ($changes eq AUDIT_CREATE or $changes eq AUDIT_REMOVE) { + + # We put the object's name in the "added" or "removed" field. + # We do this thing with NAME_FIELD because $self->name returns + # the wrong thing for Bugzilla::User. + my $name = $self->{$self->NAME_FIELD}; + my @added_removed = $changes eq AUDIT_CREATE ? (undef, $name) : ($name, undef); + $sth->execute($user_id, $class, $self->id, $changes, @added_removed); + return; + } + + # During update, it's the actual %changes hash produced by update(). + foreach my $field (keys %$changes) { + + # Skip private changes. + next if $field =~ /^_/; + my ($from, $to) = $self->_sanitize_audit_log($field, $changes->{$field}); + $sth->execute($user_id, $class, $self->id, $field, $from, $to); + } } sub _sanitize_audit_log { - my ($self, $field, $changes) = @_; - my $class = ref($self) || $self; - - # Do not store hashed passwords. Only record the algorithm used to encode them. - if ($class eq 'Bugzilla::User' && $field eq 'cryptpassword') { - foreach my $passwd (@$changes) { - next unless $passwd; - my $algorithm = 'unknown_algorithm'; - if ($passwd =~ /{([^}]+)}$/) { - $algorithm = $1; - } - $passwd = "hashed_with_$algorithm"; - } + my ($self, $field, $changes) = @_; + my $class = ref($self) || $self; + + # Do not store hashed passwords. Only record the algorithm used to encode them. + if ($class eq 'Bugzilla::User' && $field eq 'cryptpassword') { + foreach my $passwd (@$changes) { + next unless $passwd; + my $algorithm = 'unknown_algorithm'; + if ($passwd =~ /{([^}]+)}$/) { + $algorithm = $1; + } + $passwd = "hashed_with_$algorithm"; } - return @$changes; + } + return @$changes; } sub flatten_to_hash { - my $self = shift; - my $class = blessed($self); - my %hash = map { $_ => $self->{$_} } $class->_serialisation_keys; - return \%hash; + my $self = shift; + my $class = blessed($self); + my %hash = map { $_ => $self->{$_} } $class->_serialisation_keys; + return \%hash; } ############################### @@ -634,127 +641,125 @@ sub flatten_to_hash { ############################### sub any_exist { - my $class = shift; - my $table = $class->DB_TABLE; - my $dbh = Bugzilla->dbh; - my $any_exist = $dbh->selectrow_array( - "SELECT 1 FROM $table " . $dbh->sql_limit(1)); - return $any_exist ? 1 : 0; + my $class = shift; + my $table = $class->DB_TABLE; + my $dbh = Bugzilla->dbh; + my $any_exist + = $dbh->selectrow_array("SELECT 1 FROM $table " . $dbh->sql_limit(1)); + return $any_exist ? 1 : 0; } sub create { - my ($class, $params) = @_; - my $dbh = Bugzilla->dbh; + my ($class, $params) = @_; + my $dbh = Bugzilla->dbh; - $dbh->bz_start_transaction(); - $class->check_required_create_fields($params); - my $field_values = $class->run_create_validators($params); - my $object = $class->insert_create_data($field_values); - $dbh->bz_commit_transaction(); + $dbh->bz_start_transaction(); + $class->check_required_create_fields($params); + my $field_values = $class->run_create_validators($params); + my $object = $class->insert_create_data($field_values); + $dbh->bz_commit_transaction(); - if (Bugzilla->memcached->enabled - && $class->USE_MEMCACHED - && $class->IS_CONFIG) - { - Bugzilla->memcached->clear_config(); - } + if (Bugzilla->memcached->enabled && $class->USE_MEMCACHED && $class->IS_CONFIG) + { + Bugzilla->memcached->clear_config(); + } - return $object; + return $object; } # Used to validate that a field name is in fact a valid column in the # current table before inserting it into SQL. sub _check_field { - my ($invocant, $field, $function) = @_; - my $class = ref($invocant) || $invocant; - if (!Bugzilla->dbh->bz_column_info($class->DB_TABLE, $field)) { - ThrowCodeError('param_invalid', { param => $field, - function => "${class}::$function" }); - } + my ($invocant, $field, $function) = @_; + my $class = ref($invocant) || $invocant; + if (!Bugzilla->dbh->bz_column_info($class->DB_TABLE, $field)) { + ThrowCodeError('param_invalid', + {param => $field, function => "${class}::$function"}); + } } sub check_required_create_fields { - my ($class, $params) = @_; + my ($class, $params) = @_; - # This hook happens here so that even subclasses that don't call - # SUPER::create are still affected by the hook. - Bugzilla::Hook::process('object_before_create', { class => $class, - params => $params }); + # This hook happens here so that even subclasses that don't call + # SUPER::create are still affected by the hook. + Bugzilla::Hook::process('object_before_create', + {class => $class, params => $params}); - my @check_fields = $class->_required_create_fields(); - foreach my $field (@check_fields) { - $params->{$field} = undef if !exists $params->{$field}; - } + my @check_fields = $class->_required_create_fields(); + foreach my $field (@check_fields) { + $params->{$field} = undef if !exists $params->{$field}; + } } sub run_create_validators { - my ($class, $params, $options) = @_; + my ($class, $params, $options) = @_; - my $validators = $class->_get_validators; - my %field_values = %$params; + my $validators = $class->_get_validators; + my %field_values = %$params; - # Make a hash skiplist for easier searching later - my %skip_list = map { $_ => 1 } @{ $options->{skip} || [] }; + # Make a hash skiplist for easier searching later + my %skip_list = map { $_ => 1 } @{$options->{skip} || []}; - # Get the sorted field names - my @sorted_names = $class->_sort_by_dep(keys %field_values); + # Get the sorted field names + my @sorted_names = $class->_sort_by_dep(keys %field_values); - # Remove the skipped names - my @unskipped = grep { !$skip_list{$_} } @sorted_names; + # Remove the skipped names + my @unskipped = grep { !$skip_list{$_} } @sorted_names; - foreach my $field (@unskipped) { - my $value; - if (exists $validators->{$field}) { - my $validator = $validators->{$field}; - $value = $class->$validator($field_values{$field}, $field, - \%field_values); - } - else { - $value = $field_values{$field}; - } - - # We want people to be able to explicitly set fields to NULL, - # and that means they can be set to undef. - trick_taint($value) if defined $value && !ref($value); - $field_values{$field} = $value; + foreach my $field (@unskipped) { + my $value; + if (exists $validators->{$field}) { + my $validator = $validators->{$field}; + $value = $class->$validator($field_values{$field}, $field, \%field_values); } - - Bugzilla::Hook::process('object_end_of_create_validators', - { class => $class, params => \%field_values }); - - return \%field_values; -} - -sub insert_create_data { - my ($class, $field_values) = @_; - my $dbh = Bugzilla->dbh; - - my (@field_names, @values); - while (my ($field, $value) = each %$field_values) { - $class->_check_field($field, 'create'); - push(@field_names, $field); - push(@values, $value); + else { + $value = $field_values{$field}; } - my $qmarks = '?,' x @field_names; - chop($qmarks); - my $table = $class->DB_TABLE; - $dbh->do("INSERT INTO $table (" . join(', ', @field_names) - . ") VALUES ($qmarks)", undef, @values); - my $id = $dbh->bz_last_key($table, $class->ID_FIELD); + # We want people to be able to explicitly set fields to NULL, + # and that means they can be set to undef. + trick_taint($value) if defined $value && !ref($value); + $field_values{$field} = $value; + } - my $object = $class->new($id); + Bugzilla::Hook::process('object_end_of_create_validators', + {class => $class, params => \%field_values}); - Bugzilla::Hook::process('object_end_of_create', { class => $class, - object => $object }); - $object->audit_log(AUDIT_CREATE) if $object->AUDIT_CREATES; + return \%field_values; +} - return $object; +sub insert_create_data { + my ($class, $field_values) = @_; + my $dbh = Bugzilla->dbh; + + my (@field_names, @values); + while (my ($field, $value) = each %$field_values) { + $class->_check_field($field, 'create'); + push(@field_names, $field); + push(@values, $value); + } + + my $qmarks = '?,' x @field_names; + chop($qmarks); + my $table = $class->DB_TABLE; + $dbh->do( + "INSERT INTO $table (" . join(', ', @field_names) . ") VALUES ($qmarks)", + undef, @values); + my $id = $dbh->bz_last_key($table, $class->ID_FIELD); + + my $object = $class->new($id); + + Bugzilla::Hook::process('object_end_of_create', + {class => $class, object => $object}); + $object->audit_log(AUDIT_CREATE) if $object->AUDIT_CREATES; + + return $object; } sub get_all { - my $class = shift; - return @{ $class->_do_list_select() }; + my $class = shift; + return @{$class->_do_list_select()}; } ############################### @@ -764,20 +769,19 @@ sub get_all { sub check_boolean { return $_[1] ? 1 : 0 } sub check_time { - my ($invocant, $value, $field, $params, $allow_negative) = @_; + my ($invocant, $value, $field, $params, $allow_negative) = @_; - # If we don't have a current value default to zero - my $current = blessed($invocant) ? $invocant->{$field} - : 0; - $current ||= 0; + # If we don't have a current value default to zero + my $current = blessed($invocant) ? $invocant->{$field} : 0; + $current ||= 0; - # Get the new value or zero if it isn't defined - $value = trim($value) || 0; + # Get the new value or zero if it isn't defined + $value = trim($value) || 0; - # Make sure the new value is well formed - _validate_time($value, $field, $allow_negative); + # Make sure the new value is well formed + _validate_time($value, $field, $allow_negative); - return $value; + return $value; } @@ -786,26 +790,25 @@ sub check_time { ################### sub _validate_time { - my ($time, $field, $allow_negative) = @_; - - # regexp verifies one or more digits, optionally followed by a period and - # zero or more digits, OR we have a period followed by one or more digits - # (allow negatives, though, so people can back out errors in time reporting) - if ($time !~ /^-?(?:\d+(?:\.\d*)?|\.\d+)$/) { - ThrowUserError("number_not_numeric", - {field => $field, num => "$time"}); - } - - # Callers can optionally allow negative times - if ( ($time < 0) && !$allow_negative ) { - ThrowUserError("number_too_small", - {field => $field, num => "$time", min_num => "0"}); - } - - if ($time > 99999.99) { - ThrowUserError("number_too_large", - {field => $field, num => "$time", max_num => "99999.99"}); - } + my ($time, $field, $allow_negative) = @_; + + # regexp verifies one or more digits, optionally followed by a period and + # zero or more digits, OR we have a period followed by one or more digits + # (allow negatives, though, so people can back out errors in time reporting) + if ($time !~ /^-?(?:\d+(?:\.\d*)?|\.\d+)$/) { + ThrowUserError("number_not_numeric", {field => $field, num => "$time"}); + } + + # Callers can optionally allow negative times + if (($time < 0) && !$allow_negative) { + ThrowUserError("number_too_small", + {field => $field, num => "$time", min_num => "0"}); + } + + if ($time > 99999.99) { + ThrowUserError("number_too_large", + {field => $field, num => "$time", max_num => "99999.99"}); + } } # Sorts fields according to VALIDATOR_DEPENDENCIES. This is not a @@ -813,54 +816,55 @@ sub _validate_time { # *have* to be in the list--it just has to be earlier than its dependent # if it *is* in the list. sub _sort_by_dep { - my ($invocant, @fields) = @_; - - my $dependencies = $invocant->VALIDATOR_DEPENDENCIES; - my ($has_deps, $no_deps) = part { $dependencies->{$_} ? 0 : 1 } @fields; - - # For fields with no dependencies, we sort them alphabetically, - # so that validation always happens in a consistent order. - # Fields with no dependencies come at the start of the list. - my @result = sort @{ $no_deps || [] }; - - # Fields with dependencies all go at the end of the list, and if - # they have dependencies on *each other*, then they have to be - # sorted properly. We go through $has_deps in sorted order to be - # sure that fields always validate in a consistent order. - foreach my $field (sort @{ $has_deps || [] }) { - if (!grep { $_ eq $field } @result) { - _insert_dep_field($field, $has_deps, $dependencies, \@result); - } + my ($invocant, @fields) = @_; + + my $dependencies = $invocant->VALIDATOR_DEPENDENCIES; + my ($has_deps, $no_deps) = part { $dependencies->{$_} ? 0 : 1 } @fields; + + # For fields with no dependencies, we sort them alphabetically, + # so that validation always happens in a consistent order. + # Fields with no dependencies come at the start of the list. + my @result = sort @{$no_deps || []}; + + # Fields with dependencies all go at the end of the list, and if + # they have dependencies on *each other*, then they have to be + # sorted properly. We go through $has_deps in sorted order to be + # sure that fields always validate in a consistent order. + foreach my $field (sort @{$has_deps || []}) { + if (!grep { $_ eq $field } @result) { + _insert_dep_field($field, $has_deps, $dependencies, \@result); } - return @result; + } + return @result; } sub _insert_dep_field { - my ($field, $insert_me, $dependencies, $result, $loop_tracking) = @_; + my ($field, $insert_me, $dependencies, $result, $loop_tracking) = @_; - if ($loop_tracking->{$field}) { - ThrowCodeError('object_dep_sort_loop', - { field => $field, - considered => [keys %$loop_tracking] }); - } - $loop_tracking->{$field} = 1; - - my $required_fields = $dependencies->{$field}; - # Imagine Field A requires field B... - foreach my $required_field (@$required_fields) { - # If our dependency is already satisfied, we're good. - next if grep { $_ eq $required_field } @$result; - - # If our dependency is not in the remaining fields to insert, - # then we're also OK. - next if !grep { $_ eq $required_field } @$insert_me; - - # So, at this point, we know that Field B is in $insert_me. - # So let's put the required field into the result. - _insert_dep_field($required_field, $insert_me, $dependencies, - $result, $loop_tracking); - } - push(@$result, $field); + if ($loop_tracking->{$field}) { + ThrowCodeError('object_dep_sort_loop', + {field => $field, considered => [keys %$loop_tracking]}); + } + $loop_tracking->{$field} = 1; + + my $required_fields = $dependencies->{$field}; + + # Imagine Field A requires field B... + foreach my $required_field (@$required_fields) { + + # If our dependency is already satisfied, we're good. + next if grep { $_ eq $required_field } @$result; + + # If our dependency is not in the remaining fields to insert, + # then we're also OK. + next if !grep { $_ eq $required_field } @$insert_me; + + # So, at this point, we know that Field B is in $insert_me. + # So let's put the required field into the result. + _insert_dep_field($required_field, $insert_me, $dependencies, $result, + $loop_tracking); + } + push(@$result, $field); } #################### @@ -873,61 +877,67 @@ sub _insert_dep_field { # page. sub _get_db_columns { - my $invocant = shift; - my $class = ref($invocant) || $invocant; - my $cache = Bugzilla->request_cache; - my $cache_key = "object_${class}_db_columns"; - return @{ $cache->{$cache_key} } if $cache->{$cache_key}; - # Currently you can only add new columns using object_columns, not - # remove or modify existing columns, because removing columns would - # almost certainly cause Bugzilla to function improperly. - my @add_columns; - Bugzilla::Hook::process('object_columns', - { class => $class, columns => \@add_columns }); - my @columns = ($invocant->DB_COLUMNS, @add_columns); - $cache->{$cache_key} = \@columns; - return @{ $cache->{$cache_key} }; + my $invocant = shift; + my $class = ref($invocant) || $invocant; + my $cache = Bugzilla->request_cache; + my $cache_key = "object_${class}_db_columns"; + return @{$cache->{$cache_key}} if $cache->{$cache_key}; + + # Currently you can only add new columns using object_columns, not + # remove or modify existing columns, because removing columns would + # almost certainly cause Bugzilla to function improperly. + my @add_columns; + Bugzilla::Hook::process('object_columns', + {class => $class, columns => \@add_columns}); + my @columns = ($invocant->DB_COLUMNS, @add_columns); + $cache->{$cache_key} = \@columns; + return @{$cache->{$cache_key}}; } # This method is private and should only be called by Bugzilla::Object. sub _get_validators { - my $invocant = shift; - my $class = ref($invocant) || $invocant; - my $cache = Bugzilla->request_cache; - my $cache_key = "object_${class}_validators"; - return $cache->{$cache_key} if $cache->{$cache_key}; - # We copy this into a hash so that the hook doesn't modify the constant. - # (That could be bad in mod_perl.) - my %validators = %{ $invocant->VALIDATORS }; - Bugzilla::Hook::process('object_validators', - { class => $class, validators => \%validators }); - $cache->{$cache_key} = \%validators; - return $cache->{$cache_key}; + my $invocant = shift; + my $class = ref($invocant) || $invocant; + my $cache = Bugzilla->request_cache; + my $cache_key = "object_${class}_validators"; + return $cache->{$cache_key} if $cache->{$cache_key}; + + # We copy this into a hash so that the hook doesn't modify the constant. + # (That could be bad in mod_perl.) + my %validators = %{$invocant->VALIDATORS}; + Bugzilla::Hook::process('object_validators', + {class => $class, validators => \%validators}); + $cache->{$cache_key} = \%validators; + return $cache->{$cache_key}; } # These are all the fields that need to be checked, always, when # calling create(), because they have no DEFAULT and they are marked # NOT NULL. sub _required_create_fields { - my $class = shift; - my $dbh = Bugzilla->dbh; - my $table = $class->DB_TABLE; - - my @columns = $dbh->bz_table_columns($table); - my @required; - foreach my $column (@columns) { - my $def = $dbh->bz_column_info($table, $column); - if ($def->{NOTNULL} and !defined $def->{DEFAULT} - # SERIAL fields effectively have a DEFAULT, but they're not - # listed as having a DEFAULT in DB::Schema. - and $def->{TYPE} !~ /serial/i) - { - my $field = $class->REQUIRED_FIELD_MAP->{$column} || $column; - push(@required, $field); - } + my $class = shift; + my $dbh = Bugzilla->dbh; + my $table = $class->DB_TABLE; + + my @columns = $dbh->bz_table_columns($table); + my @required; + foreach my $column (@columns) { + my $def = $dbh->bz_column_info($table, $column); + if ( + $def->{NOTNULL} + and !defined $def->{DEFAULT} + + # SERIAL fields effectively have a DEFAULT, but they're not + # listed as having a DEFAULT in DB::Schema. + and $def->{TYPE} !~ /serial/i + ) + { + my $field = $class->REQUIRED_FIELD_MAP->{$column} || $column; + push(@required, $field); } - push(@required, $class->EXTRA_REQUIRED_FIELDS); - return @required; + } + push(@required, $class->EXTRA_REQUIRED_FIELDS); + return @required; } 1; diff --git a/Bugzilla/Product.pm b/Bugzilla/Product.pm index 0c0cb458d..1f80db451 100644 --- a/Bugzilla/Product.pm +++ b/Bugzilla/Product.pm @@ -39,32 +39,32 @@ use constant IS_CONFIG => 1; use constant DB_TABLE => 'products'; use constant DB_COLUMNS => qw( - id - name - classification_id - description - isactive - defaultmilestone - allows_unconfirmed + id + name + classification_id + description + isactive + defaultmilestone + allows_unconfirmed ); use constant UPDATE_COLUMNS => qw( - name - description - defaultmilestone - isactive - allows_unconfirmed + name + description + defaultmilestone + isactive + allows_unconfirmed ); use constant VALIDATORS => { - allows_unconfirmed => \&Bugzilla::Object::check_boolean, - classification => \&_check_classification, - name => \&_check_name, - description => \&_check_description, - version => \&_check_version, - defaultmilestone => \&_check_default_milestone, - isactive => \&Bugzilla::Object::check_boolean, - create_series => \&Bugzilla::Object::check_boolean + allows_unconfirmed => \&Bugzilla::Object::check_boolean, + classification => \&_check_classification, + name => \&_check_name, + description => \&_check_description, + version => \&_check_version, + defaultmilestone => \&_check_default_milestone, + isactive => \&Bugzilla::Object::check_boolean, + create_series => \&Bugzilla::Object::check_boolean }; ############################### @@ -72,258 +72,283 @@ use constant VALIDATORS => { ############################### sub create { - my $class = shift; - my $dbh = Bugzilla->dbh; + my $class = shift; + my $dbh = Bugzilla->dbh; - $dbh->bz_start_transaction(); + $dbh->bz_start_transaction(); - $class->check_required_create_fields(@_); + $class->check_required_create_fields(@_); - my $params = $class->run_create_validators(@_); - # Some fields do not exist in the DB as is. - if (defined $params->{classification}) { - $params->{classification_id} = delete $params->{classification}; - } - my $version = delete $params->{version}; - my $create_series = delete $params->{create_series}; + my $params = $class->run_create_validators(@_); + + # Some fields do not exist in the DB as is. + if (defined $params->{classification}) { + $params->{classification_id} = delete $params->{classification}; + } + my $version = delete $params->{version}; + my $create_series = delete $params->{create_series}; - my $product = $class->insert_create_data($params); - Bugzilla->user->clear_product_cache(); + my $product = $class->insert_create_data($params); + Bugzilla->user->clear_product_cache(); - # Add the new version and milestone into the DB as valid values. - Bugzilla::Version->create({ value => $version, product => $product }); - Bugzilla::Milestone->create({ value => $product->default_milestone, - product => $product }); + # Add the new version and milestone into the DB as valid values. + Bugzilla::Version->create({value => $version, product => $product}); + Bugzilla::Milestone->create( + {value => $product->default_milestone, product => $product}); - # Create groups and series for the new product, if requested. - $product->_create_bug_group() if Bugzilla->params->{'makeproductgroups'}; - $product->_create_series() if $create_series; + # Create groups and series for the new product, if requested. + $product->_create_bug_group() if Bugzilla->params->{'makeproductgroups'}; + $product->_create_series() if $create_series; - Bugzilla::Hook::process('product_end_of_create', { product => $product }); + Bugzilla::Hook::process('product_end_of_create', {product => $product}); - $dbh->bz_commit_transaction(); - Bugzilla->memcached->clear_config(); - return $product; + $dbh->bz_commit_transaction(); + Bugzilla->memcached->clear_config(); + return $product; } # This is considerably faster than calling new_from_list three times # for each product in the list, particularly with hundreds or thousands # of products. sub preload { - my ($products, $preload_flagtypes) = @_; - my %prods = map { $_->id => $_ } @$products; - my @prod_ids = keys %prods; - return unless @prod_ids; - - # We cannot |use| it due to a dependency loop with Bugzilla::User. - require Bugzilla::Component; - foreach my $field (qw(component version milestone)) { - my $classname = "Bugzilla::" . ucfirst($field); - my $objects = $classname->match({ product_id => \@prod_ids }); - - # Now populate the products with this set of objects. - foreach my $obj (@$objects) { - my $product_id = $obj->product_id; - $prods{$product_id}->{"${field}s"} ||= []; - push(@{$prods{$product_id}->{"${field}s"}}, $obj); - } - } - if ($preload_flagtypes) { - $_->flag_types foreach @$products; + my ($products, $preload_flagtypes) = @_; + my %prods = map { $_->id => $_ } @$products; + my @prod_ids = keys %prods; + return unless @prod_ids; + + # We cannot |use| it due to a dependency loop with Bugzilla::User. + require Bugzilla::Component; + foreach my $field (qw(component version milestone)) { + my $classname = "Bugzilla::" . ucfirst($field); + my $objects = $classname->match({product_id => \@prod_ids}); + + # Now populate the products with this set of objects. + foreach my $obj (@$objects) { + my $product_id = $obj->product_id; + $prods{$product_id}->{"${field}s"} ||= []; + push(@{$prods{$product_id}->{"${field}s"}}, $obj); } + } + if ($preload_flagtypes) { + $_->flag_types foreach @$products; + } } sub update { - my $self = shift; - my $dbh = Bugzilla->dbh; - - # Don't update the DB if something goes wrong below -> transaction. - $dbh->bz_start_transaction(); - my ($changes, $old_self) = $self->SUPER::update(@_); - - # Also update group settings. - if ($self->{check_group_controls}) { - require Bugzilla::Bug; - import Bugzilla::Bug qw(LogActivityEntry); - - my $old_settings = $old_self->group_controls; - my $new_settings = $self->group_controls; - my $timestamp = $dbh->selectrow_array('SELECT NOW()'); - - foreach my $gid (keys %$new_settings) { - my $old_setting = $old_settings->{$gid} || {}; - my $new_setting = $new_settings->{$gid}; - # If all new settings are 0 for a given group, we delete the entry - # from group_control_map, so we have to track it here. - my $all_zero = 1; - my @fields; - my @values; - - foreach my $field ('entry', 'membercontrol', 'othercontrol', 'canedit', - 'editcomponents', 'editbugs', 'canconfirm') - { - my $old_value = $old_setting->{$field}; - my $new_value = $new_setting->{$field}; - $all_zero = 0 if $new_value; - next if (defined $old_value && $old_value == $new_value); - push(@fields, $field); - # The value has already been validated. - detaint_natural($new_value); - push(@values, $new_value); - } - # Is there anything to update? - next unless scalar @fields; - - if ($all_zero) { - $dbh->do('DELETE FROM group_control_map - WHERE product_id = ? AND group_id = ?', - undef, $self->id, $gid); - } - else { - if (exists $old_setting->{group}) { - # There is already an entry in the DB. - my $set_fields = join(', ', map {"$_ = ?"} @fields); - $dbh->do("UPDATE group_control_map SET $set_fields - WHERE product_id = ? AND group_id = ?", - undef, (@values, $self->id, $gid)); - } - else { - # No entry yet. - my $fields = join(', ', @fields); - # +2 because of the product and group IDs. - my $qmarks = join(',', ('?') x (scalar @fields + 2)); - $dbh->do("INSERT INTO group_control_map (product_id, group_id, $fields) - VALUES ($qmarks)", undef, ($self->id, $gid, @values)); - } - } - - # If the group is mandatory, restrict all bugs to it. - if ($new_setting->{membercontrol} == CONTROLMAPMANDATORY) { - my $bug_ids = - $dbh->selectcol_arrayref('SELECT bugs.bug_id + my $self = shift; + my $dbh = Bugzilla->dbh; + + # Don't update the DB if something goes wrong below -> transaction. + $dbh->bz_start_transaction(); + my ($changes, $old_self) = $self->SUPER::update(@_); + + # Also update group settings. + if ($self->{check_group_controls}) { + require Bugzilla::Bug; + import Bugzilla::Bug qw(LogActivityEntry); + + my $old_settings = $old_self->group_controls; + my $new_settings = $self->group_controls; + my $timestamp = $dbh->selectrow_array('SELECT NOW()'); + + foreach my $gid (keys %$new_settings) { + my $old_setting = $old_settings->{$gid} || {}; + my $new_setting = $new_settings->{$gid}; + + # If all new settings are 0 for a given group, we delete the entry + # from group_control_map, so we have to track it here. + my $all_zero = 1; + my @fields; + my @values; + + foreach my $field ( + 'entry', 'membercontrol', 'othercontrol', 'canedit', + 'editcomponents', 'editbugs', 'canconfirm' + ) + { + my $old_value = $old_setting->{$field}; + my $new_value = $new_setting->{$field}; + $all_zero = 0 if $new_value; + next if (defined $old_value && $old_value == $new_value); + push(@fields, $field); + + # The value has already been validated. + detaint_natural($new_value); + push(@values, $new_value); + } + + # Is there anything to update? + next unless scalar @fields; + + if ($all_zero) { + $dbh->do( + 'DELETE FROM group_control_map + WHERE product_id = ? AND group_id = ?', undef, $self->id, $gid + ); + } + else { + if (exists $old_setting->{group}) { + + # There is already an entry in the DB. + my $set_fields = join(', ', map {"$_ = ?"} @fields); + $dbh->do( + "UPDATE group_control_map SET $set_fields + WHERE product_id = ? AND group_id = ?", undef, + (@values, $self->id, $gid) + ); + } + else { + # No entry yet. + my $fields = join(', ', @fields); + + # +2 because of the product and group IDs. + my $qmarks = join(',', ('?') x (scalar @fields + 2)); + $dbh->do( + "INSERT INTO group_control_map (product_id, group_id, $fields) + VALUES ($qmarks)", undef, ($self->id, $gid, @values) + ); + } + } + + # If the group is mandatory, restrict all bugs to it. + if ($new_setting->{membercontrol} == CONTROLMAPMANDATORY) { + my $bug_ids = $dbh->selectcol_arrayref( + 'SELECT bugs.bug_id FROM bugs LEFT JOIN bug_group_map ON bug_group_map.bug_id = bugs.bug_id AND group_id = ? WHERE product_id = ? AND bug_group_map.bug_id IS NULL', - undef, $gid, $self->id); - - if (scalar @$bug_ids) { - my $sth = $dbh->prepare('INSERT INTO bug_group_map (bug_id, group_id) - VALUES (?, ?)'); - - foreach my $bug_id (@$bug_ids) { - $sth->execute($bug_id, $gid); - # Add this change to the bug history. - LogActivityEntry($bug_id, 'bug_group', '', - $new_setting->{group}->name, - Bugzilla->user->id, $timestamp); - } - push(@{$changes->{'_group_controls'}->{'now_mandatory'}}, - {name => $new_setting->{group}->name, - bug_count => scalar @$bug_ids}); - } - } - # If the group can no longer be used to restrict bugs, remove them. - elsif ($new_setting->{membercontrol} == CONTROLMAPNA) { - my $bug_ids = - $dbh->selectcol_arrayref('SELECT bugs.bug_id + undef, $gid, $self->id + ); + + if (scalar @$bug_ids) { + my $sth = $dbh->prepare( + 'INSERT INTO bug_group_map (bug_id, group_id) + VALUES (?, ?)' + ); + + foreach my $bug_id (@$bug_ids) { + $sth->execute($bug_id, $gid); + + # Add this change to the bug history. + LogActivityEntry($bug_id, 'bug_group', '', $new_setting->{group}->name, + Bugzilla->user->id, $timestamp); + } + push( + @{$changes->{'_group_controls'}->{'now_mandatory'}}, + {name => $new_setting->{group}->name, bug_count => scalar @$bug_ids} + ); + } + } + + # If the group can no longer be used to restrict bugs, remove them. + elsif ($new_setting->{membercontrol} == CONTROLMAPNA) { + my $bug_ids = $dbh->selectcol_arrayref( + 'SELECT bugs.bug_id FROM bugs INNER JOIN bug_group_map ON bug_group_map.bug_id = bugs.bug_id WHERE product_id = ? AND group_id = ?', - undef, $self->id, $gid); - - if (scalar @$bug_ids) { - $dbh->do('DELETE FROM bug_group_map WHERE group_id = ? AND ' . - $dbh->sql_in('bug_id', $bug_ids), undef, $gid); - - # Add this change to the bug history. - foreach my $bug_id (@$bug_ids) { - LogActivityEntry($bug_id, 'bug_group', - $old_setting->{group}->name, '', - Bugzilla->user->id, $timestamp); - } - push(@{$changes->{'_group_controls'}->{'now_na'}}, - {name => $old_setting->{group}->name, - bug_count => scalar @$bug_ids}); - } - } + undef, $self->id, $gid + ); + + if (scalar @$bug_ids) { + $dbh->do( + 'DELETE FROM bug_group_map WHERE group_id = ? AND ' + . $dbh->sql_in('bug_id', $bug_ids), + undef, $gid + ); + + # Add this change to the bug history. + foreach my $bug_id (@$bug_ids) { + LogActivityEntry($bug_id, 'bug_group', $old_setting->{group}->name, + '', Bugzilla->user->id, $timestamp); + } + push( + @{$changes->{'_group_controls'}->{'now_na'}}, + {name => $old_setting->{group}->name, bug_count => scalar @$bug_ids} + ); } - - delete $self->{groups_available}; - delete $self->{groups_mandatory}; + } } - $dbh->bz_commit_transaction(); - # Changes have been committed. - delete $self->{check_group_controls}; - Bugzilla->user->clear_product_cache(); - Bugzilla->memcached->clear_config(); - return $changes; + delete $self->{groups_available}; + delete $self->{groups_mandatory}; + } + $dbh->bz_commit_transaction(); + + # Changes have been committed. + delete $self->{check_group_controls}; + Bugzilla->user->clear_product_cache(); + Bugzilla->memcached->clear_config(); + + return $changes; } sub remove_from_db { - my ($self, $params) = @_; - my $user = Bugzilla->user; - my $dbh = Bugzilla->dbh; - - $dbh->bz_start_transaction(); - - $self->_check_if_controller(); - - if ($self->bug_count) { - if (Bugzilla->params->{'allowbugdeletion'}) { - require Bugzilla::Bug; - foreach my $bug_id (@{$self->bug_ids}) { - # Note that we allow the user to delete bugs they can't see, - # which is okay, because they're deleting the whole Product. - my $bug = new Bugzilla::Bug($bug_id); - $bug->remove_from_db(); - } - } - else { - ThrowUserError('product_has_bugs', { nb => $self->bug_count }); - } + my ($self, $params) = @_; + my $user = Bugzilla->user; + my $dbh = Bugzilla->dbh; + + $dbh->bz_start_transaction(); + + $self->_check_if_controller(); + + if ($self->bug_count) { + if (Bugzilla->params->{'allowbugdeletion'}) { + require Bugzilla::Bug; + foreach my $bug_id (@{$self->bug_ids}) { + + # Note that we allow the user to delete bugs they can't see, + # which is okay, because they're deleting the whole Product. + my $bug = new Bugzilla::Bug($bug_id); + $bug->remove_from_db(); + } + } + else { + ThrowUserError('product_has_bugs', {nb => $self->bug_count}); } + } - if ($params->{delete_series}) { - my $series_ids = - $dbh->selectcol_arrayref('SELECT series_id + if ($params->{delete_series}) { + my $series_ids = $dbh->selectcol_arrayref( + 'SELECT series_id FROM series INNER JOIN series_categories ON series_categories.id = series.category - WHERE series_categories.name = ?', - undef, $self->name); + WHERE series_categories.name = ?', undef, + $self->name + ); - if (scalar @$series_ids) { - $dbh->do('DELETE FROM series WHERE ' . $dbh->sql_in('series_id', $series_ids)); - } + if (scalar @$series_ids) { + $dbh->do('DELETE FROM series WHERE ' . $dbh->sql_in('series_id', $series_ids)); + } - # If no subcategory uses this product name, completely purge it. - my $in_use = - $dbh->selectrow_array('SELECT 1 + # If no subcategory uses this product name, completely purge it. + my $in_use = $dbh->selectrow_array( + 'SELECT 1 FROM series INNER JOIN series_categories ON series_categories.id = series.subcategory - WHERE series_categories.name = ? ' . - $dbh->sql_limit(1), - undef, $self->name); - if (!$in_use) { - $dbh->do('DELETE FROM series_categories WHERE name = ?', undef, $self->name); - } + WHERE series_categories.name = ? ' + . $dbh->sql_limit(1), undef, $self->name + ); + if (!$in_use) { + $dbh->do('DELETE FROM series_categories WHERE name = ?', undef, $self->name); } + } - $self->SUPER::remove_from_db(); + $self->SUPER::remove_from_db(); - $dbh->bz_commit_transaction(); - Bugzilla->memcached->clear_config(); + $dbh->bz_commit_transaction(); + Bugzilla->memcached->clear_config(); - # We have to delete these internal variables, else we get - # the old lists of products and classifications again. - delete $user->{selectable_products}; - delete $user->{selectable_classifications}; + # We have to delete these internal variables, else we get + # the old lists of products and classifications again. + delete $user->{selectable_products}; + delete $user->{selectable_classifications}; } @@ -332,91 +357,94 @@ sub remove_from_db { ############################### sub _check_classification { - my ($invocant, $classification_name) = @_; - - my $classification_id = 1; - if (Bugzilla->params->{'useclassification'}) { - my $classification = Bugzilla::Classification->check($classification_name); - $classification_id = $classification->id; - } - return $classification_id; + my ($invocant, $classification_name) = @_; + + my $classification_id = 1; + if (Bugzilla->params->{'useclassification'}) { + my $classification = Bugzilla::Classification->check($classification_name); + $classification_id = $classification->id; + } + return $classification_id; } sub _check_name { - my ($invocant, $name) = @_; + my ($invocant, $name) = @_; - $name = trim($name); - $name || ThrowUserError('product_blank_name'); + $name = trim($name); + $name || ThrowUserError('product_blank_name'); - if (length($name) > MAX_PRODUCT_SIZE) { - ThrowUserError('product_name_too_long', {'name' => $name}); - } + if (length($name) > MAX_PRODUCT_SIZE) { + ThrowUserError('product_name_too_long', {'name' => $name}); + } - my $product = new Bugzilla::Product({name => $name}); - if ($product && (!ref $invocant || $product->id != $invocant->id)) { - # Check for exact case sensitive match: - if ($product->name eq $name) { - ThrowUserError('product_name_already_in_use', {'product' => $product->name}); - } - else { - ThrowUserError('product_name_diff_in_case', {'product' => $name, - 'existing_product' => $product->name}); - } + my $product = new Bugzilla::Product({name => $name}); + if ($product && (!ref $invocant || $product->id != $invocant->id)) { + + # Check for exact case sensitive match: + if ($product->name eq $name) { + ThrowUserError('product_name_already_in_use', {'product' => $product->name}); + } + else { + ThrowUserError('product_name_diff_in_case', + {'product' => $name, 'existing_product' => $product->name}); } - return $name; + } + return $name; } sub _check_description { - my ($invocant, $description) = @_; + my ($invocant, $description) = @_; - $description = trim($description); - $description || ThrowUserError('product_must_have_description'); - return $description; + $description = trim($description); + $description || ThrowUserError('product_must_have_description'); + return $description; } sub _check_version { - my ($invocant, $version) = @_; + my ($invocant, $version) = @_; - $version = trim($version); - $version || ThrowUserError('product_must_have_version'); - # We will check the version length when Bugzilla::Version->create will do it. - return $version; + $version = trim($version); + $version || ThrowUserError('product_must_have_version'); + + # We will check the version length when Bugzilla::Version->create will do it. + return $version; } sub _check_default_milestone { - my ($invocant, $milestone) = @_; + my ($invocant, $milestone) = @_; - # Do nothing if target milestones are not in use. - unless (Bugzilla->params->{'usetargetmilestone'}) { - return (ref $invocant) ? $invocant->default_milestone : '---'; - } + # Do nothing if target milestones are not in use. + unless (Bugzilla->params->{'usetargetmilestone'}) { + return (ref $invocant) ? $invocant->default_milestone : '---'; + } - $milestone = trim($milestone); + $milestone = trim($milestone); - if (ref $invocant) { - # The default milestone must be one of the existing milestones. - my $mil_obj = new Bugzilla::Milestone({name => $milestone, product => $invocant}); + if (ref $invocant) { - $mil_obj || ThrowUserError('product_must_define_defaultmilestone', - {product => $invocant->name, - milestone => $milestone}); - } - else { - $milestone ||= '---'; - } - return $milestone; + # The default milestone must be one of the existing milestones. + my $mil_obj + = new Bugzilla::Milestone({name => $milestone, product => $invocant}); + + $mil_obj || ThrowUserError('product_must_define_defaultmilestone', + {product => $invocant->name, milestone => $milestone}); + } + else { + $milestone ||= '---'; + } + return $milestone; } sub _check_milestone_url { - my ($invocant, $url) = @_; + my ($invocant, $url) = @_; - # Do nothing if target milestones are not in use. - unless (Bugzilla->params->{'usetargetmilestone'}) { - return (ref $invocant) ? $invocant->milestone_url : ''; - } + # Do nothing if target milestones are not in use. + unless (Bugzilla->params->{'usetargetmilestone'}) { + return (ref $invocant) ? $invocant->milestone_url : ''; + } - $url = trim($url || ''); - return $url; + $url = trim($url || ''); + return $url; } ##################################### @@ -431,393 +459,429 @@ use constant is_default => 0; ############################### sub _create_bug_group { - my $self = shift; - my $dbh = Bugzilla->dbh; - - my $group_name = $self->name; - while (new Bugzilla::Group({name => $group_name})) { - $group_name .= '_'; - } - my $group_description = get_text('bug_group_description', {product => $self}); - - my $group = Bugzilla::Group->create({name => $group_name, - description => $group_description, - isbuggroup => 1}); - - # Associate the new group and new product. - $dbh->do('INSERT INTO group_control_map + my $self = shift; + my $dbh = Bugzilla->dbh; + + my $group_name = $self->name; + while (new Bugzilla::Group({name => $group_name})) { + $group_name .= '_'; + } + my $group_description = get_text('bug_group_description', {product => $self}); + + my $group + = Bugzilla::Group->create({ + name => $group_name, description => $group_description, isbuggroup => 1 + }); + + # Associate the new group and new product. + $dbh->do( + 'INSERT INTO group_control_map (group_id, product_id, membercontrol, othercontrol) - VALUES (?, ?, ?, ?)', - undef, ($group->id, $self->id, CONTROLMAPDEFAULT, CONTROLMAPNA)); + VALUES (?, ?, ?, ?)', undef, + ($group->id, $self->id, CONTROLMAPDEFAULT, CONTROLMAPNA) + ); } sub _create_series { - my $self = shift; - - my @series; - # We do every status, every resolution, and an "opened" one as well. - foreach my $bug_status (@{get_legal_field_values('bug_status')}) { - push(@series, [$bug_status, "bug_status=" . url_quote($bug_status)]); - } - - foreach my $resolution (@{get_legal_field_values('resolution')}) { - next if !$resolution; - push(@series, [$resolution, "resolution=" . url_quote($resolution)]); - } - - my @openedstatuses = BUG_STATE_OPEN; - my $query = join("&", map { "bug_status=" . url_quote($_) } @openedstatuses); - push(@series, [get_text('series_all_open'), $query]); - - foreach my $sdata (@series) { - my $series = new Bugzilla::Series(undef, $self->name, - get_text('series_subcategory'), - $sdata->[0], Bugzilla->user->id, 1, - $sdata->[1] . "&product=" . url_quote($self->name), 1); - $series->writeToDatabase(); - } + my $self = shift; + + my @series; + + # We do every status, every resolution, and an "opened" one as well. + foreach my $bug_status (@{get_legal_field_values('bug_status')}) { + push(@series, [$bug_status, "bug_status=" . url_quote($bug_status)]); + } + + foreach my $resolution (@{get_legal_field_values('resolution')}) { + next if !$resolution; + push(@series, [$resolution, "resolution=" . url_quote($resolution)]); + } + + my @openedstatuses = BUG_STATE_OPEN; + my $query = join("&", map { "bug_status=" . url_quote($_) } @openedstatuses); + push(@series, [get_text('series_all_open'), $query]); + + foreach my $sdata (@series) { + my $series + = new Bugzilla::Series(undef, $self->name, get_text('series_subcategory'), + $sdata->[0], Bugzilla->user->id, 1, + $sdata->[1] . "&product=" . url_quote($self->name), 1); + $series->writeToDatabase(); + } } -sub set_name { $_[0]->set('name', $_[1]); } -sub set_description { $_[0]->set('description', $_[1]); } -sub set_default_milestone { $_[0]->set('defaultmilestone', $_[1]); } -sub set_is_active { $_[0]->set('isactive', $_[1]); } +sub set_name { $_[0]->set('name', $_[1]); } +sub set_description { $_[0]->set('description', $_[1]); } +sub set_default_milestone { $_[0]->set('defaultmilestone', $_[1]); } +sub set_is_active { $_[0]->set('isactive', $_[1]); } sub set_allows_unconfirmed { $_[0]->set('allows_unconfirmed', $_[1]); } sub set_group_controls { - my ($self, $group, $settings) = @_; - - $group->is_active_bug_group - || ThrowUserError('product_illegal_group', {group => $group}); - - scalar(keys %$settings) - || ThrowCodeError('product_empty_group_controls', {group => $group}); - - # We store current settings for this group. - my $gs = $self->group_controls->{$group->id}; - # If there is no entry for this group yet, create a default hash. - unless (defined $gs) { - $gs = { entry => 0, - membercontrol => CONTROLMAPNA, - othercontrol => CONTROLMAPNA, - canedit => 0, - editcomponents => 0, - editbugs => 0, - canconfirm => 0, - group => $group }; - } - - # Both settings must be defined, or none of them can be updated. - if (defined $settings->{membercontrol} && defined $settings->{othercontrol}) { - # Legality of control combination is a function of - # membercontrol\othercontrol - # NA SH DE MA - # NA + - - - - # SH + + + + - # DE + - + + - # MA - - - + - foreach my $field ('membercontrol', 'othercontrol') { - my ($is_legal) = grep { $settings->{$field} == $_ } - (CONTROLMAPNA, CONTROLMAPSHOWN, CONTROLMAPDEFAULT, CONTROLMAPMANDATORY); - defined $is_legal || ThrowCodeError('product_illegal_group_control', - { field => $field, value => $settings->{$field} }); - } - unless ($settings->{membercontrol} == $settings->{othercontrol} - || $settings->{membercontrol} == CONTROLMAPSHOWN - || ($settings->{membercontrol} == CONTROLMAPDEFAULT - && $settings->{othercontrol} != CONTROLMAPSHOWN)) - { - ThrowUserError('illegal_group_control_combination', {groupname => $group->name}); - } - $gs->{membercontrol} = $settings->{membercontrol}; - $gs->{othercontrol} = $settings->{othercontrol}; + my ($self, $group, $settings) = @_; + + $group->is_active_bug_group + || ThrowUserError('product_illegal_group', {group => $group}); + + scalar(keys %$settings) + || ThrowCodeError('product_empty_group_controls', {group => $group}); + + # We store current settings for this group. + my $gs = $self->group_controls->{$group->id}; + + # If there is no entry for this group yet, create a default hash. + unless (defined $gs) { + $gs = { + entry => 0, + membercontrol => CONTROLMAPNA, + othercontrol => CONTROLMAPNA, + canedit => 0, + editcomponents => 0, + editbugs => 0, + canconfirm => 0, + group => $group + }; + } + + # Both settings must be defined, or none of them can be updated. + if (defined $settings->{membercontrol} && defined $settings->{othercontrol}) { + + # Legality of control combination is a function of + # membercontrol\othercontrol + # NA SH DE MA + # NA + - - - + # SH + + + + + # DE + - + + + # MA - - - + + foreach my $field ('membercontrol', 'othercontrol') { + my ($is_legal) + = grep { $settings->{$field} == $_ } + (CONTROLMAPNA, CONTROLMAPSHOWN, CONTROLMAPDEFAULT, CONTROLMAPMANDATORY); + defined $is_legal || ThrowCodeError('product_illegal_group_control', + {field => $field, value => $settings->{$field}}); } - - foreach my $field ('entry', 'canedit', 'editcomponents', 'editbugs', 'canconfirm') { - next unless defined $settings->{$field}; - $gs->{$field} = $settings->{$field} ? 1 : 0; + unless ( + $settings->{membercontrol} == $settings->{othercontrol} + || $settings->{membercontrol} == CONTROLMAPSHOWN + || ( $settings->{membercontrol} == CONTROLMAPDEFAULT + && $settings->{othercontrol} != CONTROLMAPSHOWN) + ) + { + ThrowUserError('illegal_group_control_combination', + {groupname => $group->name}); } - $self->{group_controls}->{$group->id} = $gs; - $self->{check_group_controls} = 1; + $gs->{membercontrol} = $settings->{membercontrol}; + $gs->{othercontrol} = $settings->{othercontrol}; + } + + foreach + my $field ('entry', 'canedit', 'editcomponents', 'editbugs', 'canconfirm') + { + next unless defined $settings->{$field}; + $gs->{$field} = $settings->{$field} ? 1 : 0; + } + $self->{group_controls}->{$group->id} = $gs; + $self->{check_group_controls} = 1; } sub components { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - if (!defined $self->{components}) { - my $ids = $dbh->selectcol_arrayref(q{ + if (!defined $self->{components}) { + my $ids = $dbh->selectcol_arrayref( + q{ SELECT id FROM components WHERE product_id = ? - ORDER BY name}, undef, $self->id); + ORDER BY name}, undef, $self->id + ); - require Bugzilla::Component; - $self->{components} = Bugzilla::Component->new_from_list($ids); - } - return $self->{components}; + require Bugzilla::Component; + $self->{components} = Bugzilla::Component->new_from_list($ids); + } + return $self->{components}; } sub group_controls { - my ($self, $full_data) = @_; - my $dbh = Bugzilla->dbh; - - # By default, we don't return groups which are not listed in - # group_control_map. If $full_data is true, then we also - # return groups whose settings could be set for the product. - my $where_or_and = 'WHERE'; - my $and_or_where = 'AND'; - if ($full_data) { - $where_or_and = 'AND'; - $and_or_where = 'WHERE'; - } - - # If $full_data is true, we collect all the data in all cases, - # even if the cache is already populated. - # $full_data is never used except in the very special case where - # all configurable bug groups are displayed to administrators, - # so we don't care about collecting all the data again in this case. - if (!defined $self->{group_controls} || $full_data) { - # Include name to the list, to allow us sorting data more easily. - my $query = qq{SELECT id, name, entry, membercontrol, othercontrol, + my ($self, $full_data) = @_; + my $dbh = Bugzilla->dbh; + + # By default, we don't return groups which are not listed in + # group_control_map. If $full_data is true, then we also + # return groups whose settings could be set for the product. + my $where_or_and = 'WHERE'; + my $and_or_where = 'AND'; + if ($full_data) { + $where_or_and = 'AND'; + $and_or_where = 'WHERE'; + } + + # If $full_data is true, we collect all the data in all cases, + # even if the cache is already populated. + # $full_data is never used except in the very special case where + # all configurable bug groups are displayed to administrators, + # so we don't care about collecting all the data again in this case. + if (!defined $self->{group_controls} || $full_data) { + + # Include name to the list, to allow us sorting data more easily. + my $query = qq{SELECT id, name, entry, membercontrol, othercontrol, canedit, editcomponents, editbugs, canconfirm FROM groups LEFT JOIN group_control_map ON id = group_id $where_or_and product_id = ? $and_or_where isbuggroup = 1}; - $self->{group_controls} = - $dbh->selectall_hashref($query, 'id', undef, $self->id); - - # For each group ID listed above, create and store its group object. - my @gids = keys %{$self->{group_controls}}; - my $groups = Bugzilla::Group->new_from_list(\@gids); - $self->{group_controls}->{$_->id}->{group} = $_ foreach @$groups; - } - - # We never cache bug counts, for the same reason as above. - if ($full_data) { - my $counts = - $dbh->selectall_arrayref('SELECT group_id, COUNT(bugs.bug_id) AS bug_count + $self->{group_controls} + = $dbh->selectall_hashref($query, 'id', undef, $self->id); + + # For each group ID listed above, create and store its group object. + my @gids = keys %{$self->{group_controls}}; + my $groups = Bugzilla::Group->new_from_list(\@gids); + $self->{group_controls}->{$_->id}->{group} = $_ foreach @$groups; + } + + # We never cache bug counts, for the same reason as above. + if ($full_data) { + my $counts = $dbh->selectall_arrayref( + 'SELECT group_id, COUNT(bugs.bug_id) AS bug_count FROM bug_group_map INNER JOIN bugs ON bugs.bug_id = bug_group_map.bug_id - WHERE bugs.product_id = ? ' . - $dbh->sql_group_by('group_id'), - {'Slice' => {}}, $self->id); - foreach my $data (@$counts) { - $self->{group_controls}->{$data->{group_id}}->{bug_count} = $data->{bug_count}; - } + WHERE bugs.product_id = ? ' + . $dbh->sql_group_by('group_id'), {'Slice' => {}}, $self->id + ); + foreach my $data (@$counts) { + $self->{group_controls}->{$data->{group_id}}->{bug_count} = $data->{bug_count}; } - return $self->{group_controls}; + } + return $self->{group_controls}; } sub groups_available { - my ($self) = @_; - return $self->{groups_available} if defined $self->{groups_available}; - my $dbh = Bugzilla->dbh; - my $shown = CONTROLMAPSHOWN; - my $default = CONTROLMAPDEFAULT; - my %member_groups = @{ $dbh->selectcol_arrayref( - "SELECT group_id, membercontrol + my ($self) = @_; + return $self->{groups_available} if defined $self->{groups_available}; + my $dbh = Bugzilla->dbh; + my $shown = CONTROLMAPSHOWN; + my $default = CONTROLMAPDEFAULT; + my %member_groups = @{ + $dbh->selectcol_arrayref( + "SELECT group_id, membercontrol FROM group_control_map INNER JOIN groups ON group_control_map.group_id = groups.id WHERE isbuggroup = 1 AND isactive = 1 AND product_id = ? AND (membercontrol = $shown OR membercontrol = $default) - AND " . Bugzilla->user->groups_in_sql(), - {Columns=>[1,2]}, $self->id) }; - # We don't need to check the group membership here, because we only - # add these groups to the list below if the group isn't already listed - # for membercontrol. - my %other_groups = @{ $dbh->selectcol_arrayref( - "SELECT group_id, othercontrol + AND " . Bugzilla->user->groups_in_sql(), {Columns => [1, 2]}, + $self->id + ) + }; + + # We don't need to check the group membership here, because we only + # add these groups to the list below if the group isn't already listed + # for membercontrol. + my %other_groups = @{ + $dbh->selectcol_arrayref( + "SELECT group_id, othercontrol FROM group_control_map INNER JOIN groups ON group_control_map.group_id = groups.id WHERE isbuggroup = 1 AND isactive = 1 AND product_id = ? - AND (othercontrol = $shown OR othercontrol = $default)", - {Columns=>[1,2]}, $self->id) }; - - # If the user is a member, then we use the membercontrol value. - # Otherwise, we use the othercontrol value. - my %all_groups = %member_groups; - foreach my $id (keys %other_groups) { - if (!defined $all_groups{$id}) { - $all_groups{$id} = $other_groups{$id}; - } + AND (othercontrol = $shown OR othercontrol = $default)", + {Columns => [1, 2]}, $self->id + ) + }; + + # If the user is a member, then we use the membercontrol value. + # Otherwise, we use the othercontrol value. + my %all_groups = %member_groups; + foreach my $id (keys %other_groups) { + if (!defined $all_groups{$id}) { + $all_groups{$id} = $other_groups{$id}; } + } - my $available = Bugzilla::Group->new_from_list([keys %all_groups]); - foreach my $group (@$available) { - $group->{is_default} = 1 if $all_groups{$group->id} == $default; - } + my $available = Bugzilla::Group->new_from_list([keys %all_groups]); + foreach my $group (@$available) { + $group->{is_default} = 1 if $all_groups{$group->id} == $default; + } - $self->{groups_available} = $available; - return $self->{groups_available}; + $self->{groups_available} = $available; + return $self->{groups_available}; } sub groups_mandatory { - my ($self) = @_; - return $self->{groups_mandatory} if $self->{groups_mandatory}; - my $groups = Bugzilla->user->groups_as_string; - my $mandatory = CONTROLMAPMANDATORY; - # For membercontrol we don't check group_id IN, because if membercontrol - # is Mandatory, the group is Mandatory for everybody, regardless of their - # group membership. - my $ids = Bugzilla->dbh->selectcol_arrayref( - "SELECT group_id + my ($self) = @_; + return $self->{groups_mandatory} if $self->{groups_mandatory}; + my $groups = Bugzilla->user->groups_as_string; + my $mandatory = CONTROLMAPMANDATORY; + + # For membercontrol we don't check group_id IN, because if membercontrol + # is Mandatory, the group is Mandatory for everybody, regardless of their + # group membership. + my $ids = Bugzilla->dbh->selectcol_arrayref( + "SELECT group_id FROM group_control_map INNER JOIN groups ON group_control_map.group_id = groups.id WHERE product_id = ? AND isactive = 1 AND (membercontrol = $mandatory OR (othercontrol = $mandatory - AND group_id NOT IN ($groups)))", - undef, $self->id); - $self->{groups_mandatory} = Bugzilla::Group->new_from_list($ids); - return $self->{groups_mandatory}; + AND group_id NOT IN ($groups)))", undef, $self->id + ); + $self->{groups_mandatory} = Bugzilla::Group->new_from_list($ids); + return $self->{groups_mandatory}; } # We don't just check groups_valid, because we want to know specifically # if this group can be validly set by the currently-logged-in user. sub group_is_settable { - my ($self, $group) = @_; + my ($self, $group) = @_; - return 0 unless ($group->is_active && $group->is_bug_group); + return 0 unless ($group->is_active && $group->is_bug_group); - my $is_mandatory = grep { $group->id == $_->id } - @{ $self->groups_mandatory }; - my $is_available = grep { $group->id == $_->id } - @{ $self->groups_available }; - return ($is_mandatory or $is_available) ? 1 : 0; + my $is_mandatory = grep { $group->id == $_->id } @{$self->groups_mandatory}; + my $is_available = grep { $group->id == $_->id } @{$self->groups_available}; + return ($is_mandatory or $is_available) ? 1 : 0; } sub group_is_valid { - my ($self, $group) = @_; - return grep($_->id == $group->id, @{ $self->groups_valid }) ? 1 : 0; + my ($self, $group) = @_; + return grep($_->id == $group->id, @{$self->groups_valid}) ? 1 : 0; } sub groups_valid { - my ($self) = @_; - return $self->{groups_valid} if defined $self->{groups_valid}; - - # Note that we don't check OtherControl below, because there is no - # valid NA/* combination. - my $ids = Bugzilla->dbh->selectcol_arrayref( - "SELECT DISTINCT group_id + my ($self) = @_; + return $self->{groups_valid} if defined $self->{groups_valid}; + + # Note that we don't check OtherControl below, because there is no + # valid NA/* combination. + my $ids = Bugzilla->dbh->selectcol_arrayref( + "SELECT DISTINCT group_id FROM group_control_map AS gcm INNER JOIN groups ON gcm.group_id = groups.id WHERE product_id = ? AND isbuggroup = 1 - AND membercontrol != " . CONTROLMAPNA, undef, $self->id); - $self->{groups_valid} = Bugzilla::Group->new_from_list($ids); - return $self->{groups_valid}; + AND membercontrol != " . CONTROLMAPNA, undef, $self->id + ); + $self->{groups_valid} = Bugzilla::Group->new_from_list($ids); + return $self->{groups_valid}; } sub versions { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - if (!defined $self->{versions}) { - my $ids = $dbh->selectcol_arrayref(q{ + if (!defined $self->{versions}) { + my $ids = $dbh->selectcol_arrayref( + q{ SELECT id FROM versions - WHERE product_id = ?}, undef, $self->id); + WHERE product_id = ?}, undef, $self->id + ); - $self->{versions} = Bugzilla::Version->new_from_list($ids); - } - return $self->{versions}; + $self->{versions} = Bugzilla::Version->new_from_list($ids); + } + return $self->{versions}; } sub milestones { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - if (!defined $self->{milestones}) { - my $ids = $dbh->selectcol_arrayref(q{ + if (!defined $self->{milestones}) { + my $ids = $dbh->selectcol_arrayref( + q{ SELECT id FROM milestones - WHERE product_id = ?}, undef, $self->id); - - $self->{milestones} = Bugzilla::Milestone->new_from_list($ids); - } - return $self->{milestones}; + WHERE product_id = ?}, undef, $self->id + ); + + $self->{milestones} = Bugzilla::Milestone->new_from_list($ids); + } + return $self->{milestones}; } sub bug_count { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - if (!defined $self->{'bug_count'}) { - $self->{'bug_count'} = $dbh->selectrow_array(qq{ + if (!defined $self->{'bug_count'}) { + $self->{'bug_count'} = $dbh->selectrow_array( + qq{ SELECT COUNT(bug_id) FROM bugs - WHERE product_id = ?}, undef, $self->id); + WHERE product_id = ?}, undef, $self->id + ); - } - return $self->{'bug_count'}; + } + return $self->{'bug_count'}; } sub bug_ids { - my $self = shift; - my $dbh = Bugzilla->dbh; - - if (!defined $self->{'bug_ids'}) { - $self->{'bug_ids'} = - $dbh->selectcol_arrayref(q{SELECT bug_id FROM bugs - WHERE product_id = ?}, - undef, $self->id); - } - return $self->{'bug_ids'}; + my $self = shift; + my $dbh = Bugzilla->dbh; + + if (!defined $self->{'bug_ids'}) { + $self->{'bug_ids'} = $dbh->selectcol_arrayref( + q{SELECT bug_id FROM bugs + WHERE product_id = ?}, undef, $self->id + ); + } + return $self->{'bug_ids'}; } sub user_has_access { - my ($self, $user) = @_; + my ($self, $user) = @_; - return Bugzilla->dbh->selectrow_array( - 'SELECT CASE WHEN group_id IS NULL THEN 1 ELSE 0 END + return Bugzilla->dbh->selectrow_array( + 'SELECT CASE WHEN group_id IS NULL THEN 1 ELSE 0 END FROM products LEFT JOIN group_control_map ON group_control_map.product_id = products.id AND group_control_map.entry != 0 AND group_id NOT IN (' . $user->groups_as_string . ') - WHERE products.id = ? ' . Bugzilla->dbh->sql_limit(1), - undef, $self->id); + WHERE products.id = ? ' . Bugzilla->dbh->sql_limit(1), undef, $self->id + ); } sub flag_types { - my $self = shift; - - return $self->{'flag_types'} if defined $self->{'flag_types'}; - - # We cache flag types to avoid useless calls to get_clusions(). - my $cache = Bugzilla->request_cache->{flag_types_per_product} ||= {}; - $self->{flag_types} = {}; - my $prod_id = $self->id; - my $flagtypes = Bugzilla::FlagType::match({ product_id => $prod_id }); - - foreach my $type ('bug', 'attachment') { - my @flags = grep { $_->target_type eq $type } @$flagtypes; - $self->{flag_types}->{$type} = \@flags; - - # Also populate component flag types, while we are here. - foreach my $comp (@{$self->components}) { - $comp->{flag_types} ||= {}; - my $comp_id = $comp->id; - - foreach my $flag (@flags) { - my $flag_id = $flag->id; - $cache->{$flag_id} ||= $flag; - my $i = $cache->{$flag_id}->inclusions_as_hash; - my $e = $cache->{$flag_id}->exclusions_as_hash; - my $included = $i->{0}->{0} || $i->{0}->{$comp_id} - || $i->{$prod_id}->{0} || $i->{$prod_id}->{$comp_id}; - my $excluded = $e->{0}->{0} || $e->{0}->{$comp_id} - || $e->{$prod_id}->{0} || $e->{$prod_id}->{$comp_id}; - push(@{$comp->{flag_types}->{$type}}, $flag) if ($included && !$excluded); - } - } + my $self = shift; + + return $self->{'flag_types'} if defined $self->{'flag_types'}; + + # We cache flag types to avoid useless calls to get_clusions(). + my $cache = Bugzilla->request_cache->{flag_types_per_product} ||= {}; + $self->{flag_types} = {}; + my $prod_id = $self->id; + my $flagtypes = Bugzilla::FlagType::match({product_id => $prod_id}); + + foreach my $type ('bug', 'attachment') { + my @flags = grep { $_->target_type eq $type } @$flagtypes; + $self->{flag_types}->{$type} = \@flags; + + # Also populate component flag types, while we are here. + foreach my $comp (@{$self->components}) { + $comp->{flag_types} ||= {}; + my $comp_id = $comp->id; + + foreach my $flag (@flags) { + my $flag_id = $flag->id; + $cache->{$flag_id} ||= $flag; + my $i = $cache->{$flag_id}->inclusions_as_hash; + my $e = $cache->{$flag_id}->exclusions_as_hash; + my $included + = $i->{0}->{0} + || $i->{0}->{$comp_id} + || $i->{$prod_id}->{0} + || $i->{$prod_id}->{$comp_id}; + my $excluded + = $e->{0}->{0} + || $e->{0}->{$comp_id} + || $e->{$prod_id}->{0} + || $e->{$prod_id}->{$comp_id}; + push(@{$comp->{flag_types}->{$type}}, $flag) if ($included && !$excluded); + } } - return $self->{'flag_types'}; + } + return $self->{'flag_types'}; } sub classification { - my $self = shift; - $self->{'classification'} ||= - new Bugzilla::Classification({ id => $self->classification_id, cache => 1 }); - return $self->{'classification'}; + my $self = shift; + $self->{'classification'} ||= new Bugzilla::Classification( + {id => $self->classification_id, cache => 1}); + return $self->{'classification'}; } ############################### @@ -825,29 +889,29 @@ sub classification { ############################### sub allows_unconfirmed { return $_[0]->{'allows_unconfirmed'}; } -sub description { return $_[0]->{'description'}; } -sub is_active { return $_[0]->{'isactive'}; } -sub default_milestone { return $_[0]->{'defaultmilestone'}; } -sub classification_id { return $_[0]->{'classification_id'}; } +sub description { return $_[0]->{'description'}; } +sub is_active { return $_[0]->{'isactive'}; } +sub default_milestone { return $_[0]->{'defaultmilestone'}; } +sub classification_id { return $_[0]->{'classification_id'}; } ############################### #### Subroutines ###### ############################### sub check { - my ($class, $params) = @_; - $params = { name => $params } if !ref $params; - if (!$params->{allow_inaccessible}) { - $params->{_error} = 'product_access_denied'; - } - my $product = $class->SUPER::check($params); - - if (!$params->{allow_inaccessible} - && !Bugzilla->user->can_access_product($product)) - { - ThrowUserError('product_access_denied', $params); - } - return $product; + my ($class, $params) = @_; + $params = {name => $params} if !ref $params; + if (!$params->{allow_inaccessible}) { + $params->{_error} = 'product_access_denied'; + } + my $product = $class->SUPER::check($params); + + if ( !$params->{allow_inaccessible} + && !Bugzilla->user->can_access_product($product)) + { + ThrowUserError('product_access_denied', $params); + } + return $product; } 1; diff --git a/Bugzilla/RNG.pm b/Bugzilla/RNG.pm index 96e442fa0..b92cbd720 100644 --- a/Bugzilla/RNG.pm +++ b/Bugzilla/RNG.pm @@ -27,7 +27,7 @@ our @EXPORT_OK = qw(rand srand irand); use constant DIVIDE_BY => 2**32; # How many bytes of seed to read. -use constant SEED_SIZE => 16; # 128 bits. +use constant SEED_SIZE => 16; # 128 bits. ################# # Windows Stuff # @@ -42,10 +42,11 @@ use constant PROV_RSA_FULL => 1; # Flags for CryptGenRandom: # Don't ever display a UI to the user, just fail if one would be needed. use constant CRYPT_SILENT => 64; + # Don't require existing public/private keypairs. use constant CRYPT_VERIFYCONTEXT => 0xF0000000; -# For some reason, BOOLEAN doesn't work properly as a return type with +# For some reason, BOOLEAN doesn't work properly as a return type with # Win32::API. use constant RTLGENRANDOM_PROTO => <irand(); - if (defined $limit) { - # We can't just use the mod operator because it will bias - # our output. Search for "modulo bias" on the Internet for - # details. This is slower than mod(), but does not have a bias, - # as demonstrated by Math::Random::Secure's uniform.t test. - return int(_to_float($int, $limit)); - } - return $int; + my ($limit) = @_; + Bugzilla::RNG::srand() if !defined $RNG; + my $int = $RNG->irand(); + if (defined $limit) { + + # We can't just use the mod operator because it will bias + # our output. Search for "modulo bias" on the Internet for + # details. This is slower than mod(), but does not have a bias, + # as demonstrated by Math::Random::Secure's uniform.t test. + return int(_to_float($int, $limit)); + } + return $int; } sub srand (;$) { - my ($value) = @_; - # Remove any RNG that might already have been made. - $RNG = undef; - my %args; - if (defined $value) { - $args{seed} = $value; - } - $RNG = _create_rng(\%args); + my ($value) = @_; + + # Remove any RNG that might already have been made. + $RNG = undef; + my %args; + if (defined $value) { + $args{seed} = $value; + } + $RNG = _create_rng(\%args); } sub _to_float { - my ($integer, $limit) = @_; - $limit ||= 1; - return ($integer / DIVIDE_BY) * $limit; + my ($integer, $limit) = @_; + $limit ||= 1; + return ($integer / DIVIDE_BY) * $limit; } ########################## @@ -100,123 +103,123 @@ sub _to_float { ########################## sub _create_rng { - my ($params) = @_; + my ($params) = @_; - if (!defined $params->{seed}) { - $params->{seed} = _get_seed(); - } + if (!defined $params->{seed}) { + $params->{seed} = _get_seed(); + } - _check_seed($params->{seed}); + _check_seed($params->{seed}); - my @seed_ints = unpack('L*', $params->{seed}); + my @seed_ints = unpack('L*', $params->{seed}); - my $rng = Math::Random::ISAAC->new(@seed_ints); + my $rng = Math::Random::ISAAC->new(@seed_ints); - # It's faster to skip the frontend interface of Math::Random::ISAAC - # and just use the backend directly. However, in case the internal - # code of Math::Random::ISAAC changes at some point, we do make sure - # that the {backend} element actually exists first. - return $rng->{backend} ? $rng->{backend} : $rng; + # It's faster to skip the frontend interface of Math::Random::ISAAC + # and just use the backend directly. However, in case the internal + # code of Math::Random::ISAAC changes at some point, we do make sure + # that the {backend} element actually exists first. + return $rng->{backend} ? $rng->{backend} : $rng; } sub _check_seed { - my ($seed) = @_; - if (length($seed) < 8) { - warn "Your seed is less than 8 bytes (64 bits). It could be" - . " easy to crack"; - } - # If it looks like we were seeded with a 32-bit integer, warn the - # user that they are making a dangerous, easily-crackable mistake. - elsif (length($seed) <= 10 and $seed =~ /^\d+$/) { - warn "RNG seeded with a 32-bit integer, this is easy to crack"; - } + my ($seed) = @_; + if (length($seed) < 8) { + warn "Your seed is less than 8 bytes (64 bits). It could be" . " easy to crack"; + } + + # If it looks like we were seeded with a 32-bit integer, warn the + # user that they are making a dangerous, easily-crackable mistake. + elsif (length($seed) <= 10 and $seed =~ /^\d+$/) { + warn "RNG seeded with a 32-bit integer, this is easy to crack"; + } } sub _get_seed { - return _windows_seed() if ON_WINDOWS; + return _windows_seed() if ON_WINDOWS; - if (-r '/dev/urandom') { - return _read_seed_from('/dev/urandom'); - } + if (-r '/dev/urandom') { + return _read_seed_from('/dev/urandom'); + } - return _read_seed_from('/dev/random'); + return _read_seed_from('/dev/random'); } sub _read_seed_from { - my ($from) = @_; - - open(my $fh, '<', $from) or die "$from: $!"; - my $buffer; - read($fh, $buffer, SEED_SIZE); - if (length($buffer) < SEED_SIZE) { - die "Could not read enough seed bytes from $from, got only " - . length($buffer); - } - close $fh; - return $buffer; + my ($from) = @_; + + open(my $fh, '<', $from) or die "$from: $!"; + my $buffer; + read($fh, $buffer, SEED_SIZE); + if (length($buffer) < SEED_SIZE) { + die "Could not read enough seed bytes from $from, got only " . length($buffer); + } + close $fh; + return $buffer; } sub _windows_seed { - my ($major, $minor) = (Win32::GetOSVersion())[1,2]; - if ($major < 5) { - die "Bugzilla does not support versions of Windows before" - . " Windows 2000"; - } - # This means Windows 2000. - if ($major == 5 and $minor == 0) { - return _win2k_seed(); - } - - my $rtlgenrand = Win32::API->new('advapi32', RTLGENRANDOM_PROTO); - if (!defined $rtlgenrand) { - die "Could not import RtlGenRand: $^E"; - } - my $buffer = chr(0) x SEED_SIZE; - my $result = $rtlgenrand->Call($buffer, SEED_SIZE); - if (!$result) { - die "RtlGenRand failed: $^E"; - } - return $buffer; + my ($major, $minor) = (Win32::GetOSVersion())[1, 2]; + if ($major < 5) { + die "Bugzilla does not support versions of Windows before" . " Windows 2000"; + } + + # This means Windows 2000. + if ($major == 5 and $minor == 0) { + return _win2k_seed(); + } + + my $rtlgenrand = Win32::API->new('advapi32', RTLGENRANDOM_PROTO); + if (!defined $rtlgenrand) { + die "Could not import RtlGenRand: $^E"; + } + my $buffer = chr(0) x SEED_SIZE; + my $result = $rtlgenrand->Call($buffer, SEED_SIZE); + if (!$result) { + die "RtlGenRand failed: $^E"; + } + return $buffer; } sub _win2k_seed { - my $crypt_acquire = Win32::API->new( - "advapi32", 'CryptAcquireContext', 'PPPNN', 'I'); - if (!defined $crypt_acquire) { - die "Could not import CryptAcquireContext: $^E"; - } - - my $crypt_release = Win32::API->new( - "advapi32", 'CryptReleaseContext', 'NN', 'I'); - if (!defined $crypt_release) { - die "Could not import CryptReleaseContext: $^E"; - } - - my $crypt_gen_random = Win32::API->new( - "advapi32", 'CryptGenRandom', 'NNP', 'I'); - if (!defined $crypt_gen_random) { - die "Could not import CryptGenRandom: $^E"; - } - - my $context = chr(0) x Win32::API::Type->sizeof('PULONG'); - my $acquire_result = $crypt_acquire->Call( - $context, 0, 0, PROV_RSA_FULL, CRYPT_SILENT | CRYPT_VERIFYCONTEXT); - if (!defined $acquire_result) { - die "CryptAcquireContext failed: $^E"; - } - - my $pack_type = Win32::API::Type::packing('PULONG'); - $context = unpack($pack_type, $context); - - my $buffer = chr(0) x SEED_SIZE; - my $rand_result = $crypt_gen_random->Call($context, SEED_SIZE, $buffer); - my $rand_error = $^E; - # We don't check this if it fails, we don't care. - $crypt_release->Call($context, 0); - if (!defined $rand_result) { - die "CryptGenRandom failed: $rand_error"; - } - return $buffer; + my $crypt_acquire + = Win32::API->new("advapi32", 'CryptAcquireContext', 'PPPNN', 'I'); + if (!defined $crypt_acquire) { + die "Could not import CryptAcquireContext: $^E"; + } + + my $crypt_release + = Win32::API->new("advapi32", 'CryptReleaseContext', 'NN', 'I'); + if (!defined $crypt_release) { + die "Could not import CryptReleaseContext: $^E"; + } + + my $crypt_gen_random + = Win32::API->new("advapi32", 'CryptGenRandom', 'NNP', 'I'); + if (!defined $crypt_gen_random) { + die "Could not import CryptGenRandom: $^E"; + } + + my $context = chr(0) x Win32::API::Type->sizeof('PULONG'); + my $acquire_result = $crypt_acquire->Call($context, 0, 0, PROV_RSA_FULL, + CRYPT_SILENT | CRYPT_VERIFYCONTEXT); + if (!defined $acquire_result) { + die "CryptAcquireContext failed: $^E"; + } + + my $pack_type = Win32::API::Type::packing('PULONG'); + $context = unpack($pack_type, $context); + + my $buffer = chr(0) x SEED_SIZE; + my $rand_result = $crypt_gen_random->Call($context, SEED_SIZE, $buffer); + my $rand_error = $^E; + + # We don't check this if it fails, we don't care. + $crypt_release->Call($context, 0); + if (!defined $rand_result) { + die "CryptGenRandom failed: $rand_error"; + } + return $buffer; } 1; diff --git a/Bugzilla/Report.pm b/Bugzilla/Report.pm index 10af2ea9e..84ea3b38b 100644 --- a/Bugzilla/Report.pm +++ b/Bugzilla/Report.pm @@ -26,43 +26,40 @@ use constant AUDIT_UPDATES => 0; use constant AUDIT_REMOVES => 0; use constant DB_COLUMNS => qw( - id - user_id - name - query + id + user_id + name + query ); use constant UPDATE_COLUMNS => qw( - name - query + name + query ); -use constant VALIDATORS => { - name => \&_check_name, - query => \&_check_query, -}; +use constant VALIDATORS => {name => \&_check_name, query => \&_check_query,}; ############## # Validators # ############## sub _check_name { - my ($invocant, $name) = @_; - $name = clean_text($name); - $name || ThrowUserError("report_name_missing"); - $name !~ /[<>&]/ || ThrowUserError("illegal_query_name"); - if (length($name) > MAX_LEN_QUERY_NAME) { - ThrowUserError("query_name_too_long"); - } - return $name; + my ($invocant, $name) = @_; + $name = clean_text($name); + $name || ThrowUserError("report_name_missing"); + $name !~ /[<>&]/ || ThrowUserError("illegal_query_name"); + if (length($name) > MAX_LEN_QUERY_NAME) { + ThrowUserError("query_name_too_long"); + } + return $name; } sub _check_query { - my ($invocant, $query) = @_; - $query || ThrowUserError("buglist_parameters_required"); - my $cgi = new Bugzilla::CGI($query); - $cgi->clean_search_url; - return $cgi->query_string; + my ($invocant, $query) = @_; + $query || ThrowUserError("buglist_parameters_required"); + my $cgi = new Bugzilla::CGI($query); + $cgi->clean_search_url; + return $cgi->query_string; } ############# @@ -71,7 +68,7 @@ sub _check_query { sub query { return $_[0]->{'query'}; } -sub set_name { $_[0]->set('name', $_[1]); } +sub set_name { $_[0]->set('name', $_[1]); } sub set_query { $_[0]->set('query', $_[1]); } ########### @@ -79,25 +76,26 @@ sub set_query { $_[0]->set('query', $_[1]); } ########### sub create { - my $class = shift; - my $param = shift; + my $class = shift; + my $param = shift; - Bugzilla->login(LOGIN_REQUIRED); - $param->{'user_id'} = Bugzilla->user->id; + Bugzilla->login(LOGIN_REQUIRED); + $param->{'user_id'} = Bugzilla->user->id; - unshift @_, $param; - my $self = $class->SUPER::create(@_); + unshift @_, $param; + my $self = $class->SUPER::create(@_); } sub check { - my $class = shift; - my $report = $class->SUPER::check(@_); - my $user = Bugzilla->user; - if ( grep($_->id eq $report->id, @{$user->reports})) { - return $report; - } else { - ThrowUserError('report_access_denied'); - } + my $class = shift; + my $report = $class->SUPER::check(@_); + my $user = Bugzilla->user; + if (grep($_->id eq $report->id, @{$user->reports})) { + return $report; + } + else { + ThrowUserError('report_access_denied'); + } } 1; diff --git a/Bugzilla/Search.pm b/Bugzilla/Search.pm index 646f949f5..17f932869 100644 --- a/Bugzilla/Search.pm +++ b/Bugzilla/Search.pm @@ -13,8 +13,8 @@ use warnings; use parent qw(Exporter); @Bugzilla::Search::EXPORT = qw( - IsValidQueryType - split_order_term + IsValidQueryType + split_order_term ); use Bugzilla::Error; @@ -135,311 +135,275 @@ use constant NUMBER_REGEX => qr/ # If you specify a search type in the boolean charts, this describes # which operator maps to which internal function here. use constant OPERATORS => { - equals => \&_simple_operator, - notequals => \&_simple_operator, - casesubstring => \&_casesubstring, - substring => \&_substring, - substr => \&_substring, - notsubstring => \&_notsubstring, - regexp => \&_regexp, - notregexp => \&_notregexp, - lessthan => \&_simple_operator, - lessthaneq => \&_simple_operator, - matches => sub { ThrowUserError("search_content_without_matches"); }, - notmatches => sub { ThrowUserError("search_content_without_matches"); }, - greaterthan => \&_simple_operator, - greaterthaneq => \&_simple_operator, - anyexact => \&_anyexact, - anywordssubstr => \&_anywordsubstr, - allwordssubstr => \&_allwordssubstr, - nowordssubstr => \&_nowordssubstr, - anywords => \&_anywords, - allwords => \&_allwords, - nowords => \&_nowords, - changedbefore => \&_changedbefore_changedafter, - changedafter => \&_changedbefore_changedafter, - changedfrom => \&_changedfrom_changedto, - changedto => \&_changedfrom_changedto, - changedby => \&_changedby, - isempty => \&_isempty, - isnotempty => \&_isnotempty, + equals => \&_simple_operator, + notequals => \&_simple_operator, + casesubstring => \&_casesubstring, + substring => \&_substring, + substr => \&_substring, + notsubstring => \&_notsubstring, + regexp => \&_regexp, + notregexp => \&_notregexp, + lessthan => \&_simple_operator, + lessthaneq => \&_simple_operator, + matches => sub { ThrowUserError("search_content_without_matches"); }, + notmatches => sub { ThrowUserError("search_content_without_matches"); }, + greaterthan => \&_simple_operator, + greaterthaneq => \&_simple_operator, + anyexact => \&_anyexact, + anywordssubstr => \&_anywordsubstr, + allwordssubstr => \&_allwordssubstr, + nowordssubstr => \&_nowordssubstr, + anywords => \&_anywords, + allwords => \&_allwords, + nowords => \&_nowords, + changedbefore => \&_changedbefore_changedafter, + changedafter => \&_changedbefore_changedafter, + changedfrom => \&_changedfrom_changedto, + changedto => \&_changedfrom_changedto, + changedby => \&_changedby, + isempty => \&_isempty, + isnotempty => \&_isnotempty, }; # Some operators are really just standard SQL operators, and are # all implemented by the _simple_operator function, which uses this # constant. use constant SIMPLE_OPERATORS => { - equals => '=', - notequals => '!=', - greaterthan => '>', - greaterthaneq => '>=', - lessthan => '<', - lessthaneq => "<=", + equals => '=', + notequals => '!=', + greaterthan => '>', + greaterthaneq => '>=', + lessthan => '<', + lessthaneq => "<=", }; # Most operators just reverse by removing or adding "not" from/to them. # However, some operators reverse in a different way, so those are listed # here. use constant OPERATOR_REVERSE => { - nowords => 'anywords', - nowordssubstr => 'anywordssubstr', - anywords => 'nowords', - anywordssubstr => 'nowordssubstr', - lessthan => 'greaterthaneq', - lessthaneq => 'greaterthan', - greaterthan => 'lessthaneq', - greaterthaneq => 'lessthan', - isempty => 'isnotempty', - isnotempty => 'isempty', - # The following don't currently have reversals: - # casesubstring, anyexact, allwords, allwordssubstr + nowords => 'anywords', + nowordssubstr => 'anywordssubstr', + anywords => 'nowords', + anywordssubstr => 'nowordssubstr', + lessthan => 'greaterthaneq', + lessthaneq => 'greaterthan', + greaterthan => 'lessthaneq', + greaterthaneq => 'lessthan', + isempty => 'isnotempty', + isnotempty => 'isempty', + + # The following don't currently have reversals: + # casesubstring, anyexact, allwords, allwordssubstr }; # For these operators, even if a field is numeric (is_numeric returns true), # we won't treat the input like a number. use constant NON_NUMERIC_OPERATORS => qw( - changedafter - changedbefore - changedfrom - changedto - regexp - notregexp + changedafter + changedbefore + changedfrom + changedto + regexp + notregexp ); # These operators ignore the entered value use constant NO_VALUE_OPERATORS => qw( - isempty - isnotempty + isempty + isnotempty ); use constant MULTI_SELECT_OVERRIDE => { - notequals => \&_multiselect_negative, - notregexp => \&_multiselect_negative, - notsubstring => \&_multiselect_negative, - nowords => \&_multiselect_negative, - nowordssubstr => \&_multiselect_negative, - - allwords => \&_multiselect_multiple, - allwordssubstr => \&_multiselect_multiple, - anyexact => \&_multiselect_multiple, - anywords => \&_multiselect_multiple, - anywordssubstr => \&_multiselect_multiple, - - _non_changed => \&_multiselect_nonchanged, + notequals => \&_multiselect_negative, + notregexp => \&_multiselect_negative, + notsubstring => \&_multiselect_negative, + nowords => \&_multiselect_negative, + nowordssubstr => \&_multiselect_negative, + + allwords => \&_multiselect_multiple, + allwordssubstr => \&_multiselect_multiple, + anyexact => \&_multiselect_multiple, + anywords => \&_multiselect_multiple, + anywordssubstr => \&_multiselect_multiple, + + _non_changed => \&_multiselect_nonchanged, }; use constant OPERATOR_FIELD_OVERRIDE => { - # User fields - 'attachments.submitter' => { - _non_changed => \&_user_nonchanged, - }, - assigned_to => { - _non_changed => \&_user_nonchanged, - }, - assigned_to_realname => { - _non_changed => \&_user_nonchanged, - }, - cc => { - _non_changed => \&_user_nonchanged, - }, - commenter => { - _non_changed => \&_user_nonchanged, - }, - reporter => { - _non_changed => \&_user_nonchanged, - }, - reporter_realname => { - _non_changed => \&_user_nonchanged, - }, - 'requestees.login_name' => { - _non_changed => \&_user_nonchanged, - }, - 'setters.login_name' => { - _non_changed => \&_user_nonchanged, - }, - qa_contact => { - _non_changed => \&_user_nonchanged, - }, - qa_contact_realname => { - _non_changed => \&_user_nonchanged, - }, - # General Bug Fields - alias => { _non_changed => \&_alias_nonchanged }, - 'attach_data.thedata' => MULTI_SELECT_OVERRIDE, - # We check all attachment fields against this. - attachments => MULTI_SELECT_OVERRIDE, - blocked => MULTI_SELECT_OVERRIDE, - bug_file_loc => { _non_changed => \&_nullable }, - bug_group => MULTI_SELECT_OVERRIDE, - classification => { - _non_changed => \&_classification_nonchanged, - }, - component => { - _non_changed => \&_component_nonchanged, - }, - content => { - matches => \&_content_matches, - notmatches => \&_content_matches, - _default => sub { ThrowUserError("search_content_without_matches"); }, - }, - days_elapsed => { - _default => \&_days_elapsed, - }, - dependson => MULTI_SELECT_OVERRIDE, - keywords => MULTI_SELECT_OVERRIDE, - 'flagtypes.name' => { - _non_changed => \&_flagtypes_nonchanged, - }, - longdesc => { - changedby => \&_long_desc_changedby, - changedbefore => \&_long_desc_changedbefore_after, - changedafter => \&_long_desc_changedbefore_after, - _non_changed => \&_long_desc_nonchanged, - }, - 'longdescs.count' => { - changedby => \&_long_desc_changedby, - changedbefore => \&_long_desc_changedbefore_after, - changedafter => \&_long_desc_changedbefore_after, - changedfrom => \&_invalid_combination, - changedto => \&_invalid_combination, - _default => \&_long_descs_count, - }, - 'longdescs.isprivate' => MULTI_SELECT_OVERRIDE, - owner_idle_time => { - greaterthan => \&_owner_idle_time_greater_less, - greaterthaneq => \&_owner_idle_time_greater_less, - lessthan => \&_owner_idle_time_greater_less, - lessthaneq => \&_owner_idle_time_greater_less, - _default => \&_invalid_combination, - }, - product => { - _non_changed => \&_product_nonchanged, - }, - tag => MULTI_SELECT_OVERRIDE, - comment_tag => MULTI_SELECT_OVERRIDE, - - # Timetracking Fields - deadline => { _non_changed => \&_deadline }, - percentage_complete => { - _non_changed => \&_percentage_complete, - }, - work_time => { - changedby => \&_work_time_changedby, - changedbefore => \&_work_time_changedbefore_after, - changedafter => \&_work_time_changedbefore_after, - _default => \&_work_time, - }, - last_visit_ts => { - _non_changed => \&_last_visit_ts, - _default => \&_last_visit_ts_invalid_operator, - }, - - # Custom Fields - FIELD_TYPE_FREETEXT, { _non_changed => \&_nullable }, - FIELD_TYPE_BUG_ID, { _non_changed => \&_nullable_int }, - FIELD_TYPE_DATETIME, { _non_changed => \&_nullable_datetime }, - FIELD_TYPE_DATE, { _non_changed => \&_nullable_date }, - FIELD_TYPE_TEXTAREA, { _non_changed => \&_nullable }, - FIELD_TYPE_MULTI_SELECT, MULTI_SELECT_OVERRIDE, - FIELD_TYPE_BUG_URLS, MULTI_SELECT_OVERRIDE, + # User fields + 'attachments.submitter' => {_non_changed => \&_user_nonchanged,}, + assigned_to => {_non_changed => \&_user_nonchanged,}, + assigned_to_realname => {_non_changed => \&_user_nonchanged,}, + cc => {_non_changed => \&_user_nonchanged,}, + commenter => {_non_changed => \&_user_nonchanged,}, + reporter => {_non_changed => \&_user_nonchanged,}, + reporter_realname => {_non_changed => \&_user_nonchanged,}, + 'requestees.login_name' => {_non_changed => \&_user_nonchanged,}, + 'setters.login_name' => {_non_changed => \&_user_nonchanged,}, + qa_contact => {_non_changed => \&_user_nonchanged,}, + qa_contact_realname => {_non_changed => \&_user_nonchanged,}, + + # General Bug Fields + alias => {_non_changed => \&_alias_nonchanged}, + 'attach_data.thedata' => MULTI_SELECT_OVERRIDE, + + # We check all attachment fields against this. + attachments => MULTI_SELECT_OVERRIDE, + blocked => MULTI_SELECT_OVERRIDE, + bug_file_loc => {_non_changed => \&_nullable}, + bug_group => MULTI_SELECT_OVERRIDE, + classification => {_non_changed => \&_classification_nonchanged,}, + component => {_non_changed => \&_component_nonchanged,}, + content => { + matches => \&_content_matches, + notmatches => \&_content_matches, + _default => sub { ThrowUserError("search_content_without_matches"); }, + }, + days_elapsed => {_default => \&_days_elapsed,}, + dependson => MULTI_SELECT_OVERRIDE, + keywords => MULTI_SELECT_OVERRIDE, + 'flagtypes.name' => {_non_changed => \&_flagtypes_nonchanged,}, + longdesc => { + changedby => \&_long_desc_changedby, + changedbefore => \&_long_desc_changedbefore_after, + changedafter => \&_long_desc_changedbefore_after, + _non_changed => \&_long_desc_nonchanged, + }, + 'longdescs.count' => { + changedby => \&_long_desc_changedby, + changedbefore => \&_long_desc_changedbefore_after, + changedafter => \&_long_desc_changedbefore_after, + changedfrom => \&_invalid_combination, + changedto => \&_invalid_combination, + _default => \&_long_descs_count, + }, + 'longdescs.isprivate' => MULTI_SELECT_OVERRIDE, + owner_idle_time => { + greaterthan => \&_owner_idle_time_greater_less, + greaterthaneq => \&_owner_idle_time_greater_less, + lessthan => \&_owner_idle_time_greater_less, + lessthaneq => \&_owner_idle_time_greater_less, + _default => \&_invalid_combination, + }, + product => {_non_changed => \&_product_nonchanged,}, + tag => MULTI_SELECT_OVERRIDE, + comment_tag => MULTI_SELECT_OVERRIDE, + + # Timetracking Fields + deadline => {_non_changed => \&_deadline}, + percentage_complete => {_non_changed => \&_percentage_complete,}, + work_time => { + changedby => \&_work_time_changedby, + changedbefore => \&_work_time_changedbefore_after, + changedafter => \&_work_time_changedbefore_after, + _default => \&_work_time, + }, + last_visit_ts => { + _non_changed => \&_last_visit_ts, + _default => \&_last_visit_ts_invalid_operator, + }, + + # Custom Fields + FIELD_TYPE_FREETEXT, + {_non_changed => \&_nullable}, + FIELD_TYPE_BUG_ID, + {_non_changed => \&_nullable_int}, + FIELD_TYPE_DATETIME, + {_non_changed => \&_nullable_datetime}, + FIELD_TYPE_DATE, + {_non_changed => \&_nullable_date}, + FIELD_TYPE_TEXTAREA, + {_non_changed => \&_nullable}, + FIELD_TYPE_MULTI_SELECT, + MULTI_SELECT_OVERRIDE, + FIELD_TYPE_BUG_URLS, + MULTI_SELECT_OVERRIDE, }; # These are fields where special action is taken depending on the # *value* passed in to the chart, sometimes. # 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 => \&_datetime_translate, - deadline => \&_date_translate, - delta_ts => \&_datetime_translate, - - # last_visit field that accept both a 1d, 1w, 1m, 1y format and the - # %last_changed% pronoun. - last_visit_ts => \&_last_visit_datetime, - }; - foreach my $field (Bugzilla->active_custom_fields) { - if ($field->type == FIELD_TYPE_DATETIME) { - $map->{$field->name} = \&_datetime_translate; - } elsif ($field->type == FIELD_TYPE_DATE) { - $map->{$field->name} = \&_date_translate; - } + 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 => \&_datetime_translate, + deadline => \&_date_translate, + delta_ts => \&_datetime_translate, + + # last_visit field that accept both a 1d, 1w, 1m, 1y format and the + # %last_changed% pronoun. + last_visit_ts => \&_last_visit_datetime, + }; + foreach my $field (Bugzilla->active_custom_fields) { + if ($field->type == FIELD_TYPE_DATETIME) { + $map->{$field->name} = \&_datetime_translate; } - return $map; -}; + elsif ($field->type == FIELD_TYPE_DATE) { + $map->{$field->name} = \&_date_translate; + } + } + return $map; +} # Information about fields that represent "users", used by _user_nonchanged. # There are other user fields than the ones listed here, but those use # defaults in _user_nonchanged. use constant USER_FIELDS => { - 'attachments.submitter' => { - field => 'submitter_id', - join => { table => 'attachments' }, - isprivate => 1, - }, - cc => { - field => 'who', - join => { table => 'cc' }, - }, - commenter => { - field => 'who', - join => { table => 'longdescs', join => 'INNER' }, - isprivate => 1, - }, - qa_contact => { - nullable => 1, - }, - 'requestees.login_name' => { - nullable => 1, - field => 'requestee_id', - join => { table => 'flags' }, - }, - 'setters.login_name' => { - field => 'setter_id', - join => { table => 'flags' }, - }, + 'attachments.submitter' => + {field => 'submitter_id', join => {table => 'attachments'}, isprivate => 1,}, + cc => {field => 'who', join => {table => 'cc'},}, + commenter => { + field => 'who', + join => {table => 'longdescs', join => 'INNER'}, + isprivate => 1, + }, + qa_contact => {nullable => 1,}, + 'requestees.login_name' => + {nullable => 1, field => 'requestee_id', join => {table => 'flags'},}, + 'setters.login_name' => {field => 'setter_id', join => {table => 'flags'},}, }; # Backwards compatibility for times that we changed the names of fields # or URL parameters. use constant FIELD_MAP => { - 'attachments.thedata' => 'attach_data.thedata', - bugidtype => 'bug_id_type', - changedin => 'days_elapsed', - long_desc => 'longdesc', - tags => 'tag', + 'attachments.thedata' => 'attach_data.thedata', + bugidtype => 'bug_id_type', + changedin => 'days_elapsed', + long_desc => 'longdesc', + tags => 'tag', }; # Some fields are not sorted on themselves, but on other fields. # We need to have a list of these fields and what they map to. use constant SPECIAL_ORDER => { - 'target_milestone' => { - order => ['map_target_milestone.sortkey','map_target_milestone.value'], - join => { - table => 'milestones', - from => 'target_milestone', - to => 'value', - extra => ['bugs.product_id = map_target_milestone.product_id'], - join => 'INNER', - } - }, + 'target_milestone' => { + order => ['map_target_milestone.sortkey', 'map_target_milestone.value'], + join => { + table => 'milestones', + from => 'target_milestone', + to => 'value', + extra => ['bugs.product_id = map_target_milestone.product_id'], + join => 'INNER', + } + }, }; # Certain columns require other columns to come before them # in _select_columns, and should be put there if they're not there. use constant COLUMN_DEPENDS => { - classification => ['product'], - percentage_complete => ['actual_time', 'remaining_time'], + classification => ['product'], + percentage_complete => ['actual_time', 'remaining_time'], }; # This describes tables that must be joined when you want to display @@ -447,109 +411,81 @@ use constant COLUMN_DEPENDS => { # DB::Schema to figure out what needs to be joined, but for some # fields it needs a little help. sub COLUMN_JOINS { - my $invocant = shift; - my $user = blessed($invocant) ? $invocant->_user : Bugzilla->user; - - my $joins = { - actual_time => { - table => '(SELECT bug_id, SUM(work_time) AS total' - . ' FROM longdescs GROUP BY bug_id)', - join => 'INNER', - }, - alias => { - table => 'bugs_aliases', - as => 'map_alias', - }, - assigned_to => { - from => 'assigned_to', - to => 'userid', - table => 'profiles', - join => 'INNER', - }, - reporter => { - from => 'reporter', - to => 'userid', - table => 'profiles', - join => 'INNER', - }, - qa_contact => { - from => 'qa_contact', - to => 'userid', - table => 'profiles', - }, - component => { - from => 'component_id', - to => 'id', - table => 'components', - join => 'INNER', - }, - product => { - from => 'product_id', - to => 'id', - table => 'products', - join => 'INNER', - }, - classification => { - table => 'classifications', - from => 'map_product.classification_id', - to => 'id', - join => 'INNER', - }, - 'flagtypes.name' => { - as => 'map_flags', - table => 'flags', - extra => ['map_flags.attach_id IS NULL'], - then_to => { - as => 'map_flagtypes', - table => 'flagtypes', - from => 'map_flags.type_id', - to => 'id', - }, - }, - keywords => { - table => 'keywords', - then_to => { - as => 'map_keyworddefs', - table => 'keyworddefs', - from => 'map_keywords.keywordid', - to => 'id', - }, - }, - blocked => { - table => 'dependencies', - to => 'dependson', - }, - dependson => { - table => 'dependencies', - to => 'blocked', - }, - 'longdescs.count' => { - table => 'longdescs', - join => 'INNER', - }, - tag => { - as => 'map_bug_tag', - table => 'bug_tag', - then_to => { - as => 'map_tag', - table => 'tag', - extra => ['map_tag.user_id = ' . $user->id], - from => 'map_bug_tag.tag_id', - to => 'id', - }, - }, - last_visit_ts => { - as => 'bug_user_last_visit', - table => 'bug_user_last_visit', - extra => ['bug_user_last_visit.user_id = ' . $user->id], - from => 'bug_id', - to => 'bug_id', - }, - }; - return $joins; -}; + my $invocant = shift; + my $user = blessed($invocant) ? $invocant->_user : Bugzilla->user; + + my $joins = { + actual_time => { + table => '(SELECT bug_id, SUM(work_time) AS total' + . ' FROM longdescs GROUP BY bug_id)', + join => 'INNER', + }, + alias => {table => 'bugs_aliases', as => 'map_alias',}, + assigned_to => { + from => 'assigned_to', + to => 'userid', + table => 'profiles', + join => 'INNER', + }, + reporter => + {from => 'reporter', to => 'userid', table => 'profiles', join => 'INNER',}, + qa_contact => {from => 'qa_contact', to => 'userid', table => 'profiles',}, + component => + {from => 'component_id', to => 'id', table => 'components', join => 'INNER',}, + product => + {from => 'product_id', to => 'id', table => 'products', join => 'INNER',}, + classification => { + table => 'classifications', + from => 'map_product.classification_id', + to => 'id', + join => 'INNER', + }, + 'flagtypes.name' => { + as => 'map_flags', + table => 'flags', + extra => ['map_flags.attach_id IS NULL'], + then_to => { + as => 'map_flagtypes', + table => 'flagtypes', + from => 'map_flags.type_id', + to => 'id', + }, + }, + keywords => { + table => 'keywords', + then_to => { + as => 'map_keyworddefs', + table => 'keyworddefs', + from => 'map_keywords.keywordid', + to => 'id', + }, + }, + blocked => {table => 'dependencies', to => 'dependson',}, + dependson => {table => 'dependencies', to => 'blocked',}, + 'longdescs.count' => {table => 'longdescs', join => 'INNER',}, + tag => { + as => 'map_bug_tag', + table => 'bug_tag', + then_to => { + as => 'map_tag', + table => 'tag', + extra => ['map_tag.user_id = ' . $user->id], + from => 'map_bug_tag.tag_id', + to => 'id', + }, + }, + last_visit_ts => { + as => 'bug_user_last_visit', + table => 'bug_user_last_visit', + extra => ['bug_user_last_visit.user_id = ' . $user->id], + from => 'bug_id', + to => 'bug_id', + }, + }; + return $joins; +} -# This constant defines the columns that can be selected in a query +# This constant defines the columns that can be selected in a query # and/or displayed in a bug list. Column records include the following # fields: # @@ -559,7 +495,7 @@ sub COLUMN_JOINS { # that returns the value of the column); # # 3. title: The title of the column as displayed to users. -# +# # Note: There are a few hacks in the code that deviate from these definitions. # In particular, the redundant short_desc column is removed when the # client requests "all" columns. @@ -570,150 +506,149 @@ sub COLUMN_JOINS { # and we don't want it to happen at compile time, so we have it as a # subroutine. sub COLUMNS { - my $invocant = shift; - my $user = blessed($invocant) ? $invocant->_user : Bugzilla->user; - my $dbh = Bugzilla->dbh; - my $cache = Bugzilla->request_cache; + my $invocant = shift; + my $user = blessed($invocant) ? $invocant->_user : Bugzilla->user; + my $dbh = Bugzilla->dbh; + my $cache = Bugzilla->request_cache; - if (defined $cache->{search_columns}->{$user->id}) { - return $cache->{search_columns}->{$user->id}; - } - - # These are columns that don't exist in fielddefs, but are valid buglist - # columns. (Also see near the bottom of this function for the definition - # of short_short_desc.) - my %columns = ( - relevance => { title => 'Relevance' }, - ); - - # Next we define columns that have special SQL instead of just something - # like "bugs.bug_id". - my $total_time = "(map_actual_time.total + bugs.remaining_time)"; - my %special_sql = ( - alias => $dbh->sql_group_concat('DISTINCT map_alias.alias'), - deadline => $dbh->sql_date_format('bugs.deadline', '%Y-%m-%d'), - actual_time => 'map_actual_time.total', - - # "FLOOR" is in there to turn this into an integer, making searches - # totally predictable. Otherwise you get floating-point numbers that - # are rather hard to search reliably if you're asking for exact - # numbers. - percentage_complete => - "(CASE WHEN $total_time = 0" - . " THEN 0" - . " ELSE FLOOR(100 * (map_actual_time.total / $total_time))" - . " END)", - - 'flagtypes.name' => $dbh->sql_group_concat('DISTINCT ' - . $dbh->sql_string_concat('map_flagtypes.name', 'map_flags.status'), - undef, undef, 'map_flagtypes.sortkey, map_flagtypes.name'), - - 'keywords' => $dbh->sql_group_concat('DISTINCT map_keyworddefs.name'), - - blocked => $dbh->sql_group_concat('DISTINCT map_blocked.blocked'), - dependson => $dbh->sql_group_concat('DISTINCT map_dependson.dependson'), - - 'longdescs.count' => 'COUNT(DISTINCT map_longdescs_count.comment_id)', - - tag => $dbh->sql_group_concat('DISTINCT map_tag.name'), - last_visit_ts => 'bug_user_last_visit.last_visit_ts', - ); - - # Backward-compatibility for old field names. Goes new_name => old_name. - # These are here and not in _translate_old_column because the rest of the - # code actually still uses the old names, while the fielddefs table uses - # the new names (which is not the case for the fields handled by - # _translate_old_column). - my %old_names = ( - creation_ts => 'opendate', - delta_ts => 'changeddate', - work_time => 'actual_time', - ); - - # Fields that are email addresses - my @email_fields = qw(assigned_to reporter qa_contact); - # Other fields that are stored in the bugs table as an id, but - # should be displayed using their name. - my @id_fields = qw(product component classification); - - foreach my $col (@email_fields) { - my $sql = "map_${col}.login_name"; - if (!$user->id) { - $sql = $dbh->sql_string_until($sql, $dbh->quote('@')); - } - $special_sql{$col} = $sql; - $special_sql{"${col}_realname"} = "map_${col}.realname"; - } - - foreach my $col (@id_fields) { - $special_sql{$col} = "map_${col}.name"; + if (defined $cache->{search_columns}->{$user->id}) { + return $cache->{search_columns}->{$user->id}; + } + + # These are columns that don't exist in fielddefs, but are valid buglist + # columns. (Also see near the bottom of this function for the definition + # of short_short_desc.) + my %columns = (relevance => {title => 'Relevance'},); + + # Next we define columns that have special SQL instead of just something + # like "bugs.bug_id". + my $total_time = "(map_actual_time.total + bugs.remaining_time)"; + my %special_sql = ( + alias => $dbh->sql_group_concat('DISTINCT map_alias.alias'), + deadline => $dbh->sql_date_format('bugs.deadline', '%Y-%m-%d'), + actual_time => 'map_actual_time.total', + + # "FLOOR" is in there to turn this into an integer, making searches + # totally predictable. Otherwise you get floating-point numbers that + # are rather hard to search reliably if you're asking for exact + # numbers. + percentage_complete => "(CASE WHEN $total_time = 0" + . " THEN 0" + . " ELSE FLOOR(100 * (map_actual_time.total / $total_time))" . " END)", + + 'flagtypes.name' => $dbh->sql_group_concat( + 'DISTINCT ' . $dbh->sql_string_concat('map_flagtypes.name', 'map_flags.status'), + undef, + undef, + 'map_flagtypes.sortkey, map_flagtypes.name' + ), + + 'keywords' => $dbh->sql_group_concat('DISTINCT map_keyworddefs.name'), + + blocked => $dbh->sql_group_concat('DISTINCT map_blocked.blocked'), + dependson => $dbh->sql_group_concat('DISTINCT map_dependson.dependson'), + + 'longdescs.count' => 'COUNT(DISTINCT map_longdescs_count.comment_id)', + + tag => $dbh->sql_group_concat('DISTINCT map_tag.name'), + last_visit_ts => 'bug_user_last_visit.last_visit_ts', + ); + + # Backward-compatibility for old field names. Goes new_name => old_name. + # These are here and not in _translate_old_column because the rest of the + # code actually still uses the old names, while the fielddefs table uses + # the new names (which is not the case for the fields handled by + # _translate_old_column). + my %old_names = ( + creation_ts => 'opendate', + delta_ts => 'changeddate', + work_time => 'actual_time', + ); + + # Fields that are email addresses + my @email_fields = qw(assigned_to reporter qa_contact); + + # Other fields that are stored in the bugs table as an id, but + # should be displayed using their name. + my @id_fields = qw(product component classification); + + foreach my $col (@email_fields) { + my $sql = "map_${col}.login_name"; + if (!$user->id) { + $sql = $dbh->sql_string_until($sql, $dbh->quote('@')); + } + $special_sql{$col} = $sql; + $special_sql{"${col}_realname"} = "map_${col}.realname"; + } + + foreach my $col (@id_fields) { + $special_sql{$col} = "map_${col}.name"; + } + + # Do the actual column-getting from fielddefs, now. + my @fields = @{Bugzilla->fields({obsolete => 0, buglist => 1})}; + foreach my $field (@fields) { + my $id = $field->name; + $id = $old_names{$id} if exists $old_names{$id}; + my $sql; + if (exists $special_sql{$id}) { + $sql = $special_sql{$id}; + } + elsif ($field->type == FIELD_TYPE_MULTI_SELECT) { + $sql = $dbh->sql_group_concat('DISTINCT map_' . $field->name . '.value'); } - - # Do the actual column-getting from fielddefs, now. - my @fields = @{ Bugzilla->fields({ obsolete => 0, buglist => 1 }) }; - foreach my $field (@fields) { - my $id = $field->name; - $id = $old_names{$id} if exists $old_names{$id}; - my $sql; - if (exists $special_sql{$id}) { - $sql = $special_sql{$id}; - } - elsif ($field->type == FIELD_TYPE_MULTI_SELECT) { - $sql = $dbh->sql_group_concat( - 'DISTINCT map_' . $field->name . '.value'); - } - else { - $sql = 'bugs.' . $field->name; - } - $columns{$id} = { name => $sql, title => $field->description }; + else { + $sql = 'bugs.' . $field->name; } + $columns{$id} = {name => $sql, title => $field->description}; + } - # The short_short_desc column is identical to short_desc - $columns{'short_short_desc'} = $columns{'short_desc'}; + # The short_short_desc column is identical to short_desc + $columns{'short_short_desc'} = $columns{'short_desc'}; - Bugzilla::Hook::process('buglist_columns', { columns => \%columns }); + Bugzilla::Hook::process('buglist_columns', {columns => \%columns}); - $cache->{search_columns}->{$user->id} = \%columns; - return $cache->{search_columns}->{$user->id}; + $cache->{search_columns}->{$user->id} = \%columns; + return $cache->{search_columns}->{$user->id}; } sub REPORT_COLUMNS { - my $invocant = shift; - my $user = blessed($invocant) ? $invocant->_user : Bugzilla->user; - - my $columns = dclone(blessed($invocant) ? $invocant->COLUMNS : COLUMNS); - # There's no reason to support reporting on unique fields. - # Also, some other fields don't make very good reporting axises, - # or simply don't work with the current reporting system. - my @no_report_columns = - qw(bug_id alias short_short_desc opendate changeddate - flagtypes.name relevance); - - # If you're not a time-tracker, you can't use time-tracking - # columns. - if (!$user->is_timetracker) { - push(@no_report_columns, TIMETRACKING_FIELDS); - } + my $invocant = shift; + my $user = blessed($invocant) ? $invocant->_user : Bugzilla->user; - foreach my $name (@no_report_columns) { - delete $columns->{$name}; - } - return $columns; + my $columns = dclone(blessed($invocant) ? $invocant->COLUMNS : COLUMNS); + + # There's no reason to support reporting on unique fields. + # Also, some other fields don't make very good reporting axises, + # or simply don't work with the current reporting system. + my @no_report_columns = qw(bug_id alias short_short_desc opendate changeddate + flagtypes.name relevance); + + # If you're not a time-tracker, you can't use time-tracking + # columns. + if (!$user->is_timetracker) { + push(@no_report_columns, TIMETRACKING_FIELDS); + } + + foreach my $name (@no_report_columns) { + delete $columns->{$name}; + } + return $columns; } # These are fields that never go into the GROUP BY on any DB. bug_id # is here because it *always* goes into the GROUP BY as the first item, # so it should be skipped when determining extra GROUP BY columns. use constant GROUP_BY_SKIP => qw( - alias - blocked - bug_id - dependson - flagtypes.name - keywords - longdescs.count - percentage_complete - tag + alias + blocked + bug_id + dependson + flagtypes.name + keywords + longdescs.count + percentage_complete + tag ); ############### @@ -722,27 +657,27 @@ use constant GROUP_BY_SKIP => qw( # Note that the params argument may be modified by Bugzilla::Search sub new { - my $invocant = shift; - my $class = ref($invocant) || $invocant; - - my $self = { @_ }; - bless($self, $class); - $self->{'user'} ||= Bugzilla->user; - - # There are certain behaviors of the CGI "Vars" hash that we don't want. - # In particular, if you put a single-value arrayref into it, later you - # get back out a string, which breaks anyexact charts (because they - # need arrays even for individual items, or we will re-trigger bug 67036). - # - # We can't just untie the hash--that would give us a hash with no values. - # We have to manually copy the hash into a new one, and we have to always - # do it, because there's no way to know if we were passed a tied hash - # or not. - my $params_in = $self->_params; - my %params = map { $_ => $params_in->{$_} } keys %$params_in; - $self->{params} = \%params; - - return $self; + my $invocant = shift; + my $class = ref($invocant) || $invocant; + + my $self = {@_}; + bless($self, $class); + $self->{'user'} ||= Bugzilla->user; + + # There are certain behaviors of the CGI "Vars" hash that we don't want. + # In particular, if you put a single-value arrayref into it, later you + # get back out a string, which breaks anyexact charts (because they + # need arrays even for individual items, or we will re-trigger bug 67036). + # + # We can't just untie the hash--that would give us a hash with no values. + # We have to manually copy the hash into a new one, and we have to always + # do it, because there's no way to know if we were passed a tied hash + # or not. + my $params_in = $self->_params; + my %params = map { $_ => $params_in->{$_} } keys %$params_in; + $self->{params} = \%params; + + return $self; } @@ -751,148 +686,156 @@ sub new { #################### sub data { - my $self = shift; - return $self->{data} if $self->{data}; - my $dbh = Bugzilla->dbh; - - # If all fields belong to the 'bugs' table, there is no need to split - # the original query into two pieces. Else we override the 'fields' - # argument to first get bug IDs based on the search criteria defined - # by the caller, and the desired fields are collected in the 2nd query. - my @orig_fields = $self->_input_columns; - my $all_in_bugs_table = 1; - foreach my $field (@orig_fields) { - next if ($self->COLUMNS->{$field}->{name} // $field) =~ /^bugs\.\w+$/; - $self->{fields} = ['bug_id']; - $all_in_bugs_table = 0; - last; - } - - my $start_time = [gettimeofday()]; - my $sql = $self->_sql; - # Do we just want bug IDs to pass to the 2nd query or all the data immediately? - my $func = $all_in_bugs_table ? 'selectall_arrayref' : 'selectcol_arrayref'; - my $bug_ids = $dbh->$func($sql); - my @extra_data = ({sql => $sql, time => tv_interval($start_time)}); - # Restore the original 'fields' argument, just in case. - $self->{fields} = \@orig_fields unless $all_in_bugs_table; - - # If there are no bugs found, or all fields are in the 'bugs' table, - # there is no need for another query. - if (!scalar @$bug_ids || $all_in_bugs_table) { - $self->{data} = $bug_ids; - return wantarray ? ($self->{data}, \@extra_data) : $self->{data}; - } - - # Make sure the bug_id will be returned. If not, append it to the list. - my $pos = firstidx { $_ eq 'bug_id' } @orig_fields; - if ($pos < 0) { - push(@orig_fields, 'bug_id'); - $pos = $#orig_fields; - } - - # Now create a query with the buglist above as the single criteria - # and the fields that the caller wants. No need to redo security checks; - # the list has already been validated above. - my $search = $self->new('fields' => \@orig_fields, - 'params' => {bug_id => $bug_ids, bug_id_type => 'anyexact'}, - 'sharer' => $self->_sharer_id, - 'user' => $self->_user, - 'allow_unlimited' => 1, - '_no_security_check' => 1); - - $start_time = [gettimeofday()]; - $sql = $search->_sql; - my $unsorted_data = $dbh->selectall_arrayref($sql); - push(@extra_data, {sql => $sql, time => tv_interval($start_time)}); - # Let's sort the data. We didn't do it in the query itself because - # we already know in which order to sort bugs thanks to the first query, - # and this avoids additional table joins in the SQL query. - my %data = map { $_->[$pos] => $_ } @$unsorted_data; - $self->{data} = [map { $data{$_} } @$bug_ids]; + my $self = shift; + return $self->{data} if $self->{data}; + my $dbh = Bugzilla->dbh; + + # If all fields belong to the 'bugs' table, there is no need to split + # the original query into two pieces. Else we override the 'fields' + # argument to first get bug IDs based on the search criteria defined + # by the caller, and the desired fields are collected in the 2nd query. + my @orig_fields = $self->_input_columns; + my $all_in_bugs_table = 1; + foreach my $field (@orig_fields) { + next if ($self->COLUMNS->{$field}->{name} // $field) =~ /^bugs\.\w+$/; + $self->{fields} = ['bug_id']; + $all_in_bugs_table = 0; + last; + } + + my $start_time = [gettimeofday()]; + my $sql = $self->_sql; + + # Do we just want bug IDs to pass to the 2nd query or all the data immediately? + my $func = $all_in_bugs_table ? 'selectall_arrayref' : 'selectcol_arrayref'; + my $bug_ids = $dbh->$func($sql); + my @extra_data = ({sql => $sql, time => tv_interval($start_time)}); + + # Restore the original 'fields' argument, just in case. + $self->{fields} = \@orig_fields unless $all_in_bugs_table; + + # If there are no bugs found, or all fields are in the 'bugs' table, + # there is no need for another query. + if (!scalar @$bug_ids || $all_in_bugs_table) { + $self->{data} = $bug_ids; return wantarray ? ($self->{data}, \@extra_data) : $self->{data}; + } + + # Make sure the bug_id will be returned. If not, append it to the list. + my $pos = firstidx { $_ eq 'bug_id' } @orig_fields; + if ($pos < 0) { + push(@orig_fields, 'bug_id'); + $pos = $#orig_fields; + } + + # Now create a query with the buglist above as the single criteria + # and the fields that the caller wants. No need to redo security checks; + # the list has already been validated above. + my $search = $self->new( + 'fields' => \@orig_fields, + 'params' => {bug_id => $bug_ids, bug_id_type => 'anyexact'}, + 'sharer' => $self->_sharer_id, + 'user' => $self->_user, + 'allow_unlimited' => 1, + '_no_security_check' => 1 + ); + + $start_time = [gettimeofday()]; + $sql = $search->_sql; + my $unsorted_data = $dbh->selectall_arrayref($sql); + push(@extra_data, {sql => $sql, time => tv_interval($start_time)}); + + # Let's sort the data. We didn't do it in the query itself because + # we already know in which order to sort bugs thanks to the first query, + # and this avoids additional table joins in the SQL query. + my %data = map { $_->[$pos] => $_ } @$unsorted_data; + $self->{data} = [map { $data{$_} } @$bug_ids]; + return wantarray ? ($self->{data}, \@extra_data) : $self->{data}; } sub _sql { - my ($self) = @_; - return $self->{sql} if $self->{sql}; - my $dbh = Bugzilla->dbh; - - my ($joins, $clause) = $self->_charts_to_conditions(); - - if (!$clause->as_string - && !Bugzilla->params->{'search_allow_no_criteria'} - && !$self->{allow_unlimited}) - { - ThrowUserError('buglist_parameters_required'); - } - - my $select = join(', ', $self->_sql_select); - my $from = $self->_sql_from($joins); - my $where = $self->_sql_where($clause); - my $group_by = $dbh->sql_group_by($self->_sql_group_by); - my $order_by = $self->_sql_order_by - ? "\nORDER BY " . join(', ', $self->_sql_order_by) : ''; - my $limit = $self->_sql_limit; - $limit = "\n$limit" if $limit; - - my $query = <{sql} if $self->{sql}; + my $dbh = Bugzilla->dbh; + + my ($joins, $clause) = $self->_charts_to_conditions(); + + if ( !$clause->as_string + && !Bugzilla->params->{'search_allow_no_criteria'} + && !$self->{allow_unlimited}) + { + ThrowUserError('buglist_parameters_required'); + } + + my $select = join(', ', $self->_sql_select); + my $from = $self->_sql_from($joins); + my $where = $self->_sql_where($clause); + my $group_by = $dbh->sql_group_by($self->_sql_group_by); + my $order_by + = $self->_sql_order_by + ? "\nORDER BY " . join(', ', $self->_sql_order_by) + : ''; + my $limit = $self->_sql_limit; + $limit = "\n$limit" if $limit; + + my $query = <{sql} = $query; - return $self->{sql}; + $self->{sql} = $query; + return $self->{sql}; } sub search_description { - my ($self, $params) = @_; - my $desc = $self->{'search_description'} ||= []; - if ($params) { - push(@$desc, $params); - } - # Make sure that the description has actually been generated if - # people are asking for the whole thing. - else { - $self->_sql; - } - return $self->{'search_description'}; + my ($self, $params) = @_; + my $desc = $self->{'search_description'} ||= []; + if ($params) { + push(@$desc, $params); + } + + # Make sure that the description has actually been generated if + # people are asking for the whole thing. + else { + $self->_sql; + } + return $self->{'search_description'}; } sub boolean_charts_to_custom_search { - my ($self, $cgi_buffer) = @_; - my $boolean_charts = $self->_boolean_charts; - my @as_params = $boolean_charts ? $boolean_charts->as_params : (); - - # We need to start our new ids after the last custom search "f" id. - # We can just pick the last id in the array because they are sorted - # numerically. - my $last_id = ($self->_field_ids)[-1]; - my $count = defined($last_id) ? $last_id + 1 : 0; - foreach my $param_set (@as_params) { - foreach my $name (keys %$param_set) { - my $value = $param_set->{$name}; - next if !defined $value; - $cgi_buffer->param($name . $count, $value); - } - $count++; + my ($self, $cgi_buffer) = @_; + my $boolean_charts = $self->_boolean_charts; + my @as_params = $boolean_charts ? $boolean_charts->as_params : (); + + # We need to start our new ids after the last custom search "f" id. + # We can just pick the last id in the array because they are sorted + # numerically. + my $last_id = ($self->_field_ids)[-1]; + my $count = defined($last_id) ? $last_id + 1 : 0; + foreach my $param_set (@as_params) { + foreach my $name (keys %$param_set) { + my $value = $param_set->{$name}; + next if !defined $value; + $cgi_buffer->param($name . $count, $value); } + $count++; + } } sub invalid_order_columns { - my ($self) = @_; - my @invalid_columns; - foreach my $order ($self->_input_order) { - next if defined $self->_validate_order_column($order); - push(@invalid_columns, $order); - } - return \@invalid_columns; + my ($self) = @_; + my @invalid_columns; + foreach my $order ($self->_input_order) { + next if defined $self->_validate_order_column($order); + push(@invalid_columns, $order); + } + return \@invalid_columns; } sub order { - my ($self) = @_; - return $self->_valid_order; + my ($self) = @_; + return $self->_valid_order; } ###################### @@ -901,49 +844,50 @@ sub order { # Fields that are legal for boolean charts of any kind. sub _chart_fields { - my ($self) = @_; + my ($self) = @_; - if (!$self->{chart_fields}) { - my $chart_fields = Bugzilla->fields({ by_name => 1 }); + if (!$self->{chart_fields}) { + my $chart_fields = Bugzilla->fields({by_name => 1}); - if (!$self->_user->is_timetracker) { - foreach my $tt_field (TIMETRACKING_FIELDS) { - delete $chart_fields->{$tt_field}; - } - } - $self->{chart_fields} = $chart_fields; + if (!$self->_user->is_timetracker) { + foreach my $tt_field (TIMETRACKING_FIELDS) { + delete $chart_fields->{$tt_field}; + } } - return $self->{chart_fields}; + $self->{chart_fields} = $chart_fields; + } + return $self->{chart_fields}; } # There are various places in Search.pm that we need to know the list of # valid multi-select fields--or really, fields that are stored like # multi-selects, which includes BUG_URLS fields. sub _multi_select_fields { - my ($self) = @_; - $self->{multi_select_fields} ||= Bugzilla->fields({ - by_name => 1, - type => [FIELD_TYPE_MULTI_SELECT, FIELD_TYPE_BUG_URLS]}); - return $self->{multi_select_fields}; + my ($self) = @_; + $self->{multi_select_fields} + ||= Bugzilla->fields({ + by_name => 1, type => [FIELD_TYPE_MULTI_SELECT, FIELD_TYPE_BUG_URLS] + }); + return $self->{multi_select_fields}; } # $self->{params} contains values that could be undef, could be a string, # or could be an arrayref. Sometimes we want that value as an array, # always. sub _param_array { - my ($self, $name) = @_; - my $value = $self->_params->{$name}; - if (!defined $value) { - return (); - } - if (ref($value) eq 'ARRAY') { - return @$value; - } - return ($value); -} - -sub _params { $_[0]->{params} } -sub _user { return $_[0]->{user} } + my ($self, $name) = @_; + my $value = $self->_params->{$name}; + if (!defined $value) { + return (); + } + if (ref($value) eq 'ARRAY') { + return @$value; + } + return ($value); +} + +sub _params { $_[0]->{params} } +sub _user { return $_[0]->{user} } sub _sharer_id { $_[0]->{sharer} } ############################## @@ -952,81 +896,84 @@ sub _sharer_id { $_[0]->{sharer} } # These are the fields the user has chosen to display on the buglist, # exactly as they were passed to new(). -sub _input_columns { @{ $_[0]->{'fields'} || [] } } +sub _input_columns { @{$_[0]->{'fields'} || []} } # These are columns that are also going to be in the SELECT for one reason # or another, but weren't actually requested by the caller. sub _extra_columns { - my ($self) = @_; - # Everything that's going to be in the ORDER BY must also be - # in the SELECT. - push(@{ $self->{extra_columns} }, $self->_valid_order_columns); - return @{ $self->{extra_columns} }; + my ($self) = @_; + + # Everything that's going to be in the ORDER BY must also be + # in the SELECT. + push(@{$self->{extra_columns}}, $self->_valid_order_columns); + return @{$self->{extra_columns}}; } # For search functions to modify extra_columns. It doesn't matter if # people push the same column onto this array multiple times, because # _select_columns will call "uniq" on its final result. sub _add_extra_column { - my ($self, $column) = @_; - push(@{ $self->{extra_columns} }, $column); + my ($self, $column) = @_; + push(@{$self->{extra_columns}}, $column); } # These are the columns that we're going to be actually SELECTing. sub _display_columns { - my ($self) = @_; - return @{ $self->{display_columns} } if $self->{display_columns}; - - # Do not alter the list from _input_columns at all, even if there are - # duplicated columns. Those are passed by the caller, and the caller - # expects to get them back in the exact same order. - my @columns = $self->_input_columns; - - # Only add columns which are not already listed. - my %list = map { $_ => 1 } @columns; - foreach my $column ($self->_extra_columns) { - push(@columns, $column) unless $list{$column}++; - } - $self->{display_columns} = \@columns; - return @{ $self->{display_columns} }; + my ($self) = @_; + return @{$self->{display_columns}} if $self->{display_columns}; + + # Do not alter the list from _input_columns at all, even if there are + # duplicated columns. Those are passed by the caller, and the caller + # expects to get them back in the exact same order. + my @columns = $self->_input_columns; + + # Only add columns which are not already listed. + my %list = map { $_ => 1 } @columns; + foreach my $column ($self->_extra_columns) { + push(@columns, $column) unless $list{$column}++; + } + $self->{display_columns} = \@columns; + return @{$self->{display_columns}}; } # These are the columns that are involved in the query. sub _select_columns { - my ($self) = @_; - return @{ $self->{select_columns} } if $self->{select_columns}; - - my @select_columns; - foreach my $column ($self->_display_columns) { - if (my $add_first = COLUMN_DEPENDS->{$column}) { - push(@select_columns, @$add_first); - } - push(@select_columns, $column); + my ($self) = @_; + return @{$self->{select_columns}} if $self->{select_columns}; + + my @select_columns; + foreach my $column ($self->_display_columns) { + if (my $add_first = COLUMN_DEPENDS->{$column}) { + push(@select_columns, @$add_first); } - # Remove duplicated columns. - $self->{select_columns} = [uniq @select_columns]; - return @{ $self->{select_columns} }; + push(@select_columns, $column); + } + + # Remove duplicated columns. + $self->{select_columns} = [uniq @select_columns]; + return @{$self->{select_columns}}; } # This takes _display_columns and translates it into the actual SQL that # will go into the SELECT clause. sub _sql_select { - my ($self) = @_; - my @sql_fields; - foreach my $column ($self->_display_columns) { - my $sql = $self->COLUMNS->{$column}->{name} // ''; - if ($sql) { - my $alias = $column; - # Aliases cannot contain dots in them. We convert them to underscores. - $alias =~ tr/./_/; - $sql .= " AS $alias"; - } - else { - $sql = $column; - } - push(@sql_fields, $sql); + my ($self) = @_; + my @sql_fields; + foreach my $column ($self->_display_columns) { + my $sql = $self->COLUMNS->{$column}->{name} // ''; + if ($sql) { + my $alias = $column; + + # Aliases cannot contain dots in them. We convert them to underscores. + $alias =~ tr/./_/; + $sql .= " AS $alias"; + } + else { + $sql = $column; } - return @sql_fields; + push(@sql_fields, $sql); + } + return @sql_fields; } ################################ @@ -1035,85 +982,83 @@ sub _sql_select { # The "order" that was requested by the consumer, exactly as it was # requested. -sub _input_order { @{ $_[0]->{'order'} || [] } } +sub _input_order { @{$_[0]->{'order'} || []} } + # Requested order with invalid values removed and old names translated sub _valid_order { - my ($self) = @_; - return map { ($self->_validate_order_column($_)) } $self->_input_order; + my ($self) = @_; + return map { ($self->_validate_order_column($_)) } $self->_input_order; } + # The valid order with just the column names, and no ASC or DESC. sub _valid_order_columns { - my ($self) = @_; - return map { (split_order_term($_))[0] } $self->_valid_order; + my ($self) = @_; + return map { (split_order_term($_))[0] } $self->_valid_order; } sub _validate_order_column { - my ($self, $order_item) = @_; + my ($self, $order_item) = @_; - # Translate old column names - my ($field, $direction) = split_order_term($order_item); - $field = $self->_translate_old_column($field); + # Translate old column names + my ($field, $direction) = split_order_term($order_item); + $field = $self->_translate_old_column($field); - # Only accept valid columns - return if (!exists $self->COLUMNS->{$field}); + # Only accept valid columns + return if (!exists $self->COLUMNS->{$field}); - # Relevance column can be used only with one or more fulltext searches - return if ($field eq 'relevance' && !$self->COLUMNS->{$field}->{name}); + # Relevance column can be used only with one or more fulltext searches + return if ($field eq 'relevance' && !$self->COLUMNS->{$field}->{name}); - $direction = " $direction" if $direction; - return "$field$direction"; + $direction = " $direction" if $direction; + return "$field$direction"; } # A hashref that describes all the special stuff that has to be done # for various fields if they go into the ORDER BY clause. sub _special_order { - my ($self) = @_; - return $self->{special_order} if $self->{special_order}; - - my %special_order = %{ SPECIAL_ORDER() }; - my $select_fields = Bugzilla->fields({ type => FIELD_TYPE_SINGLE_SELECT }); - foreach my $field (@$select_fields) { - next if $field->is_abnormal; - my $name = $field->name; - $special_order{$name} = { - order => ["map_$name.sortkey", "map_$name.value"], - join => { - table => $name, - from => "bugs.$name", - to => "value", - join => 'INNER', - } - }; - } - $self->{special_order} = \%special_order; - return $self->{special_order}; + my ($self) = @_; + return $self->{special_order} if $self->{special_order}; + + my %special_order = %{SPECIAL_ORDER()}; + my $select_fields = Bugzilla->fields({type => FIELD_TYPE_SINGLE_SELECT}); + foreach my $field (@$select_fields) { + next if $field->is_abnormal; + my $name = $field->name; + $special_order{$name} = { + order => ["map_$name.sortkey", "map_$name.value"], + join => {table => $name, from => "bugs.$name", to => "value", join => 'INNER',} + }; + } + $self->{special_order} = \%special_order; + return $self->{special_order}; } sub _sql_order_by { - my ($self) = @_; - if (!$self->{sql_order_by}) { - my @order_by = map { $self->_translate_order_by_column($_) } - $self->_valid_order; - $self->{sql_order_by} = \@order_by; - } - return @{ $self->{sql_order_by} }; + my ($self) = @_; + if (!$self->{sql_order_by}) { + my @order_by + = map { $self->_translate_order_by_column($_) } $self->_valid_order; + $self->{sql_order_by} = \@order_by; + } + return @{$self->{sql_order_by}}; } sub _translate_order_by_column { - my ($self, $order_by_item) = @_; - - my ($field, $direction) = split_order_term($order_by_item); - - $direction = '' if lc($direction) eq 'asc'; - my $special_order = $self->_special_order->{$field}->{order}; - # Standard fields have underscores in their SELECT alias instead - # of a period (because aliases can't have periods). - $field =~ s/\./_/g; - my @items = $special_order ? @$special_order : $field; - if (lc($direction) eq 'desc') { - @items = map { "$_ DESC" } @items; - } - return @items; + my ($self, $order_by_item) = @_; + + my ($field, $direction) = split_order_term($order_by_item); + + $direction = '' if lc($direction) eq 'asc'; + my $special_order = $self->_special_order->{$field}->{order}; + + # Standard fields have underscores in their SELECT alias instead + # of a period (because aliases can't have periods). + $field =~ s/\./_/g; + my @items = $special_order ? @$special_order : $field; + if (lc($direction) eq 'desc') { + @items = map {"$_ DESC"} @items; + } + return @items; } ############################# @@ -1121,32 +1066,30 @@ sub _translate_order_by_column { ############################# sub _sql_limit { - my ($self) = @_; - my $limit = $self->_params->{limit}; - my $offset = $self->_params->{offset}; - - my $max_results = Bugzilla->params->{'max_search_results'}; - if (!$self->{allow_unlimited} && (!$limit || $limit > $max_results)) { - $limit = $max_results; - } - - if (defined($offset) && !$limit) { - $limit = INT_MAX; - } - if (defined $limit) { - detaint_natural($limit) - || ThrowCodeError('param_must_be_numeric', - { function => 'Bugzilla::Search::new', - param => 'limit' }); - if (defined $offset) { - detaint_natural($offset) - || ThrowCodeError('param_must_be_numeric', - { function => 'Bugzilla::Search::new', - param => 'offset' }); - } - return Bugzilla->dbh->sql_limit($limit, $offset); - } - return ''; + my ($self) = @_; + my $limit = $self->_params->{limit}; + my $offset = $self->_params->{offset}; + + my $max_results = Bugzilla->params->{'max_search_results'}; + if (!$self->{allow_unlimited} && (!$limit || $limit > $max_results)) { + $limit = $max_results; + } + + if (defined($offset) && !$limit) { + $limit = INT_MAX; + } + if (defined $limit) { + detaint_natural($limit) + || ThrowCodeError('param_must_be_numeric', + {function => 'Bugzilla::Search::new', param => 'limit'}); + if (defined $offset) { + detaint_natural($offset) + || ThrowCodeError('param_must_be_numeric', + {function => 'Bugzilla::Search::new', param => 'offset'}); + } + return Bugzilla->dbh->sql_limit($limit, $offset); + } + return ''; } ############################ @@ -1154,176 +1097,176 @@ sub _sql_limit { ############################ sub _column_join { - my ($self, $field) = @_; - # The _realname fields require the same join as the username fields. - $field =~ s/_realname$//; - my $column_joins = $self->_get_column_joins(); - my $join_info = $column_joins->{$field}; - if ($join_info) { - # Don't allow callers to modify the constant. - $join_info = dclone($join_info); - } - else { - if ($self->_multi_select_fields->{$field}) { - $join_info = { table => "bug_$field" }; - } - } - if ($join_info and !$join_info->{as}) { - $join_info = dclone($join_info); - $join_info->{as} = "map_$field"; + my ($self, $field) = @_; + + # The _realname fields require the same join as the username fields. + $field =~ s/_realname$//; + my $column_joins = $self->_get_column_joins(); + my $join_info = $column_joins->{$field}; + if ($join_info) { + + # Don't allow callers to modify the constant. + $join_info = dclone($join_info); + } + else { + if ($self->_multi_select_fields->{$field}) { + $join_info = {table => "bug_$field"}; } - return $join_info ? $join_info : (); + } + if ($join_info and !$join_info->{as}) { + $join_info = dclone($join_info); + $join_info->{as} = "map_$field"; + } + return $join_info ? $join_info : (); } # Sometimes we join the same table more than once. In this case, we # want to AND all the various critiera that were used in both joins. sub _combine_joins { - my ($self, $joins) = @_; - my @result; - while(my $join = shift @$joins) { - my $name = $join->{as}; - my ($others_like_me, $the_rest) = part { $_->{as} eq $name ? 0 : 1 } - @$joins; - if ($others_like_me) { - my $from = $join->{from}; - my $to = $join->{to}; - # Sanity check to make sure that we have the same from and to - # for all the same-named joins. - if ($from) { - all { $_->{from} eq $from } @$others_like_me - or die "Not all same-named joins have identical 'from': " - . Dumper($join, $others_like_me); - } - if ($to) { - all { $_->{to} eq $to } @$others_like_me - or die "Not all same-named joins have identical 'to': " - . Dumper($join, $others_like_me); - } - - # We don't need to call uniq here--translate_join will do that - # for us. - my @conditions = map { @{ $_->{extra} || [] } } - ($join, @$others_like_me); - $join->{extra} = \@conditions; - $joins = $the_rest; - } - push(@result, $join); - } - - return @result; + my ($self, $joins) = @_; + my @result; + while (my $join = shift @$joins) { + my $name = $join->{as}; + my ($others_like_me, $the_rest) = part { $_->{as} eq $name ? 0 : 1 } + @$joins; + if ($others_like_me) { + my $from = $join->{from}; + my $to = $join->{to}; + + # Sanity check to make sure that we have the same from and to + # for all the same-named joins. + if ($from) { + all { $_->{from} eq $from } @$others_like_me + or die "Not all same-named joins have identical 'from': " + . Dumper($join, $others_like_me); + } + if ($to) { + all { $_->{to} eq $to } @$others_like_me + or die "Not all same-named joins have identical 'to': " + . Dumper($join, $others_like_me); + } + + # We don't need to call uniq here--translate_join will do that + # for us. + my @conditions = map { @{$_->{extra} || []} } ($join, @$others_like_me); + $join->{extra} = \@conditions; + $joins = $the_rest; + } + push(@result, $join); + } + + return @result; } # Takes all the "then_to" items and just puts them as the next item in # the array. Right now this only does one level of "then_to", but we # could re-write this to handle then_to recursively if we need more levels. sub _extract_then_to { - my ($self, $joins) = @_; - my @result; - foreach my $join (@$joins) { - push(@result, $join); - if (my $then_to = $join->{then_to}) { - push(@result, $then_to); - } + my ($self, $joins) = @_; + my @result; + foreach my $join (@$joins) { + push(@result, $join); + if (my $then_to = $join->{then_to}) { + push(@result, $then_to); } - return @result; + } + return @result; } # JOIN statements for the SELECT and ORDER BY columns. This should not be # called until the moment it is needed, because _select_columns might be # modified by the charts. sub _select_order_joins { - my ($self) = @_; - my @joins; - foreach my $field ($self->_select_columns) { - my @column_join = $self->_column_join($field); - push(@joins, @column_join); - } - foreach my $field ($self->_valid_order_columns) { - my $join_info = $self->_special_order->{$field}->{join}; - if ($join_info) { - # Don't let callers modify SPECIAL_ORDER. - $join_info = dclone($join_info); - if (!$join_info->{as}) { - $join_info->{as} = "map_$field"; - } - push(@joins, $join_info); - } + my ($self) = @_; + my @joins; + foreach my $field ($self->_select_columns) { + my @column_join = $self->_column_join($field); + push(@joins, @column_join); + } + foreach my $field ($self->_valid_order_columns) { + my $join_info = $self->_special_order->{$field}->{join}; + if ($join_info) { + + # Don't let callers modify SPECIAL_ORDER. + $join_info = dclone($join_info); + if (!$join_info->{as}) { + $join_info->{as} = "map_$field"; + } + push(@joins, $join_info); } - return @joins; + } + return @joins; } # These are the joins that are *always* in the FROM clause. sub _standard_joins { - my ($self) = @_; - my $user = $self->_user; - my @joins; - return () if $self->{_no_security_check}; - - my $security_join = { - table => 'bug_group_map', - as => 'security_map', - }; - push(@joins, $security_join); + my ($self) = @_; + my $user = $self->_user; + my @joins; + return () if $self->{_no_security_check}; - if ($user->id) { - # See also _standard_joins for the other half of the below statement - if (!Bugzilla->params->{'or_groups'}) { - $security_join->{extra} = - ["NOT (" . $user->groups_in_sql('security_map.group_id') . ")"]; - } - - my $security_cc_join = { - table => 'cc', - as => 'security_cc', - extra => ['security_cc.who = ' . $user->id], - }; - push(@joins, $security_cc_join); + my $security_join = {table => 'bug_group_map', as => 'security_map',}; + push(@joins, $security_join); + + if ($user->id) { + + # See also _standard_joins for the other half of the below statement + if (!Bugzilla->params->{'or_groups'}) { + $security_join->{extra} + = ["NOT (" . $user->groups_in_sql('security_map.group_id') . ")"]; } - - return @joins; + + my $security_cc_join = { + table => 'cc', + as => 'security_cc', + extra => ['security_cc.who = ' . $user->id], + }; + push(@joins, $security_cc_join); + } + + return @joins; } sub _sql_from { - my ($self, $joins_input) = @_; - my @joins = ($self->_standard_joins, $self->_select_order_joins, - @$joins_input); - @joins = $self->_extract_then_to(\@joins); - @joins = $self->_combine_joins(\@joins); - my @join_sql = map { $self->_translate_join($_) } @joins; - return "bugs\n" . join("\n", @join_sql); + my ($self, $joins_input) = @_; + my @joins = ($self->_standard_joins, $self->_select_order_joins, @$joins_input); + @joins = $self->_extract_then_to(\@joins); + @joins = $self->_combine_joins(\@joins); + my @join_sql = map { $self->_translate_join($_) } @joins; + return "bugs\n" . join("\n", @join_sql); } # This takes a join data structure and turns it into actual JOIN SQL. sub _translate_join { - my ($self, $join_info) = @_; - - die "join with no table: " . Dumper($join_info) if !$join_info->{table}; - die "join with no 'as': " . Dumper($join_info) if !$join_info->{as}; - - my $from_table = $join_info->{bugs_table} || "bugs"; - my $from = $join_info->{from} || "bug_id"; - if ($from =~ /^(\w+)\.(\w+)$/) { - ($from_table, $from) = ($1, $2); - } - my $table = $join_info->{table}; - my $name = $join_info->{as}; - my $to = $join_info->{to} || "bug_id"; - my $join = $join_info->{join} || 'LEFT'; - my @extra = @{ $join_info->{extra} || [] }; - $name =~ s/\./_/g; - - # If a term contains ORs, we need to put parens around the condition. - # This is a pretty weak test, but it's actually OK to put parens - # around too many things. - @extra = map { $_ =~ /\bOR\b/i ? "($_)" : $_ } @extra; - my $extra_condition = join(' AND ', uniq @extra); - if ($extra_condition) { - $extra_condition = " AND $extra_condition"; - } - - my @join_sql = "$join JOIN $table AS $name" - . " ON $from_table.$from = $name.$to$extra_condition"; - return @join_sql; + my ($self, $join_info) = @_; + + die "join with no table: " . Dumper($join_info) if !$join_info->{table}; + die "join with no 'as': " . Dumper($join_info) if !$join_info->{as}; + + my $from_table = $join_info->{bugs_table} || "bugs"; + my $from = $join_info->{from} || "bug_id"; + if ($from =~ /^(\w+)\.(\w+)$/) { + ($from_table, $from) = ($1, $2); + } + my $table = $join_info->{table}; + my $name = $join_info->{as}; + my $to = $join_info->{to} || "bug_id"; + my $join = $join_info->{join} || 'LEFT'; + my @extra = @{$join_info->{extra} || []}; + $name =~ s/\./_/g; + + # If a term contains ORs, we need to put parens around the condition. + # This is a pretty weak test, but it's actually OK to put parens + # around too many things. + @extra = map { $_ =~ /\bOR\b/i ? "($_)" : $_ } @extra; + my $extra_condition = join(' AND ', uniq @extra); + if ($extra_condition) { + $extra_condition = " AND $extra_condition"; + } + + my @join_sql = "$join JOIN $table AS $name" + . " ON $from_table.$from = $name.$to$extra_condition"; + return @join_sql; } ############################# @@ -1336,54 +1279,60 @@ sub _translate_join { # The terms that are always in the WHERE clause. These implement bug # group security. sub _standard_where { - my ($self) = @_; - return ('1=1') if $self->{_no_security_check}; - # If replication lags badly between the shadow db and the main DB, - # it's possible for bugs to show up in searches before their group - # controls are properly set. To prevent this, when initially creating - # bugs we set their creation_ts to NULL, and don't give them a creation_ts - # until their group controls are set. So if a bug has a NULL creation_ts, - # it shouldn't show up in searches at all. - my @where = ('bugs.creation_ts IS NOT NULL'); - - my $user = $self->_user; - my $security_term = ''; - # See also _standard_joins for the other half of the below statement - if (Bugzilla->params->{'or_groups'}) { - $security_term .= " (security_map.group_id IS NULL OR security_map.group_id IN (" . $user->groups_as_string . "))"; - } - else { - $security_term = 'security_map.group_id IS NULL'; - } - - if ($user->id) { - my $userid = $user->id; - # This indentation makes the resulting SQL more readable. - $security_term .= <{_no_security_check}; + + # If replication lags badly between the shadow db and the main DB, + # it's possible for bugs to show up in searches before their group + # controls are properly set. To prevent this, when initially creating + # bugs we set their creation_ts to NULL, and don't give them a creation_ts + # until their group controls are set. So if a bug has a NULL creation_ts, + # it shouldn't show up in searches at all. + my @where = ('bugs.creation_ts IS NOT NULL'); + + my $user = $self->_user; + my $security_term = ''; + + # See also _standard_joins for the other half of the below statement + if (Bugzilla->params->{'or_groups'}) { + $security_term + .= " (security_map.group_id IS NULL OR security_map.group_id IN (" + . $user->groups_as_string . "))"; + } + else { + $security_term = 'security_map.group_id IS NULL'; + } + + if ($user->id) { + my $userid = $user->id; + + # This indentation makes the resulting SQL more readable. + $security_term .= <params->{'useqacontact'}) { - $security_term.= " OR bugs.qa_contact = $userid"; - } - $security_term = "($security_term)"; + if (Bugzilla->params->{'useqacontact'}) { + $security_term .= " OR bugs.qa_contact = $userid"; } + $security_term = "($security_term)"; + } - push(@where, $security_term); + push(@where, $security_term); - return @where; + return @where; } sub _sql_where { - my ($self, $main_clause) = @_; - # The newline and this particular spacing makes the resulting - # SQL a bit more readable for debugging. - my $where = join("\n AND ", $self->_standard_where); - my $clause_sql = $main_clause->as_string; - $where .= "\n AND " . $clause_sql if $clause_sql; - return $where; + my ($self, $main_clause) = @_; + + # The newline and this particular spacing makes the resulting + # SQL a bit more readable for debugging. + my $where = join("\n AND ", $self->_standard_where); + my $clause_sql = $main_clause->as_string; + $where .= "\n AND " . $clause_sql if $clause_sql; + return $where; } ################################ @@ -1393,40 +1342,40 @@ sub _sql_where { # And these are the fields that we have to do GROUP BY for in DBs # that are more strict about putting everything into GROUP BY. sub _sql_group_by { - my ($self) = @_; - - # Strict DBs require every element from the SELECT to be in the GROUP BY, - # unless that element is being used in an aggregate function. - my @extra_group_by; - foreach my $column ($self->_select_columns) { - next if $self->_skip_group_by->{$column}; - my $sql = $self->COLUMNS->{$column}->{name} // $column; - push(@extra_group_by, $sql); - } + my ($self) = @_; - # And all items from ORDER BY must be in the GROUP BY. The above loop - # doesn't catch items that were put into the ORDER BY from SPECIAL_ORDER. - foreach my $column ($self->_valid_order_columns) { - my $special_order = $self->_special_order->{$column}->{order}; - next if !$special_order; - push(@extra_group_by, @$special_order); - } - - @extra_group_by = uniq @extra_group_by; - - # bug_id is the only field we actually group by. - return ('bugs.bug_id', join(',', @extra_group_by)); + # Strict DBs require every element from the SELECT to be in the GROUP BY, + # unless that element is being used in an aggregate function. + my @extra_group_by; + foreach my $column ($self->_select_columns) { + next if $self->_skip_group_by->{$column}; + my $sql = $self->COLUMNS->{$column}->{name} // $column; + push(@extra_group_by, $sql); + } + + # And all items from ORDER BY must be in the GROUP BY. The above loop + # doesn't catch items that were put into the ORDER BY from SPECIAL_ORDER. + foreach my $column ($self->_valid_order_columns) { + my $special_order = $self->_special_order->{$column}->{order}; + next if !$special_order; + push(@extra_group_by, @$special_order); + } + + @extra_group_by = uniq @extra_group_by; + + # bug_id is the only field we actually group by. + return ('bugs.bug_id', join(',', @extra_group_by)); } # A helper for _sql_group_by. sub _skip_group_by { - my ($self) = @_; - return $self->{skip_group_by} if $self->{skip_group_by}; - my @skip_list = GROUP_BY_SKIP; - push(@skip_list, keys %{ $self->_multi_select_fields }); - my %skip_hash = map { $_ => 1 } @skip_list; - $self->{skip_group_by} = \%skip_hash; - return $self->{skip_group_by}; + my ($self) = @_; + return $self->{skip_group_by} if $self->{skip_group_by}; + my @skip_list = GROUP_BY_SKIP; + push(@skip_list, keys %{$self->_multi_select_fields}); + my %skip_hash = map { $_ => 1 } @skip_list; + $self->{skip_group_by} = \%skip_hash; + return $self->{skip_group_by}; } ############################################## @@ -1435,244 +1384,255 @@ sub _skip_group_by { # Backwards compatibility for old field names. sub _convert_old_params { - my ($self) = @_; - my $params = $self->_params; - - # bugidtype has different values in modern Search.pm. - if (defined $params->{'bugidtype'}) { - my $value = $params->{'bugidtype'}; - $params->{'bugidtype'} = $value eq 'exclude' ? 'nowords' : 'anyexact'; - } - - foreach my $old_name (keys %{ FIELD_MAP() }) { - if (defined $params->{$old_name}) { - my $new_name = FIELD_MAP->{$old_name}; - $params->{$new_name} = delete $params->{$old_name}; - } + my ($self) = @_; + my $params = $self->_params; + + # bugidtype has different values in modern Search.pm. + if (defined $params->{'bugidtype'}) { + my $value = $params->{'bugidtype'}; + $params->{'bugidtype'} = $value eq 'exclude' ? 'nowords' : 'anyexact'; + } + + foreach my $old_name (keys %{FIELD_MAP()}) { + if (defined $params->{$old_name}) { + my $new_name = FIELD_MAP->{$old_name}; + $params->{$new_name} = delete $params->{$old_name}; } + } } # This parses all the standard search parameters except for the boolean # charts. sub _special_charts { - my ($self) = @_; - $self->_convert_old_params(); - $self->_special_parse_bug_status(); - $self->_special_parse_resolution(); - my $clause = new Bugzilla::Search::Clause(); - $clause->add( $self->_parse_basic_fields() ); - $clause->add( $self->_special_parse_email() ); - $clause->add( $self->_special_parse_chfield() ); - $clause->add( $self->_special_parse_deadline() ); - return $clause; + my ($self) = @_; + $self->_convert_old_params(); + $self->_special_parse_bug_status(); + $self->_special_parse_resolution(); + my $clause = new Bugzilla::Search::Clause(); + $clause->add($self->_parse_basic_fields()); + $clause->add($self->_special_parse_email()); + $clause->add($self->_special_parse_chfield()); + $clause->add($self->_special_parse_deadline()); + return $clause; } sub _parse_basic_fields { - my ($self) = @_; - my $params = $self->_params; - my $chart_fields = $self->_chart_fields; - - my $clause = new Bugzilla::Search::Clause(); - foreach my $field_name (keys %$chart_fields) { - # CGI params shouldn't have periods in them, so we only accept - # period-separated fields with underscores where the periods go. - my $param_name = $field_name; - $param_name =~ s/\./_/g; - my @values = $self->_param_array($param_name); - next if !@values; - my $default_op = $param_name eq 'content' ? 'matches' : 'anyexact'; - my $operator = $params->{"${param_name}_type"} || $default_op; - # Fields that are displayed as multi-selects are passed as arrays, - # so that they can properly search values that contain commas. - # However, other fields are sent as strings, so that they are properly - # split on commas if required. - my $field = $chart_fields->{$field_name}; - my $pass_value; - if ($field->is_select or $field->name eq 'version' - or $field->name eq 'target_milestone') - { - $pass_value = \@values; - } - else { - $pass_value = join(',', @values); - } - $clause->add($field_name, $operator, $pass_value); + my ($self) = @_; + my $params = $self->_params; + my $chart_fields = $self->_chart_fields; + + my $clause = new Bugzilla::Search::Clause(); + foreach my $field_name (keys %$chart_fields) { + + # CGI params shouldn't have periods in them, so we only accept + # period-separated fields with underscores where the periods go. + my $param_name = $field_name; + $param_name =~ s/\./_/g; + my @values = $self->_param_array($param_name); + next if !@values; + my $default_op = $param_name eq 'content' ? 'matches' : 'anyexact'; + my $operator = $params->{"${param_name}_type"} || $default_op; + + # Fields that are displayed as multi-selects are passed as arrays, + # so that they can properly search values that contain commas. + # However, other fields are sent as strings, so that they are properly + # split on commas if required. + my $field = $chart_fields->{$field_name}; + my $pass_value; + if ( $field->is_select + or $field->name eq 'version' + or $field->name eq 'target_milestone') + { + $pass_value = \@values; + } + else { + $pass_value = join(',', @values); } - return @{$clause->children} ? $clause : undef; + $clause->add($field_name, $operator, $pass_value); + } + return @{$clause->children} ? $clause : undef; } sub _special_parse_bug_status { - my ($self) = @_; - my $params = $self->_params; - return if !defined $params->{'bug_status'}; - # We want to allow the bug_status_type parameter to work normally, - # meaning that this special code should only be activated if we are - # doing the normal "anyexact" search on bug_status. - return if (defined $params->{'bug_status_type'} - and $params->{'bug_status_type'} ne 'anyexact'); - - my @bug_status = $self->_param_array('bug_status'); - # Also include inactive bug statuses, as you can query them. - my $legal_statuses = $self->_chart_fields->{'bug_status'}->legal_values; - - # If the status contains __open__ or __closed__, translate those - # into their equivalent lists of open and closed statuses. - if (grep { $_ eq '__open__' } @bug_status) { - my @open = grep { $_->is_open } @$legal_statuses; - @open = map { $_->name } @open; - push(@bug_status, @open); - } - if (grep { $_ eq '__closed__' } @bug_status) { - my @closed = grep { not $_->is_open } @$legal_statuses; - @closed = map { $_->name } @closed; - push(@bug_status, @closed); - } - - @bug_status = uniq @bug_status; - my $all = grep { $_ eq "__all__" } @bug_status; - # This will also handle removing __open__ and __closed__ for us - # (__all__ too, which is why we check for it above, first). - @bug_status = _valid_values(\@bug_status, $legal_statuses); - - # If the user has selected every status, change to selecting none. - # This is functionally equivalent, but quite a lot faster. - if ($all or scalar(@bug_status) == scalar(@$legal_statuses)) { - delete $params->{'bug_status'}; - } - else { - $params->{'bug_status'} = \@bug_status; - } + my ($self) = @_; + my $params = $self->_params; + return if !defined $params->{'bug_status'}; + + # We want to allow the bug_status_type parameter to work normally, + # meaning that this special code should only be activated if we are + # doing the normal "anyexact" search on bug_status. + return + if (defined $params->{'bug_status_type'} + and $params->{'bug_status_type'} ne 'anyexact'); + + my @bug_status = $self->_param_array('bug_status'); + + # Also include inactive bug statuses, as you can query them. + my $legal_statuses = $self->_chart_fields->{'bug_status'}->legal_values; + + # If the status contains __open__ or __closed__, translate those + # into their equivalent lists of open and closed statuses. + if (grep { $_ eq '__open__' } @bug_status) { + my @open = grep { $_->is_open } @$legal_statuses; + @open = map { $_->name } @open; + push(@bug_status, @open); + } + if (grep { $_ eq '__closed__' } @bug_status) { + my @closed = grep { not $_->is_open } @$legal_statuses; + @closed = map { $_->name } @closed; + push(@bug_status, @closed); + } + + @bug_status = uniq @bug_status; + my $all = grep { $_ eq "__all__" } @bug_status; + + # This will also handle removing __open__ and __closed__ for us + # (__all__ too, which is why we check for it above, first). + @bug_status = _valid_values(\@bug_status, $legal_statuses); + + # If the user has selected every status, change to selecting none. + # This is functionally equivalent, but quite a lot faster. + if ($all or scalar(@bug_status) == scalar(@$legal_statuses)) { + delete $params->{'bug_status'}; + } + else { + $params->{'bug_status'} = \@bug_status; + } } sub _special_parse_chfield { - my ($self) = @_; - my $params = $self->_params; - - my $date_from = trim(lc($params->{'chfieldfrom'} || '')); - my $date_to = trim(lc($params->{'chfieldto'} || '')); - $date_from = '' if $date_from eq 'now'; - $date_to = '' if $date_to eq 'now'; - my @fields = $self->_param_array('chfield'); - my $value_to = $params->{'chfieldvalue'}; - $value_to = '' if !defined $value_to; - - @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 - # if there is a "from" date specified. It doesn't conflict with - # searching [Bug creation], because a bug's delta_ts is set to - # its creation_ts when it is created. So this just gives the - # database an additional index to possibly choose, on a table that - # is smaller than bugs_activity. - if ($date_from ne '') { - $clause->add('delta_ts', 'greaterthaneq', $date_from); - } - # It's not normally safe to do it for "to" dates, though--"chfieldto" means - # "a field that changed before this date", and delta_ts could be either - # later or earlier than that, if we're searching for the time that a field - # changed. However, chfieldto all by itself, without any chfieldvalue or - # chfield, means "just search delta_ts", and so we still want that to - # work. - if ($date_to ne '' and !@fields and $value_to eq '') { - $clause->add('delta_ts', 'lessthaneq', $date_to); - } - - # 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'); - - 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; + my ($self) = @_; + my $params = $self->_params; + + my $date_from = trim(lc($params->{'chfieldfrom'} || '')); + my $date_to = trim(lc($params->{'chfieldto'} || '')); + $date_from = '' if $date_from eq 'now'; + $date_to = '' if $date_to eq 'now'; + my @fields = $self->_param_array('chfield'); + my $value_to = $params->{'chfieldvalue'}; + $value_to = '' if !defined $value_to; + + @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 + # if there is a "from" date specified. It doesn't conflict with + # searching [Bug creation], because a bug's delta_ts is set to + # its creation_ts when it is created. So this just gives the + # database an additional index to possibly choose, on a table that + # is smaller than bugs_activity. + if ($date_from ne '') { + $clause->add('delta_ts', 'greaterthaneq', $date_from); + } + + # It's not normally safe to do it for "to" dates, though--"chfieldto" means + # "a field that changed before this date", and delta_ts could be either + # later or earlier than that, if we're searching for the time that a field + # changed. However, chfieldto all by itself, without any chfieldvalue or + # chfield, means "just search delta_ts", and so we still want that to + # work. + if ($date_to ne '' and !@fields and $value_to eq '') { + $clause->add('delta_ts', 'lessthaneq', $date_to); + } + + # 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'); + + 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; } sub _special_parse_deadline { - my ($self) = @_; - my $params = $self->_params; + my ($self) = @_; + my $params = $self->_params; - my $clause = new Bugzilla::Search::Clause(); - if (my $from = $params->{'deadlinefrom'}) { - $clause->add('deadline', 'greaterthaneq', $from); - } - if (my $to = $params->{'deadlineto'}) { - $clause->add('deadline', 'lessthaneq', $to); - } + my $clause = new Bugzilla::Search::Clause(); + if (my $from = $params->{'deadlinefrom'}) { + $clause->add('deadline', 'greaterthaneq', $from); + } + if (my $to = $params->{'deadlineto'}) { + $clause->add('deadline', 'lessthaneq', $to); + } - return @{$clause->children} ? $clause : undef; + return @{$clause->children} ? $clause : undef; } sub _special_parse_email { - my ($self) = @_; - my $params = $self->_params; - - my @email_params = grep { $_ =~ /^email\d+$/ } keys %$params; - - my $clause = new Bugzilla::Search::Clause(); - foreach my $param (@email_params) { - $param =~ /(\d+)$/; - my $id = $1; - my $email = trim($params->{"email$id"}); - next if !$email; - my $type = $params->{"emailtype$id"} || 'anyexact'; - # 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)) { - if ($params->{"email$field$id"}) { - $or_clause->add($field, $type, $email); - } - } - if ($params->{"emaillongdesc$id"}) { - $or_clause->add("commenter", $type, $email); - } - - $clause->add($or_clause); + my ($self) = @_; + my $params = $self->_params; + + my @email_params = grep { $_ =~ /^email\d+$/ } keys %$params; + + my $clause = new Bugzilla::Search::Clause(); + foreach my $param (@email_params) { + $param =~ /(\d+)$/; + my $id = $1; + my $email = trim($params->{"email$id"}); + next if !$email; + my $type = $params->{"emailtype$id"} || 'anyexact'; + + # 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)) { + if ($params->{"email$field$id"}) { + $or_clause->add($field, $type, $email); + } + } + if ($params->{"emaillongdesc$id"}) { + $or_clause->add("commenter", $type, $email); } - return @{$clause->children} ? $clause : undef; + $clause->add($or_clause); + } + + return @{$clause->children} ? $clause : undef; } sub _special_parse_resolution { - my ($self) = @_; - my $params = $self->_params; - return if !defined $params->{'resolution'}; - - my @resolution = $self->_param_array('resolution'); - my $legal_resolutions = $self->_chart_fields->{resolution}->legal_values; - @resolution = _valid_values(\@resolution, $legal_resolutions, '---'); - if (scalar(@resolution) == scalar(@$legal_resolutions)) { - delete $params->{'resolution'}; - } + my ($self) = @_; + my $params = $self->_params; + return if !defined $params->{'resolution'}; + + my @resolution = $self->_param_array('resolution'); + my $legal_resolutions = $self->_chart_fields->{resolution}->legal_values; + @resolution = _valid_values(\@resolution, $legal_resolutions, '---'); + if (scalar(@resolution) == scalar(@$legal_resolutions)) { + delete $params->{'resolution'}; + } } sub _valid_values { - my ($input, $valid, $extra_value) = @_; - my @result; - foreach my $item (@$input) { - $item = trim($item); - if (defined $extra_value and $item eq $extra_value) { - push(@result, $item); - } - elsif (grep { $_->name eq $item } @$valid) { - push(@result, $item); - } + my ($input, $valid, $extra_value) = @_; + my @result; + foreach my $item (@$input) { + $item = trim($item); + if (defined $extra_value and $item eq $extra_value) { + push(@result, $item); } - return @result; + elsif (grep { $_->name eq $item } @$valid) { + push(@result, $item); + } + } + return @result; } ###################################### @@ -1680,239 +1640,247 @@ sub _valid_values { ###################################### sub _charts_to_conditions { - my ($self) = @_; - - my $clause = $self->_charts; - my @joins; - $clause->walk_conditions(sub { - my ($clause, $condition) = @_; - return if !$condition->translated; - push(@joins, @{ $condition->translated->{joins} }); - }); - return (\@joins, $clause); + my ($self) = @_; + + my $clause = $self->_charts; + my @joins; + $clause->walk_conditions(sub { + my ($clause, $condition) = @_; + return if !$condition->translated; + push(@joins, @{$condition->translated->{joins}}); + }); + return (\@joins, $clause); } sub _charts { - my ($self) = @_; - - my $clause = $self->_params_to_data_structure(); - my $chart_id = 0; - $clause->walk_conditions(sub { $self->_handle_chart($chart_id++, @_) }); - return $clause; + my ($self) = @_; + + my $clause = $self->_params_to_data_structure(); + my $chart_id = 0; + $clause->walk_conditions(sub { $self->_handle_chart($chart_id++, @_) }); + return $clause; } sub _params_to_data_structure { - my ($self) = @_; - - # First we get the "special" charts, representing all the normal - # fields on the search page. This may modify _params, so it needs to - # happen first. - my $clause = $self->_special_charts; - - # Then we process the old Boolean Charts input format. - $clause->add( $self->_boolean_charts ); - - # And then process the modern "custom search" format. - $clause->add( $self->_custom_search ); - - return $clause; -} + my ($self) = @_; -sub _boolean_charts { - my ($self) = @_; - - my $params = $self->_params; - my @param_list = keys %$params; - - my @all_field_params = grep { /^field-?\d+/ } @param_list; - my @chart_ids = map { /^field(-?\d+)/; $1 } @all_field_params; - @chart_ids = sort { $a <=> $b } uniq @chart_ids; - - my $clause = new Bugzilla::Search::Clause(); - foreach my $chart_id (@chart_ids) { - my @all_and = grep { /^field$chart_id-\d+/ } @param_list; - my @and_ids = map { /^field$chart_id-(\d+)/; $1 } @all_and; - @and_ids = sort { $a <=> $b } uniq @and_ids; - - my $and_clause = new Bugzilla::Search::Clause(); - foreach my $and_id (@and_ids) { - my @all_or = grep { /^field$chart_id-$and_id-\d+/ } @param_list; - my @or_ids = map { /^field$chart_id-$and_id-(\d+)/; $1 } @all_or; - @or_ids = sort { $a <=> $b } uniq @or_ids; - - my $or_clause = new Bugzilla::Search::Clause('OR'); - foreach my $or_id (@or_ids) { - my $identifier = "$chart_id-$and_id-$or_id"; - my $field = $params->{"field$identifier"}; - my $operator = $params->{"type$identifier"}; - my $value = $params->{"value$identifier"}; - # no-value operators ignore the value, however a value needs to be set - $value = ' ' if $operator && grep { $_ eq $operator } NO_VALUE_OPERATORS; - $or_clause->add($field, $operator, $value); - } - $and_clause->add($or_clause); - $and_clause->negate(1) if $params->{"negate$chart_id"}; - } - $clause->add($and_clause); - } + # First we get the "special" charts, representing all the normal + # fields on the search page. This may modify _params, so it needs to + # happen first. + my $clause = $self->_special_charts; - return @{$clause->children} ? $clause : undef; -} + # Then we process the old Boolean Charts input format. + $clause->add($self->_boolean_charts); -sub _custom_search { - my ($self) = @_; - my $params = $self->_params; + # And then process the modern "custom search" format. + $clause->add($self->_custom_search); - my @field_ids = $self->_field_ids; - return unless scalar @field_ids; + return $clause; +} - my $joiner = $params->{j_top} || ''; - my $current_clause = $joiner eq 'AND_G' - ? new Bugzilla::Search::ClauseGroup() - : new Bugzilla::Search::Clause($joiner); +sub _boolean_charts { + my ($self) = @_; + + my $params = $self->_params; + my @param_list = keys %$params; + + my @all_field_params = grep {/^field-?\d+/} @param_list; + my @chart_ids = map { /^field(-?\d+)/; $1 } @all_field_params; + @chart_ids = sort { $a <=> $b } uniq @chart_ids; + + my $clause = new Bugzilla::Search::Clause(); + foreach my $chart_id (@chart_ids) { + my @all_and = grep {/^field$chart_id-\d+/} @param_list; + my @and_ids = map { /^field$chart_id-(\d+)/; $1 } @all_and; + @and_ids = sort { $a <=> $b } uniq @and_ids; + + my $and_clause = new Bugzilla::Search::Clause(); + foreach my $and_id (@and_ids) { + my @all_or = grep {/^field$chart_id-$and_id-\d+/} @param_list; + my @or_ids = map { /^field$chart_id-$and_id-(\d+)/; $1 } @all_or; + @or_ids = sort { $a <=> $b } uniq @or_ids; + + my $or_clause = new Bugzilla::Search::Clause('OR'); + foreach my $or_id (@or_ids) { + my $identifier = "$chart_id-$and_id-$or_id"; + my $field = $params->{"field$identifier"}; + my $operator = $params->{"type$identifier"}; + my $value = $params->{"value$identifier"}; - my @clause_stack; - foreach my $id (@field_ids) { - my $field = $params->{"f$id"}; - if ($field eq 'OP') { - my $joiner = $params->{"j$id"} || ''; - my $new_clause = $joiner eq 'AND_G' - ? new Bugzilla::Search::ClauseGroup() - : new Bugzilla::Search::Clause($joiner); - $new_clause->negate($params->{"n$id"}); - $current_clause->add($new_clause); - push(@clause_stack, $current_clause); - $current_clause = $new_clause; - next; - } - if ($field eq 'CP') { - $current_clause = pop @clause_stack; - ThrowCodeError('search_cp_without_op', { id => $id }) - if !$current_clause; - next; - } - - my $operator = $params->{"o$id"}; - my $value = $params->{"v$id"}; # no-value operators ignore the value, however a value needs to be set $value = ' ' if $operator && grep { $_ eq $operator } NO_VALUE_OPERATORS; - my $condition = condition($field, $operator, $value); - $condition->negate($params->{"n$id"}); - $current_clause->add($condition); + $or_clause->add($field, $operator, $value); + } + $and_clause->add($or_clause); + $and_clause->negate(1) if $params->{"negate$chart_id"}; } - - # We allow people to specify more OPs than CPs, so at the end of the - # loop our top clause may be still in the stack instead of being - # $current_clause. - return $clause_stack[0] || $current_clause; -} + $clause->add($and_clause); + } -sub _field_ids { - my ($self) = @_; - my $params = $self->_params; - my @param_list = keys %$params; - - my @field_params = grep { /^f\d+$/ } @param_list; - my @field_ids = map { /(\d+)/; $1 } @field_params; - @field_ids = sort { $a <=> $b } @field_ids; - return @field_ids; + return @{$clause->children} ? $clause : undef; } -sub _handle_chart { - my ($self, $chart_id, $clause, $condition) = @_; - my $dbh = Bugzilla->dbh; - my $params = $self->_params; - my ($field, $operator, $value) = $condition->fov; - return if (!defined $field or !defined $operator or !defined $value); - $field = FIELD_MAP->{$field} || $field; - - my ($string_value, $orig_value); - state $is_mysql = $dbh->isa('Bugzilla::DB::Mysql') ? 1 : 0; - - if (ref $value eq 'ARRAY') { - # Trim input and ignore blank values. - @$value = map { trim($_) } @$value; - @$value = grep { defined $_ and $_ ne '' } @$value; - return if !@$value; - $orig_value = join(',', @$value); - if ($field eq 'longdesc' && $is_mysql) { - @$value = map { _convert_unicode_characters($_) } @$value; - } - $string_value = join(',', @$value); +sub _custom_search { + my ($self) = @_; + my $params = $self->_params; + + my @field_ids = $self->_field_ids; + return unless scalar @field_ids; + + my $joiner = $params->{j_top} || ''; + my $current_clause + = $joiner eq 'AND_G' + ? new Bugzilla::Search::ClauseGroup() + : new Bugzilla::Search::Clause($joiner); + + my @clause_stack; + foreach my $id (@field_ids) { + my $field = $params->{"f$id"}; + if ($field eq 'OP') { + my $joiner = $params->{"j$id"} || ''; + my $new_clause + = $joiner eq 'AND_G' + ? new Bugzilla::Search::ClauseGroup() + : new Bugzilla::Search::Clause($joiner); + $new_clause->negate($params->{"n$id"}); + $current_clause->add($new_clause); + push(@clause_stack, $current_clause); + $current_clause = $new_clause; + next; } - else { - return if $value eq ''; - $orig_value = $value; - if ($field eq 'longdesc' && $is_mysql) { - $value = _convert_unicode_characters($value); - } - $string_value = $value; + if ($field eq 'CP') { + $current_clause = pop @clause_stack; + ThrowCodeError('search_cp_without_op', {id => $id}) if !$current_clause; + next; } - $self->_chart_fields->{$field} - or ThrowCodeError("invalid_field_name", { field => $field }); - trick_taint($field); - - # This is the field as you'd reference it in a SQL statement. - my $full_field = $field =~ /\./ ? $field : "bugs.$field"; - - # "value" and "quoted" are for search functions that always operate - # on a scalar string and never care if they were passed multiple - # parameters. If the user does pass multiple parameters, they will - # become a space-separated string for those search functions. - # - # all_values is for search functions that do operate - # on multiple values, like anyexact. - - my %search_args = ( - chart_id => $chart_id, - sequence => $chart_id, - field => $field, - full_field => $full_field, - operator => $operator, - value => $string_value, - all_values => $value, - joins => [], - bugs_table => 'bugs', - table_suffix => '', - condition => $condition, - ); - $clause->update_search_args(\%search_args); - - $search_args{quoted} = $self->_quote_unless_numeric(\%search_args); - # This should add a "term" selement to %search_args. - $self->do_search_function(\%search_args); - - # If term is left empty, then this means the criteria - # has no effect and can be ignored. - return unless $search_args{term}; - - # All the things here that don't get pulled out of - # %search_args are their original values before - # do_search_function modified them. - $self->search_description({ - field => $field, type => $operator, - value => $orig_value, term => $search_args{term}, - }); + my $operator = $params->{"o$id"}; + my $value = $params->{"v$id"}; - foreach my $join (@{ $search_args{joins} }) { - $join->{bugs_table} = $search_args{bugs_table}; - $join->{table_suffix} = $search_args{table_suffix}; - } + # no-value operators ignore the value, however a value needs to be set + $value = ' ' if $operator && grep { $_ eq $operator } NO_VALUE_OPERATORS; + my $condition = condition($field, $operator, $value); + $condition->negate($params->{"n$id"}); + $current_clause->add($condition); + } - $condition->translated(\%search_args); + # We allow people to specify more OPs than CPs, so at the end of the + # loop our top clause may be still in the stack instead of being + # $current_clause. + return $clause_stack[0] || $current_clause; +} + +sub _field_ids { + my ($self) = @_; + my $params = $self->_params; + my @param_list = keys %$params; + + my @field_params = grep {/^f\d+$/} @param_list; + my @field_ids = map { /(\d+)/; $1 } @field_params; + @field_ids = sort { $a <=> $b } @field_ids; + return @field_ids; +} + +sub _handle_chart { + my ($self, $chart_id, $clause, $condition) = @_; + my $dbh = Bugzilla->dbh; + my $params = $self->_params; + my ($field, $operator, $value) = $condition->fov; + return if (!defined $field or !defined $operator or !defined $value); + $field = FIELD_MAP->{$field} || $field; + + my ($string_value, $orig_value); + state $is_mysql = $dbh->isa('Bugzilla::DB::Mysql') ? 1 : 0; + + if (ref $value eq 'ARRAY') { + + # Trim input and ignore blank values. + @$value = map { trim($_) } @$value; + @$value = grep { defined $_ and $_ ne '' } @$value; + return if !@$value; + $orig_value = join(',', @$value); + if ($field eq 'longdesc' && $is_mysql) { + @$value = map { _convert_unicode_characters($_) } @$value; + } + $string_value = join(',', @$value); + } + else { + return if $value eq ''; + $orig_value = $value; + if ($field eq 'longdesc' && $is_mysql) { + $value = _convert_unicode_characters($value); + } + $string_value = $value; + } + + $self->_chart_fields->{$field} + or ThrowCodeError("invalid_field_name", {field => $field}); + trick_taint($field); + + # This is the field as you'd reference it in a SQL statement. + my $full_field = $field =~ /\./ ? $field : "bugs.$field"; + + # "value" and "quoted" are for search functions that always operate + # on a scalar string and never care if they were passed multiple + # parameters. If the user does pass multiple parameters, they will + # become a space-separated string for those search functions. + # + # all_values is for search functions that do operate + # on multiple values, like anyexact. + + my %search_args = ( + chart_id => $chart_id, + sequence => $chart_id, + field => $field, + full_field => $full_field, + operator => $operator, + value => $string_value, + all_values => $value, + joins => [], + bugs_table => 'bugs', + table_suffix => '', + condition => $condition, + ); + $clause->update_search_args(\%search_args); + + $search_args{quoted} = $self->_quote_unless_numeric(\%search_args); + + # This should add a "term" selement to %search_args. + $self->do_search_function(\%search_args); + + # If term is left empty, then this means the criteria + # has no effect and can be ignored. + return unless $search_args{term}; + + # All the things here that don't get pulled out of + # %search_args are their original values before + # do_search_function modified them. + $self->search_description({ + field => $field, + type => $operator, + value => $orig_value, + term => $search_args{term}, + }); + + foreach my $join (@{$search_args{joins}}) { + $join->{bugs_table} = $search_args{bugs_table}; + $join->{table_suffix} = $search_args{table_suffix}; + } + + $condition->translated(\%search_args); } # XXX - This is a hack for MySQL which doesn't understand Unicode characters # above U+FFFF, see Bugzilla::Comment::_check_thetext(). This hack can go away # once we require MySQL 5.5.3 and use utf8mb4. sub _convert_unicode_characters { - my $string = shift; + my $string = shift; - # Perl 5.13.8 and older complain about non-characters. - no warnings 'utf8'; - $string =~ s/([\x{10000}-\x{10FFFF}])/"\x{FDD0}[" . uc(sprintf('U+%04x', ord($1))) . "]\x{FDD1}"/eg; - return $string; + # Perl 5.13.8 and older complain about non-characters. + no warnings 'utf8'; + $string + =~ s/([\x{10000}-\x{10FFFF}])/"\x{FDD0}[" . uc(sprintf('U+%04x', ord($1))) . "]\x{FDD1}"/eg; + return $string; } ################################## @@ -1922,121 +1890,126 @@ sub _convert_unicode_characters { # This takes information about the current boolean chart and translates # it into SQL, using the constants at the top of this file. sub do_search_function { - my ($self, $args) = @_; - my ($field, $operator) = @$args{qw(field operator)}; - - if (my $parse_func = SPECIAL_PARSING->{$field}) { - $self->$parse_func($args); - # Some parsing functions set $term, though most do not. - # For the ones that set $term, we don't need to do any further - # parsing. - return if $args->{term}; - } - - my $operator_field_override = $self->_get_operator_field_override(); - my $override = $operator_field_override->{$field}; - # Attachment fields get special handling, if they don't have a specific - # individual override. - if (!$override and $field =~ /^attachments\./) { - $override = $operator_field_override->{attachments}; - } - # If there's still no override, check for an override on the field's type. - if (!$override) { - my $field_obj = $self->_chart_fields->{$field}; - $override = $operator_field_override->{$field_obj->type}; - } - - if ($override) { - my $search_func = $self->_pick_override_function($override, $operator); - $self->$search_func($args) if $search_func; - } + my ($self, $args) = @_; + my ($field, $operator) = @$args{qw(field operator)}; - # Some search functions set $term, and some don't. For the ones that - # don't (or for fields that don't have overrides) we now call the - # direct operator function from OPERATORS. - if (!defined $args->{term}) { - $self->_do_operator_function($args); - } - - if (!defined $args->{term}) { - # This field and this type don't work together. Generally, - # this should never be reached, because it should be handled - # explicitly by OPERATOR_FIELD_OVERRIDE. - ThrowUserError("search_field_operator_invalid", - { field => $field, operator => $operator }); - } + if (my $parse_func = SPECIAL_PARSING->{$field}) { + $self->$parse_func($args); + + # Some parsing functions set $term, though most do not. + # For the ones that set $term, we don't need to do any further + # parsing. + return if $args->{term}; + } + + my $operator_field_override = $self->_get_operator_field_override(); + my $override = $operator_field_override->{$field}; + + # Attachment fields get special handling, if they don't have a specific + # individual override. + if (!$override and $field =~ /^attachments\./) { + $override = $operator_field_override->{attachments}; + } + + # If there's still no override, check for an override on the field's type. + if (!$override) { + my $field_obj = $self->_chart_fields->{$field}; + $override = $operator_field_override->{$field_obj->type}; + } + + if ($override) { + my $search_func = $self->_pick_override_function($override, $operator); + $self->$search_func($args) if $search_func; + } + + # Some search functions set $term, and some don't. For the ones that + # don't (or for fields that don't have overrides) we now call the + # direct operator function from OPERATORS. + if (!defined $args->{term}) { + $self->_do_operator_function($args); + } + + if (!defined $args->{term}) { + + # This field and this type don't work together. Generally, + # this should never be reached, because it should be handled + # explicitly by OPERATOR_FIELD_OVERRIDE. + ThrowUserError("search_field_operator_invalid", + {field => $field, operator => $operator}); + } } # A helper for various search functions that need to run operator # functions directly. sub _do_operator_function { - my ($self, $func_args) = @_; - my $operator = $func_args->{operator}; - my $operator_func = OPERATORS->{$operator} - || ThrowCodeError("search_field_operator_unsupported", - { operator => $operator }); - $self->$operator_func($func_args); + my ($self, $func_args) = @_; + my $operator = $func_args->{operator}; + my $operator_func + = OPERATORS->{$operator} + || ThrowCodeError("search_field_operator_unsupported", + {operator => $operator}); + $self->$operator_func($func_args); } sub _reverse_operator { - my ($self, $operator) = @_; - my $reverse = OPERATOR_REVERSE->{$operator}; - return $reverse if $reverse; - if ($operator =~ s/^not//) { - return $operator; - } - return "not$operator"; + my ($self, $operator) = @_; + my $reverse = OPERATOR_REVERSE->{$operator}; + return $reverse if $reverse; + if ($operator =~ s/^not//) { + return $operator; + } + return "not$operator"; } sub _pick_override_function { - my ($self, $override, $operator) = @_; - my $search_func = $override->{$operator}; - - if (!$search_func) { - # If we don't find an override for one specific operator, - # then there are some special override types: - # _non_changed: For any operator that doesn't have the word - # "changed" in it - # _default: Overrides all operators that aren't explicitly specified. - if ($override->{_non_changed} and $operator !~ /changed/) { - $search_func = $override->{_non_changed}; - } - elsif ($override->{_default}) { - $search_func = $override->{_default}; - } + my ($self, $override, $operator) = @_; + my $search_func = $override->{$operator}; + + if (!$search_func) { + + # If we don't find an override for one specific operator, + # then there are some special override types: + # _non_changed: For any operator that doesn't have the word + # "changed" in it + # _default: Overrides all operators that aren't explicitly specified. + if ($override->{_non_changed} and $operator !~ /changed/) { + $search_func = $override->{_non_changed}; + } + elsif ($override->{_default}) { + $search_func = $override->{_default}; } + } - return $search_func; + return $search_func; } sub _get_operator_field_override { - my $self = shift; - my $cache = Bugzilla->request_cache; + my $self = shift; + my $cache = Bugzilla->request_cache; - return $cache->{operator_field_override} - if defined $cache->{operator_field_override}; + return $cache->{operator_field_override} + if defined $cache->{operator_field_override}; - my %operator_field_override = %{ OPERATOR_FIELD_OVERRIDE() }; - Bugzilla::Hook::process('search_operator_field_override', - { search => $self, - operators => \%operator_field_override }); + my %operator_field_override = %{OPERATOR_FIELD_OVERRIDE()}; + Bugzilla::Hook::process('search_operator_field_override', + {search => $self, operators => \%operator_field_override}); - $cache->{operator_field_override} = \%operator_field_override; - return $cache->{operator_field_override}; + $cache->{operator_field_override} = \%operator_field_override; + return $cache->{operator_field_override}; } sub _get_column_joins { - my $self = shift; - my $cache = Bugzilla->request_cache; + my $self = shift; + my $cache = Bugzilla->request_cache; - return $cache->{column_joins} if defined $cache->{column_joins}; + return $cache->{column_joins} if defined $cache->{column_joins}; - my %column_joins = %{ $self->COLUMN_JOINS() }; - Bugzilla::Hook::process('buglist_column_joins', - { column_joins => \%column_joins }); + my %column_joins = %{$self->COLUMN_JOINS()}; + Bugzilla::Hook::process('buglist_column_joins', + {column_joins => \%column_joins}); - $cache->{column_joins} = \%column_joins; - return $cache->{column_joins}; + $cache->{column_joins} = \%column_joins; + return $cache->{column_joins}; } ########################### @@ -2048,47 +2021,49 @@ sub _get_column_joins { # is just a performance optimization, but on SQLite it actually changes # the behavior of some searches. sub _quote_unless_numeric { - my ($self, $args, $value) = @_; - if (!defined $value) { - $value = $args->{value}; - } - my ($field, $operator) = @$args{qw(field operator)}; - - my $numeric_operator = !grep { $_ eq $operator } NON_NUMERIC_OPERATORS; - 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); + my ($self, $args, $value) = @_; + if (!defined $value) { + $value = $args->{value}; + } + my ($field, $operator) = @$args{qw(field operator)}; + + my $numeric_operator = !grep { $_ eq $operator } NON_NUMERIC_OPERATORS; + 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"; - my $dbh = Bugzilla->dbh; - my $list = $dbh->selectcol_arrayref($q); - return $negate ? "1=1" : "1=2" unless @$list; - return $dbh->sql_in($outer, $list, $negate); + 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"; + my $dbh = Bugzilla->dbh; + my $list = $dbh->selectcol_arrayref($q); + return $negate ? "1=1" : "1=2" unless @$list; + return $dbh->sql_in($outer, $list, $negate); } # Used by anyexact to get the list of input values. This allows us to @@ -2096,68 +2071,69 @@ sub build_subselect { # still accept string values for the boolean charts (and split them on # commas). sub _all_values { - my ($self, $args, $split_on) = @_; - $split_on ||= qr/[\s,]+/; - my $dbh = Bugzilla->dbh; - my $all_values = $args->{all_values}; - - my @array; - if (ref $all_values eq 'ARRAY') { - @array = @$all_values; - } - else { - @array = split($split_on, $all_values); - @array = map { trim($_) } @array; - @array = grep { defined $_ and $_ ne '' } @array; - } - - if ($args->{field} eq 'resolution') { - @array = map { $_ eq '---' ? '' : $_ } @array; - } - - return @array; + my ($self, $args, $split_on) = @_; + $split_on ||= qr/[\s,]+/; + my $dbh = Bugzilla->dbh; + my $all_values = $args->{all_values}; + + my @array; + if (ref $all_values eq 'ARRAY') { + @array = @$all_values; + } + else { + @array = split($split_on, $all_values); + @array = map { trim($_) } @array; + @array = grep { defined $_ and $_ ne '' } @array; + } + + if ($args->{field} eq 'resolution') { + @array = map { $_ eq '---' ? '' : $_ } @array; + } + + return @array; } # Support for "any/all/nowordssubstr" comparison type ("words as substrings") sub _substring_terms { - my ($self, $args) = @_; - my $dbh = Bugzilla->dbh; + my ($self, $args) = @_; + my $dbh = Bugzilla->dbh; - # We don't have to (or want to) use _all_values, because we'd just - # split each term on spaces and commas anyway. - my @words = split(/[\s,]+/, $args->{value}); - @words = grep { defined $_ and $_ ne '' } @words; - my @terms = map { $dbh->sql_ilike($_, $args->{full_field}) } @words; - return @terms; + # We don't have to (or want to) use _all_values, because we'd just + # split each term on spaces and commas anyway. + my @words = split(/[\s,]+/, $args->{value}); + @words = grep { defined $_ and $_ ne '' } @words; + my @terms = map { $dbh->sql_ilike($_, $args->{full_field}) } @words; + return @terms; } sub _word_terms { - my ($self, $args) = @_; - my $dbh = Bugzilla->dbh; - - my @values = split(/[\s,]+/, $args->{value}); - @values = grep { defined $_ and $_ ne '' } @values; - my @substring_terms = $self->_substring_terms($args); - - my @terms; - my $start = $dbh->WORD_START; - my $end = $dbh->WORD_END; - foreach my $word (@values) { - my $regex = $start . quotemeta($word) . $end; - my $quoted = $dbh->quote($regex); - # We don't have to check the regexp, because we escaped it, so we're - # sure it's valid. - my $regex_term = $dbh->sql_regexp($args->{full_field}, $quoted, - 'no check'); - # Regular expressions are slow--substring searches are faster. - # If we're searching for a word, we're also certain that the - # substring will appear in the value. So we limit first by - # substring and then by a regex that will match just words. - my $substring_term = shift @substring_terms; - push(@terms, "$substring_term AND $regex_term"); - } - - return @terms; + my ($self, $args) = @_; + my $dbh = Bugzilla->dbh; + + my @values = split(/[\s,]+/, $args->{value}); + @values = grep { defined $_ and $_ ne '' } @values; + my @substring_terms = $self->_substring_terms($args); + + my @terms; + my $start = $dbh->WORD_START; + my $end = $dbh->WORD_END; + foreach my $word (@values) { + my $regex = $start . quotemeta($word) . $end; + my $quoted = $dbh->quote($regex); + + # We don't have to check the regexp, because we escaped it, so we're + # sure it's valid. + my $regex_term = $dbh->sql_regexp($args->{full_field}, $quoted, 'no check'); + + # Regular expressions are slow--substring searches are faster. + # If we're searching for a word, we're also certain that the + # substring will appear in the value. So we limit first by + # substring and then by a regex that will match just words. + my $substring_term = shift @substring_terms; + push(@terms, "$substring_term AND $regex_term"); + } + + return @terms; } ##################################### @@ -2165,107 +2141,116 @@ sub _word_terms { ##################################### sub _timestamp_translate { - my ($self, $ignore_time, $args) = @_; - my $value = $args->{value}; - my $dbh = Bugzilla->dbh; + my ($self, $ignore_time, $args) = @_; + my $value = $args->{value}; + my $dbh = Bugzilla->dbh; - return if $value !~ /^(?:[\+\-]?\d+[hdwmy]s?|now)$/i; + return if $value !~ /^(?:[\+\-]?\d+[hdwmy]s?|now)$/i; - $value = SqlifyDate($value); - # By default, the time is appended to the date, which we don't always want. - if ($ignore_time) { - ($value) = split(/\s/, $value); - } - $args->{value} = $value; - $args->{quoted} = $dbh->quote($value); + $value = SqlifyDate($value); + + # By default, the time is appended to the date, which we don't always want. + if ($ignore_time) { + ($value) = split(/\s/, $value); + } + $args->{value} = $value; + $args->{quoted} = $dbh->quote($value); } sub _datetime_translate { - return shift->_timestamp_translate(0, @_); + return shift->_timestamp_translate(0, @_); } sub _last_visit_datetime { - my ($self, $args) = @_; - my $value = $args->{value}; - - $self->_datetime_translate($args); - if ($value eq $args->{value}) { - # Failed to translate a datetime. let's try the pronoun expando. - if ($value eq '%last_changed%') { - $self->_add_extra_column('changeddate'); - $args->{value} = $args->{quoted} = 'bugs.delta_ts'; - } + my ($self, $args) = @_; + my $value = $args->{value}; + + $self->_datetime_translate($args); + if ($value eq $args->{value}) { + + # Failed to translate a datetime. let's try the pronoun expando. + if ($value eq '%last_changed%') { + $self->_add_extra_column('changeddate'); + $args->{value} = $args->{quoted} = 'bugs.delta_ts'; } + } } sub _date_translate { - return shift->_timestamp_translate(1, @_); + return shift->_timestamp_translate(1, @_); } sub SqlifyDate { - my ($str) = @_; - my $fmt = "%Y-%m-%d %H:%M:%S"; - $str = "" if (!defined $str || lc($str) eq 'now'); - if ($str eq "") { - my ($sec, $min, $hour, $mday, $month, $year, $wday) = localtime(time()); - return sprintf("%4d-%02d-%02d 00:00:00", $year+1900, $month+1, $mday); - } - - if ($str =~ /^(-|\+)?(\d+)([hdwmy])(s?)$/i) { # relative date - my ($sign, $amount, $unit, $startof, $date) = ($1, $2, lc $3, lc $4, time); - my ($sec, $min, $hour, $mday, $month, $year, $wday) = localtime($date); - if ($sign && $sign eq '+') { $amount = -$amount; } - $startof = 1 if $amount == 0; - if ($unit eq 'w') { # convert weeks to days - $amount = 7*$amount; - $amount += $wday if $startof; - $unit = 'd'; - } - if ($unit eq 'd') { - if ($startof) { - $fmt = "%Y-%m-%d 00:00:00"; - $date -= $sec + 60*$min + 3600*$hour; - } - $date -= 24*3600*$amount; - return time2str($fmt, $date); - } - elsif ($unit eq 'y') { - if ($startof) { - return sprintf("%4d-01-01 00:00:00", $year+1900-$amount); - } - else { - return sprintf("%4d-%02d-%02d %02d:%02d:%02d", - $year+1900-$amount, $month+1, $mday, $hour, $min, $sec); - } - } - elsif ($unit eq 'm') { - $month -= $amount; - $year += floor($month/12); - $month %= 12; - if ($startof) { - return sprintf("%4d-%02d-01 00:00:00", $year+1900, $month+1); - } - else { - return sprintf("%4d-%02d-%02d %02d:%02d:%02d", - $year+1900, $month+1, $mday, $hour, $min, $sec); - } - } - elsif ($unit eq 'h') { - # Special case for 'beginning of an hour' - if ($startof) { - $fmt = "%Y-%m-%d %H:00:00"; - } - $date -= 3600*$amount; - return time2str($fmt, $date); - } - return undef; # should not happen due to regexp at top - } - my $date = str2time($str); - if (!defined($date)) { - ThrowUserError("illegal_date", { date => $str }); - } - return time2str($fmt, $date); + my ($str) = @_; + my $fmt = "%Y-%m-%d %H:%M:%S"; + $str = "" if (!defined $str || lc($str) eq 'now'); + if ($str eq "") { + my ($sec, $min, $hour, $mday, $month, $year, $wday) = localtime(time()); + return sprintf("%4d-%02d-%02d 00:00:00", $year + 1900, $month + 1, $mday); + } + + if ($str =~ /^(-|\+)?(\d+)([hdwmy])(s?)$/i) { # relative date + my ($sign, $amount, $unit, $startof, $date) = ($1, $2, lc $3, lc $4, time); + my ($sec, $min, $hour, $mday, $month, $year, $wday) = localtime($date); + if ($sign && $sign eq '+') { $amount = -$amount; } + $startof = 1 if $amount == 0; + if ($unit eq 'w') { # convert weeks to days + $amount = 7 * $amount; + $amount += $wday if $startof; + $unit = 'd'; + } + if ($unit eq 'd') { + if ($startof) { + $fmt = "%Y-%m-%d 00:00:00"; + $date -= $sec + 60 * $min + 3600 * $hour; + } + $date -= 24 * 3600 * $amount; + return time2str($fmt, $date); + } + elsif ($unit eq 'y') { + if ($startof) { + return sprintf("%4d-01-01 00:00:00", $year + 1900 - $amount); + } + else { + return sprintf( + "%4d-%02d-%02d %02d:%02d:%02d", + $year + 1900 - $amount, + $month + 1, $mday, $hour, $min, $sec + ); + } + } + elsif ($unit eq 'm') { + $month -= $amount; + $year += floor($month / 12); + $month %= 12; + if ($startof) { + return sprintf("%4d-%02d-01 00:00:00", $year + 1900, $month + 1); + } + else { + return sprintf( + "%4d-%02d-%02d %02d:%02d:%02d", + $year + 1900, + $month + 1, $mday, $hour, $min, $sec + ); + } + } + elsif ($unit eq 'h') { + + # Special case for 'beginning of an hour' + if ($startof) { + $fmt = "%Y-%m-%d %H:00:00"; + } + $date -= 3600 * $amount; + return time2str($fmt, $date); + } + return undef; # should not happen due to regexp at top + } + my $date = str2time($str); + if (!defined($date)) { + ThrowUserError("illegal_date", {date => $str}); + } + return time2str($fmt, $date); } ###################################### @@ -2273,104 +2258,109 @@ sub SqlifyDate { ###################################### sub pronoun { - my ($noun, $user) = (@_); - if ($noun eq "%user%") { - if ($user->id) { - return $user->id; - } else { - ThrowUserError('login_required_for_pronoun'); - } - } - if ($noun eq "%reporter%") { - return "bugs.reporter"; - } - if ($noun eq "%assignee%") { - return "bugs.assigned_to"; + my ($noun, $user) = (@_); + if ($noun eq "%user%") { + if ($user->id) { + return $user->id; } - if ($noun eq "%qacontact%") { - return "COALESCE(bugs.qa_contact,0)"; + else { + ThrowUserError('login_required_for_pronoun'); } + } + if ($noun eq "%reporter%") { + return "bugs.reporter"; + } + if ($noun eq "%assignee%") { + return "bugs.assigned_to"; + } + if ($noun eq "%qacontact%") { + return "COALESCE(bugs.qa_contact,0)"; + } - ThrowUserError('illegal_pronoun', { pronoun => $noun }); + ThrowUserError('illegal_pronoun', {pronoun => $noun}); } sub _contact_pronoun { - my ($self, $args) = @_; - my $value = $args->{value}; - my $user = $self->_user; + my ($self, $args) = @_; + my $value = $args->{value}; + my $user = $self->_user; - if ($value =~ /^\%group\.[^%]+%$/) { - $self->_contact_exact_group($args); - } - elsif ($value =~ /^(%\w+%)$/) { - $args->{value} = pronoun($1, $user); - $args->{quoted} = $args->{value}; - $args->{value_is_id} = 1; - } + if ($value =~ /^\%group\.[^%]+%$/) { + $self->_contact_exact_group($args); + } + elsif ($value =~ /^(%\w+%)$/) { + $args->{value} = pronoun($1, $user); + $args->{quoted} = $args->{value}; + $args->{value_is_id} = 1; + } } sub _contact_exact_group { - my ($self, $args) = @_; - my ($value, $operator, $field, $chart_id, $joins, $sequence) = - @$args{qw(value operator field chart_id joins sequence)}; - my $dbh = Bugzilla->dbh; - my $user = $self->_user; - - # We already know $value will match this regexp, else we wouldn't be here. - $value =~ /\%group\.([^%]+)%/; - my $group_name = $1; - my $group = Bugzilla::Group->check({ name => $group_name, _error => 'invalid_group_name' }); - # Pass $group_name instead of $group->name to the error message - # to not leak the existence of the group. - $user->in_group($group) - || ThrowUserError('invalid_group_name', { name => $group_name }); - # Now that we know the user belongs to this group, it's safe - # to disclose more information. - $group->check_members_are_visible(); - - my $group_ids = Bugzilla::Group->flatten_group_membership($group->id); - - if ($field eq 'cc' && $chart_id eq '') { - # This is for the email1, email2, email3 fields from query.cgi. - $chart_id = "CC$$sequence"; - $args->{sequence}++; - } - - my $from = $field; - # These fields need an additional table. - if ($field =~ /^(commenter|cc)$/) { - my $join_table = $field; - $join_table = 'longdescs' if $field eq 'commenter'; - my $join_table_alias = "${field}_$chart_id"; - push(@$joins, { table => $join_table, as => $join_table_alias }); - $from = "$join_table_alias.who"; - } - - my $table = "user_group_map_$chart_id"; - my $join = { - table => 'user_group_map', - as => $table, - from => $from, - to => 'user_id', - extra => [$dbh->sql_in("$table.group_id", $group_ids), - "$table.isbless = 0"], - }; - push(@$joins, $join); - if ($operator =~ /^not/) { - $args->{term} = "$table.group_id IS NULL"; - } - else { - $args->{term} = "$table.group_id IS NOT NULL"; - } + my ($self, $args) = @_; + my ($value, $operator, $field, $chart_id, $joins, $sequence) + = @$args{qw(value operator field chart_id joins sequence)}; + my $dbh = Bugzilla->dbh; + my $user = $self->_user; + + # We already know $value will match this regexp, else we wouldn't be here. + $value =~ /\%group\.([^%]+)%/; + my $group_name = $1; + my $group = Bugzilla::Group->check( + {name => $group_name, _error => 'invalid_group_name'}); + + # Pass $group_name instead of $group->name to the error message + # to not leak the existence of the group. + $user->in_group($group) + || ThrowUserError('invalid_group_name', {name => $group_name}); + + # Now that we know the user belongs to this group, it's safe + # to disclose more information. + $group->check_members_are_visible(); + + my $group_ids = Bugzilla::Group->flatten_group_membership($group->id); + + if ($field eq 'cc' && $chart_id eq '') { + + # This is for the email1, email2, email3 fields from query.cgi. + $chart_id = "CC$$sequence"; + $args->{sequence}++; + } + + my $from = $field; + + # These fields need an additional table. + if ($field =~ /^(commenter|cc)$/) { + my $join_table = $field; + $join_table = 'longdescs' if $field eq 'commenter'; + my $join_table_alias = "${field}_$chart_id"; + push(@$joins, {table => $join_table, as => $join_table_alias}); + $from = "$join_table_alias.who"; + } + + my $table = "user_group_map_$chart_id"; + my $join = { + table => 'user_group_map', + as => $table, + from => $from, + to => 'user_id', + extra => [$dbh->sql_in("$table.group_id", $group_ids), "$table.isbless = 0"], + }; + push(@$joins, $join); + if ($operator =~ /^not/) { + $args->{term} = "$table.group_id IS NULL"; + } + else { + $args->{term} = "$table.group_id IS NOT NULL"; + } } sub _get_user_id { - my ($self, $value) = @_; + my ($self, $value) = @_; - if ($value =~ /^%\w+%$/) { - return pronoun($value, $self->_user); - } - return login_to_id($value, THROW_ERROR); + if ($value =~ /^%\w+%$/) { + return pronoun($value, $self->_user); + } + return login_to_id($value, THROW_ERROR); } ##################################################################### @@ -2378,546 +2368,556 @@ sub _get_user_id { ##################################################################### sub _invalid_combination { - my ($self, $args) = @_; - my ($field, $operator) = @$args{qw(field operator)}; - ThrowUserError('search_field_operator_invalid', - { field => $field, operator => $operator }); + my ($self, $args) = @_; + my ($field, $operator) = @$args{qw(field operator)}; + ThrowUserError('search_field_operator_invalid', + {field => $field, operator => $operator}); } # For all the "user" fields--assigned_to, reporter, qa_contact, # cc, commenter, requestee, etc. sub _user_nonchanged { - my ($self, $args) = @_; - my ($field, $operator, $chart_id, $sequence, $joins) = - @$args{qw(field operator chart_id sequence joins)}; - - my $is_in_other_table; - if (my $join = USER_FIELDS->{$field}->{join}) { - $is_in_other_table = 1; - my $as = "${field}_$chart_id"; - # Needed for setters.login_name and requestees.login_name. - # Otherwise when we try to join "profiles" below, we'd get - # something like "setters.login_name.login_name" in the "from". - $as =~ s/\./_/g; - # This helps implement the email1, email2, etc. parameters. - if ($chart_id =~ /default/) { - $as .= "_$sequence"; - } - my $isprivate = USER_FIELDS->{$field}->{isprivate}; - my $extra = ($isprivate and !$self->_user->is_insider) - ? ["$as.isprivate = 0"] : []; - # We want to copy $join so as not to modify USER_FIELDS. - push(@$joins, { %$join, as => $as, extra => $extra }); - my $search_field = USER_FIELDS->{$field}->{field}; - $args->{full_field} = "$as.$search_field"; - } + my ($self, $args) = @_; + my ($field, $operator, $chart_id, $sequence, $joins) + = @$args{qw(field operator chart_id sequence joins)}; + + my $is_in_other_table; + if (my $join = USER_FIELDS->{$field}->{join}) { + $is_in_other_table = 1; + my $as = "${field}_$chart_id"; + + # Needed for setters.login_name and requestees.login_name. + # Otherwise when we try to join "profiles" below, we'd get + # something like "setters.login_name.login_name" in the "from". + $as =~ s/\./_/g; + + # This helps implement the email1, email2, etc. parameters. + if ($chart_id =~ /default/) { + $as .= "_$sequence"; + } + my $isprivate = USER_FIELDS->{$field}->{isprivate}; + my $extra + = ($isprivate and !$self->_user->is_insider) ? ["$as.isprivate = 0"] : []; + + # We want to copy $join so as not to modify USER_FIELDS. + push(@$joins, {%$join, as => $as, extra => $extra}); + my $search_field = USER_FIELDS->{$field}->{field}; + $args->{full_field} = "$as.$search_field"; + } + + my $is_nullable = USER_FIELDS->{$field}->{nullable}; + my $null_alternate = "''"; + + # When using a pronoun, we use the userid, and we don't have to + # join the profiles table. + 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"; - my $is_nullable = USER_FIELDS->{$field}->{nullable}; - my $null_alternate = "''"; - # When using a pronoun, we use the userid, and we don't have to - # join the profiles table. - if ($args->{value_is_id}) { - $null_alternate = 0; + # For fields with periods in their name. + $as =~ s/\./_/; + my $join = { + table => 'profiles', + as => $as, + from => $args->{full_field}, + to => 'userid', + join => (!$is_in_other_table and !$is_nullable) ? 'INNER' : undef, + }; + 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, + # it does. + if ($is_nullable) { + $args->{full_field} = "COALESCE($args->{full_field}, $null_alternate)"; + } + + # For fields whose values are stored in other tables, negation (NOT) + # only works properly if we put the condition into the JOIN instead + # of the WHERE. + if ($is_in_other_table) { + + # Using the last join works properly whether we're searching based + # on userid or login_name. + my $last_join = $joins->[-1]; + + # For negative operators, the system we're using here + # only works properly if we reverse the operator and check IS NULL + # in the WHERE. + my $is_negative = $operator =~ /^(?:no|isempty)/ ? 1 : 0; + if ($is_negative) { + $args->{operator} = $self->_reverse_operator($operator); } - 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"; + $self->_do_operator_function($args); + push(@{$last_join->{extra}}, $args->{term}); + + # For login_name searches, we only want a single join. + # So we create a subselect table out of our two joins. This makes + # negation (NOT) work properly for values that are in other + # tables. + if ($last_join->{table} eq 'profiles') { + pop @$joins; + $last_join->{join} = 'INNER'; + my ($join_sql) = $self->_translate_join($last_join); + my $first_join = $joins->[-1]; + my $as = $first_join->{as}; + my $table = $first_join->{table}; + my $columns = "bug_id"; + $columns .= ",isprivate" if @{$first_join->{extra}}; + my $new_table = "SELECT DISTINCT $columns FROM $table AS $as $join_sql"; + $first_join->{table} = "($new_table)"; + + # We always want to LEFT JOIN the generated table. + delete $first_join->{join}; + + # To support OR charts, we need multiple tables. + my $new_as = $first_join->{as} . "_$sequence"; + $_ =~ s/\Q$as\E/$new_as/ foreach @{$first_join->{extra}}; + $first_join->{as} = $new_as; + $last_join = $first_join; + } + + # If we're joining the first table (we're using a pronoun and + # searching by user id) then we need to check $other_table->{field}. + my $check_field = $last_join->{as} . '.bug_id'; + if ($is_negative) { + $args->{term} = "$check_field IS NULL"; } else { - my $as = "name_${field}_$chart_id"; - # For fields with periods in their name. - $as =~ s/\./_/; - my $join = { - table => 'profiles', - as => $as, - from => $args->{full_field}, - to => 'userid', - join => (!$is_in_other_table and !$is_nullable) ? 'INNER' : undef, - }; - 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, - # it does. - if ($is_nullable) { - $args->{full_field} = "COALESCE($args->{full_field}, $null_alternate)"; - } - - # For fields whose values are stored in other tables, negation (NOT) - # only works properly if we put the condition into the JOIN instead - # of the WHERE. - if ($is_in_other_table) { - # Using the last join works properly whether we're searching based - # on userid or login_name. - my $last_join = $joins->[-1]; - - # For negative operators, the system we're using here - # only works properly if we reverse the operator and check IS NULL - # in the WHERE. - my $is_negative = $operator =~ /^(?:no|isempty)/ ? 1 : 0; - if ($is_negative) { - $args->{operator} = $self->_reverse_operator($operator); - } - $self->_do_operator_function($args); - push(@{ $last_join->{extra} }, $args->{term}); - - # For login_name searches, we only want a single join. - # So we create a subselect table out of our two joins. This makes - # negation (NOT) work properly for values that are in other - # tables. - if ($last_join->{table} eq 'profiles') { - pop @$joins; - $last_join->{join} = 'INNER'; - my ($join_sql) = $self->_translate_join($last_join); - my $first_join = $joins->[-1]; - my $as = $first_join->{as}; - my $table = $first_join->{table}; - my $columns = "bug_id"; - $columns .= ",isprivate" if @{ $first_join->{extra} }; - my $new_table = "SELECT DISTINCT $columns FROM $table AS $as $join_sql"; - $first_join->{table} = "($new_table)"; - # We always want to LEFT JOIN the generated table. - delete $first_join->{join}; - # To support OR charts, we need multiple tables. - my $new_as = $first_join->{as} . "_$sequence"; - $_ =~ s/\Q$as\E/$new_as/ foreach @{ $first_join->{extra} }; - $first_join->{as} = $new_as; - $last_join = $first_join; - } - - # If we're joining the first table (we're using a pronoun and - # searching by user id) then we need to check $other_table->{field}. - my $check_field = $last_join->{as} . '.bug_id'; - if ($is_negative) { - $args->{term} = "$check_field IS NULL"; - } - else { - $args->{term} = "$check_field IS NOT NULL"; - } + $args->{term} = "$check_field IS NOT NULL"; } + } } # XXX This duplicates having Commenter as a search field. 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"; - } + 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 { - my ($self, $args) = @_; - 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)); - my $join = { - table => 'longdescs', - as => $table, - extra => ["$table.bug_when $sql_operator $sql_date"], - }; - push(@$joins, $join); - $args->{term} = "$table.bug_when IS NOT NULL"; - - # 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"; - } + my ($self, $args) = @_; + 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)); + my $join = { + table => 'longdescs', + as => $table, + extra => ["$table.bug_when $sql_operator $sql_date"], + }; + push(@$joins, $join); + $args->{term} = "$table.bug_when IS NOT NULL"; + + # 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_nonchanged { - my ($self, $args) = @_; - my ($chart_id, $operator, $value, $joins, $bugs_table) = - @$args{qw(chart_id operator value joins bugs_table)}; - - if ($operator =~ /^is(not)?empty$/) { - $args->{term} = $self->_multiselect_isempty($args, $operator eq 'isnotempty'); - return; - } - my $dbh = Bugzilla->dbh; - - my $table = "longdescs_$chart_id"; - my $join_args = { - chart_id => $chart_id, - sequence => $chart_id, - field => 'longdesc', - full_field => "$table.thetext", - operator => $operator, - value => $value, - all_values => $value, - quoted => $dbh->quote($value), - joins => [], - bugs_table => $bugs_table, - }; - $self->_do_operator_function($join_args); - - # If the user is not part of the insiders group, they cannot see - # private comments - if (!$self->_user->is_insider) { - $join_args->{term} .= " AND $table.isprivate = 0"; - } - - my $join = { - table => 'longdescs', - as => $table, - extra => [ $join_args->{term} ], - }; - push(@$joins, $join); - - $args->{term} = "$table.comment_id IS NOT NULL"; + my ($self, $args) = @_; + my ($chart_id, $operator, $value, $joins, $bugs_table) + = @$args{qw(chart_id operator value joins bugs_table)}; + + if ($operator =~ /^is(not)?empty$/) { + $args->{term} = $self->_multiselect_isempty($args, $operator eq 'isnotempty'); + return; + } + my $dbh = Bugzilla->dbh; + + my $table = "longdescs_$chart_id"; + my $join_args = { + chart_id => $chart_id, + sequence => $chart_id, + field => 'longdesc', + full_field => "$table.thetext", + operator => $operator, + value => $value, + all_values => $value, + quoted => $dbh->quote($value), + joins => [], + bugs_table => $bugs_table, + }; + $self->_do_operator_function($join_args); + + # If the user is not part of the insiders group, they cannot see + # private comments + if (!$self->_user->is_insider) { + $join_args->{term} .= " AND $table.isprivate = 0"; + } + + my $join = {table => 'longdescs', as => $table, extra => [$join_args->{term}],}; + push(@$joins, $join); + + $args->{term} = "$table.comment_id IS NOT NULL"; } sub _content_matches { - my ($self, $args) = @_; - my ($chart_id, $joins, $fields, $operator, $value) = - @$args{qw(chart_id joins fields operator value)}; - my $dbh = Bugzilla->dbh; - - # "content" is an alias for columns containing text for which we - # can search a full-text index and retrieve results by relevance, - # currently just bug comments (and summaries to some degree). - # There's only one way to search a full-text index, so we only - # accept the "matches" operator, which is specific to full-text - # index searches. - - # Add the fulltext table to the query so we can search on it. - my $table = "bugs_fulltext_$chart_id"; - my $comments_col = "comments"; - $comments_col = "comments_noprivate" unless $self->_user->is_insider; - push(@$joins, { table => 'bugs_fulltext', as => $table }); - - # Create search terms to add to the SELECT and WHERE clauses. - my ($term1, $rterm1) = - $dbh->sql_fulltext_search("$table.$comments_col", $value); - my ($term2, $rterm2) = - $dbh->sql_fulltext_search("$table.short_desc", $value); - $rterm1 = $term1 if !$rterm1; - $rterm2 = $term2 if !$rterm2; - - # The term to use in the WHERE clause. - my $term = "$term1 OR $term2"; - if ($operator =~ /not/i) { - $term = "NOT($term)"; - } - $args->{term} = $term; - - # In order to sort by relevance (in case the user requests it), - # we SELECT the relevance value so we can add it to the ORDER BY - # clause. Every time a new fulltext chart isadded, this adds more - # terms to the relevance sql. - # - # We build the relevance SQL by modifying the COLUMNS list directly, - # which is kind of a hack but works. - my $current = $self->COLUMNS->{'relevance'}->{name}; - $current = $current ? "$current + " : ''; - # For NOT searches, we just add 0 to the relevance. - my $select_term = $operator =~ /not/ ? 0 : "($current$rterm1 + $rterm2)"; - $self->COLUMNS->{'relevance'}->{name} = $select_term; + my ($self, $args) = @_; + my ($chart_id, $joins, $fields, $operator, $value) + = @$args{qw(chart_id joins fields operator value)}; + my $dbh = Bugzilla->dbh; + + # "content" is an alias for columns containing text for which we + # can search a full-text index and retrieve results by relevance, + # currently just bug comments (and summaries to some degree). + # There's only one way to search a full-text index, so we only + # accept the "matches" operator, which is specific to full-text + # index searches. + + # Add the fulltext table to the query so we can search on it. + my $table = "bugs_fulltext_$chart_id"; + my $comments_col = "comments"; + $comments_col = "comments_noprivate" unless $self->_user->is_insider; + push(@$joins, {table => 'bugs_fulltext', as => $table}); + + # Create search terms to add to the SELECT and WHERE clauses. + my ($term1, $rterm1) + = $dbh->sql_fulltext_search("$table.$comments_col", $value); + my ($term2, $rterm2) = $dbh->sql_fulltext_search("$table.short_desc", $value); + $rterm1 = $term1 if !$rterm1; + $rterm2 = $term2 if !$rterm2; + + # The term to use in the WHERE clause. + my $term = "$term1 OR $term2"; + if ($operator =~ /not/i) { + $term = "NOT($term)"; + } + $args->{term} = $term; + + # In order to sort by relevance (in case the user requests it), + # we SELECT the relevance value so we can add it to the ORDER BY + # clause. Every time a new fulltext chart isadded, this adds more + # terms to the relevance sql. + # + # We build the relevance SQL by modifying the COLUMNS list directly, + # which is kind of a hack but works. + my $current = $self->COLUMNS->{'relevance'}->{name}; + $current = $current ? "$current + " : ''; + + # For NOT searches, we just add 0 to the relevance. + my $select_term = $operator =~ /not/ ? 0 : "($current$rterm1 + $rterm2)"; + $self->COLUMNS->{'relevance'}->{name} = $select_term; } sub _long_descs_count { - my ($self, $args) = @_; - my ($chart_id, $joins) = @$args{qw(chart_id joins)}; - my $table = "longdescs_count_$chart_id"; - my $extra = $self->_user->is_insider ? "" : "WHERE isprivate = 0"; - my $join = { - table => "(SELECT bug_id, COUNT(*) AS num" - . " FROM longdescs $extra GROUP BY bug_id)", - as => $table, - }; - push(@$joins, $join); - $args->{full_field} = "${table}.num"; + my ($self, $args) = @_; + my ($chart_id, $joins) = @$args{qw(chart_id joins)}; + my $table = "longdescs_count_$chart_id"; + my $extra = $self->_user->is_insider ? "" : "WHERE isprivate = 0"; + my $join = { + table => "(SELECT bug_id, COUNT(*) AS num" + . " FROM longdescs $extra GROUP BY bug_id)", + as => $table, + }; + push(@$joins, $join); + $args->{full_field} = "${table}.num"; } sub _work_time_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 AND $table.work_time != 0"; + 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 AND $table.work_time != 0"; } sub _work_time_changedbefore_after { - my ($self, $args) = @_; - my ($chart_id, $operator, $value, $joins) = - @$args{qw(chart_id operator value joins)}; - my $dbh = Bugzilla->dbh; - - my $table = "longdescs_$chart_id"; - my $sql_operator = ($operator =~ /before/) ? '<=' : '>='; - my $sql_date = $dbh->quote(SqlifyDate($value)); - my $join = { - table => 'longdescs', - as => $table, - extra => ["$table.work_time != 0", - "$table.bug_when $sql_operator $sql_date"], - }; - push(@$joins, $join); - - $args->{term} = "$table.bug_when IS NOT NULL"; + my ($self, $args) = @_; + my ($chart_id, $operator, $value, $joins) + = @$args{qw(chart_id operator value joins)}; + my $dbh = Bugzilla->dbh; + + my $table = "longdescs_$chart_id"; + my $sql_operator = ($operator =~ /before/) ? '<=' : '>='; + my $sql_date = $dbh->quote(SqlifyDate($value)); + my $join = { + table => 'longdescs', + as => $table, + extra => ["$table.work_time != 0", "$table.bug_when $sql_operator $sql_date"], + }; + push(@$joins, $join); + + $args->{term} = "$table.bug_when IS NOT NULL"; } sub _work_time { - my ($self, $args) = @_; - $self->_add_extra_column('actual_time'); - $args->{full_field} = $self->COLUMNS->{actual_time}->{name}; + my ($self, $args) = @_; + $self->_add_extra_column('actual_time'); + $args->{full_field} = $self->COLUMNS->{actual_time}->{name}; } sub _percentage_complete { - my ($self, $args) = @_; - - $args->{full_field} = $self->COLUMNS->{percentage_complete}->{name}; + my ($self, $args) = @_; + + $args->{full_field} = $self->COLUMNS->{percentage_complete}->{name}; - # We need actual_time in _select_columns, otherwise we can't use - # it in the expression for searching percentage_complete. - $self->_add_extra_column('actual_time'); + # We need actual_time in _select_columns, otherwise we can't use + # it in the expression for searching percentage_complete. + $self->_add_extra_column('actual_time'); } sub _last_visit_ts { - my ($self, $args) = @_; + my ($self, $args) = @_; - $args->{full_field} = $self->COLUMNS->{last_visit_ts}->{name}; - $self->_add_extra_column('last_visit_ts'); + $args->{full_field} = $self->COLUMNS->{last_visit_ts}->{name}; + $self->_add_extra_column('last_visit_ts'); } sub _last_visit_ts_invalid_operator { - my ($self, $args) = @_; + my ($self, $args) = @_; - ThrowUserError('search_field_operator_invalid', - { field => $args->{field}, - operator => $args->{operator} }); + ThrowUserError('search_field_operator_invalid', + {field => $args->{field}, operator => $args->{operator}}); } sub _days_elapsed { - my ($self, $args) = @_; - my $dbh = Bugzilla->dbh; - - $args->{full_field} = "(" . $dbh->sql_to_days('NOW()') . " - " . - $dbh->sql_to_days('bugs.delta_ts') . ")"; + my ($self, $args) = @_; + my $dbh = Bugzilla->dbh; + + $args->{full_field} + = "(" + . $dbh->sql_to_days('NOW()') . " - " + . $dbh->sql_to_days('bugs.delta_ts') . ")"; } sub _component_nonchanged { - my ($self, $args) = @_; - - $args->{full_field} = "components.name"; - $self->_do_operator_function($args); - my $term = $args->{term}; - $args->{term} = build_subselect("bugs.component_id", - "components.id", "components", $args->{term}); + my ($self, $args) = @_; + + $args->{full_field} = "components.name"; + $self->_do_operator_function($args); + my $term = $args->{term}; + $args->{term} + = build_subselect("bugs.component_id", "components.id", "components", + $args->{term}); } sub _product_nonchanged { - my ($self, $args) = @_; - - # Generate the restriction condition - $args->{full_field} = "products.name"; - $self->_do_operator_function($args); - my $term = $args->{term}; - $args->{term} = build_subselect("bugs.product_id", - "products.id", "products", $term); + my ($self, $args) = @_; + + # Generate the restriction condition + $args->{full_field} = "products.name"; + $self->_do_operator_function($args); + my $term = $args->{term}; + $args->{term} + = build_subselect("bugs.product_id", "products.id", "products", $term); } sub _alias_nonchanged { - my ($self, $args) = @_; + my ($self, $args) = @_; - $args->{full_field} = "bugs_aliases.alias"; - $self->_do_operator_function($args); - $args->{term} = build_subselect("bugs.bug_id", - "bugs_aliases.bug_id", "bugs_aliases", $args->{term}); + $args->{full_field} = "bugs_aliases.alias"; + $self->_do_operator_function($args); + $args->{term} + = build_subselect("bugs.bug_id", "bugs_aliases.bug_id", "bugs_aliases", + $args->{term}); } sub _classification_nonchanged { - my ($self, $args) = @_; - my $joins = $args->{joins}; - - # This joins the right tables for us. - $self->_add_extra_column('product'); - - # Generate the restriction condition - $args->{full_field} = "classifications.name"; - $self->_do_operator_function($args); - my $term = $args->{term}; - $args->{term} = build_subselect("map_product.classification_id", - "classifications.id", "classifications", $term); + my ($self, $args) = @_; + my $joins = $args->{joins}; + + # This joins the right tables for us. + $self->_add_extra_column('product'); + + # Generate the restriction condition + $args->{full_field} = "classifications.name"; + $self->_do_operator_function($args); + my $term = $args->{term}; + $args->{term} = build_subselect("map_product.classification_id", + "classifications.id", "classifications", $term); } sub _nullable { - my ($self, $args) = @_; - my $field = $args->{full_field}; - $args->{full_field} = "COALESCE($field, '')"; + my ($self, $args) = @_; + my $field = $args->{full_field}; + $args->{full_field} = "COALESCE($field, '')"; } sub _nullable_int { - my ($self, $args) = @_; - my $field = $args->{full_field}; - $args->{full_field} = "COALESCE($field, 0)"; + my ($self, $args) = @_; + my $field = $args->{full_field}; + $args->{full_field} = "COALESCE($field, 0)"; } sub _nullable_datetime { - my ($self, $args) = @_; - my $field = $args->{full_field}; - my $empty = Bugzilla->dbh->quote(EMPTY_DATETIME); - $args->{full_field} = "COALESCE($field, $empty)"; + my ($self, $args) = @_; + my $field = $args->{full_field}; + my $empty = Bugzilla->dbh->quote(EMPTY_DATETIME); + $args->{full_field} = "COALESCE($field, $empty)"; } sub _nullable_date { - my ($self, $args) = @_; - my $field = $args->{full_field}; - my $empty = Bugzilla->dbh->quote(EMPTY_DATE); - $args->{full_field} = "COALESCE($field, $empty)"; + my ($self, $args) = @_; + my $field = $args->{full_field}; + my $empty = Bugzilla->dbh->quote(EMPTY_DATE); + $args->{full_field} = "COALESCE($field, $empty)"; } sub _deadline { - my ($self, $args) = @_; - my $field = $args->{full_field}; - # This makes "equals" searches work on all DBs (even on MySQL, which - # has a bug: http://bugs.mysql.com/bug.php?id=60324). - $args->{full_field} = Bugzilla->dbh->sql_date_format($field, '%Y-%m-%d'); - $self->_nullable_datetime($args); + my ($self, $args) = @_; + my $field = $args->{full_field}; + + # This makes "equals" searches work on all DBs (even on MySQL, which + # has a bug: http://bugs.mysql.com/bug.php?id=60324). + $args->{full_field} = Bugzilla->dbh->sql_date_format($field, '%Y-%m-%d'); + $self->_nullable_datetime($args); } sub _owner_idle_time_greater_less { - my ($self, $args) = @_; - my ($chart_id, $joins, $value, $operator) = - @$args{qw(chart_id joins value operator)}; - my $dbh = Bugzilla->dbh; - - my $table = "idle_$chart_id"; - my $quoted = $dbh->quote(SqlifyDate($value)); - - my $ld_table = "comment_$table"; - my $act_table = "activity_$table"; - my $comments_join = { - table => 'longdescs', - as => $ld_table, - from => 'assigned_to', - to => 'who', - extra => ["$ld_table.bug_when > $quoted"], - }; - my $activity_join = { - table => 'bugs_activity', - as => $act_table, - from => 'assigned_to', - to => 'who', - extra => ["$act_table.bug_when > $quoted"] - }; - - push(@$joins, $comments_join, $activity_join); - - if ($operator =~ /greater/) { - $args->{term} = - "$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)"; - } + my ($self, $args) = @_; + my ($chart_id, $joins, $value, $operator) + = @$args{qw(chart_id joins value operator)}; + my $dbh = Bugzilla->dbh; + + my $table = "idle_$chart_id"; + my $quoted = $dbh->quote(SqlifyDate($value)); + + my $ld_table = "comment_$table"; + my $act_table = "activity_$table"; + my $comments_join = { + table => 'longdescs', + as => $ld_table, + from => 'assigned_to', + to => 'who', + extra => ["$ld_table.bug_when > $quoted"], + }; + my $activity_join = { + table => 'bugs_activity', + as => $act_table, + from => 'assigned_to', + to => 'who', + extra => ["$act_table.bug_when > $quoted"] + }; + + push(@$joins, $comments_join, $activity_join); + + if ($operator =~ /greater/) { + $args->{term} = "$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)"; + } } sub _multiselect_negative { - my ($self, $args) = @_; - my ($field, $operator) = @$args{qw(field operator)}; + my ($self, $args) = @_; + my ($field, $operator) = @$args{qw(field operator)}; - $args->{operator} = $self->_reverse_operator($operator); - $args->{term} = $self->_multiselect_term($args, 1); + $args->{operator} = $self->_reverse_operator($operator); + $args->{term} = $self->_multiselect_term($args, 1); } sub _multiselect_multiple { - my ($self, $args) = @_; - my ($chart_id, $field, $operator, $value) - = @$args{qw(chart_id field operator value)}; - my $dbh = Bugzilla->dbh; - - # We want things like "cf_multi_select=two+words" to still be - # considered a search for two separate words, unless we're using - # anyexact. (_all_values would consider that to be one "word" with a - # space in it, because it's not in the Boolean Charts). - my @words = $operator eq 'anyexact' ? $self->_all_values($args) - : split(/[\s,]+/, $value); - - my @terms; - foreach my $word (@words) { - next if $word eq ''; - $args->{value} = $word; - $args->{quoted} = $dbh->quote($word); - push(@terms, $self->_multiselect_term($args)); - } - - # The spacing in the joins helps make the resulting SQL more readable. - if ($operator =~ /^any/) { - $args->{term} = join("\n OR ", @terms); - } - else { - $args->{term} = join("\n AND ", @terms); - } + my ($self, $args) = @_; + my ($chart_id, $field, $operator, $value) + = @$args{qw(chart_id field operator value)}; + my $dbh = Bugzilla->dbh; + + # We want things like "cf_multi_select=two+words" to still be + # considered a search for two separate words, unless we're using + # anyexact. (_all_values would consider that to be one "word" with a + # space in it, because it's not in the Boolean Charts). + my @words + = $operator eq 'anyexact' + ? $self->_all_values($args) + : split(/[\s,]+/, $value); + + my @terms; + foreach my $word (@words) { + next if $word eq ''; + $args->{value} = $word; + $args->{quoted} = $dbh->quote($word); + push(@terms, $self->_multiselect_term($args)); + } + + # The spacing in the joins helps make the resulting SQL more readable. + if ($operator =~ /^any/) { + $args->{term} = join("\n OR ", @terms); + } + else { + $args->{term} = join("\n AND ", @terms); + } } sub _flagtypes_nonchanged { - my ($self, $args) = @_; - my ($chart_id, $operator, $value, $joins, $bugs_table, $condition) = - @$args{qw(chart_id operator value joins bugs_table condition)}; - - if ($operator =~ /^is(not)?empty$/) { - $args->{term} = $self->_multiselect_isempty($args, $operator eq 'isnotempty'); - return; - } - - my $dbh = Bugzilla->dbh; - - # For 'not' operators, we need to negate the whole term. - # If you search for "Flags" (does not contain) "approval+" we actually want - # to return *bugs* that don't contain an approval+ flag. Without rewriting - # the negation we'll search for *flags* which don't contain approval+. - if ($operator =~ s/^not//) { - $args->{operator} = $operator; - $condition->operator($operator); - $condition->negate(1); - } - - my $subselect_args = { - chart_id => $chart_id, - sequence => $chart_id, - field => 'flagtypes.name', - full_field => $dbh->sql_string_concat("flagtypes_$chart_id.name", "flags_$chart_id.status"), - operator => $operator, - value => $value, - all_values => $value, - quoted => $dbh->quote($value), - joins => [], - bugs_table => "bugs_$chart_id", - }; - $self->_do_operator_function($subselect_args); - my $subselect_term = $subselect_args->{term}; - - # don't call build_subselect as this must run as a true sub-select - $args->{term} = "EXISTS ( + my ($self, $args) = @_; + my ($chart_id, $operator, $value, $joins, $bugs_table, $condition) + = @$args{qw(chart_id operator value joins bugs_table condition)}; + + if ($operator =~ /^is(not)?empty$/) { + $args->{term} = $self->_multiselect_isempty($args, $operator eq 'isnotempty'); + return; + } + + my $dbh = Bugzilla->dbh; + + # For 'not' operators, we need to negate the whole term. + # If you search for "Flags" (does not contain) "approval+" we actually want + # to return *bugs* that don't contain an approval+ flag. Without rewriting + # the negation we'll search for *flags* which don't contain approval+. + if ($operator =~ s/^not//) { + $args->{operator} = $operator; + $condition->operator($operator); + $condition->negate(1); + } + + my $subselect_args = { + chart_id => $chart_id, + sequence => $chart_id, + field => 'flagtypes.name', + full_field => + $dbh->sql_string_concat("flagtypes_$chart_id.name", "flags_$chart_id.status"), + operator => $operator, + value => $value, + all_values => $value, + quoted => $dbh->quote($value), + joins => [], + bugs_table => "bugs_$chart_id", + }; + $self->_do_operator_function($subselect_args); + my $subselect_term = $subselect_args->{term}; + + # don't call build_subselect as this must run as a true sub-select + $args->{term} = "EXISTS ( SELECT 1 FROM $bugs_table bugs_$chart_id LEFT JOIN attachments AS attachments_$chart_id @@ -2934,209 +2934,224 @@ sub _flagtypes_nonchanged { } sub _multiselect_nonchanged { - my ($self, $args) = @_; - my ($chart_id, $joins, $field, $operator) = - @$args{qw(chart_id joins field operator)}; - $args->{term} = $self->_multiselect_term($args) + my ($self, $args) = @_; + my ($chart_id, $joins, $field, $operator) + = @$args{qw(chart_id joins field operator)}; + $args->{term} = $self->_multiselect_term($args); } sub _multiselect_table { - my ($self, $args) = @_; - my ($field, $chart_id) = @$args{qw(field chart_id)}; - my $dbh = Bugzilla->dbh; - - if ($field eq 'keywords') { - $args->{full_field} = 'keyworddefs.name'; - return "keywords INNER JOIN keyworddefs". - " ON keywords.keywordid = keyworddefs.id"; - } - elsif ($field eq 'tag') { - $args->{full_field} = 'tag.name'; - return "bug_tag INNER JOIN tag ON bug_tag.tag_id = tag.id AND user_id = " - . ($self->_sharer_id || $self->_user->id); - } - elsif ($field eq 'bug_group') { - $args->{full_field} = 'groups.name'; - return "bug_group_map INNER JOIN groups + my ($self, $args) = @_; + my ($field, $chart_id) = @$args{qw(field chart_id)}; + my $dbh = Bugzilla->dbh; + + if ($field eq 'keywords') { + $args->{full_field} = 'keyworddefs.name'; + return "keywords INNER JOIN keyworddefs" + . " ON keywords.keywordid = keyworddefs.id"; + } + elsif ($field eq 'tag') { + $args->{full_field} = 'tag.name'; + return "bug_tag INNER JOIN tag ON bug_tag.tag_id = tag.id AND user_id = " + . ($self->_sharer_id || $self->_user->id); + } + elsif ($field eq 'bug_group') { + $args->{full_field} = 'groups.name'; + return "bug_group_map INNER JOIN groups ON bug_group_map.group_id = groups.id"; - } - elsif ($field eq 'blocked' or $field eq 'dependson') { - my $select = $field eq 'blocked' ? 'dependson' : 'blocked'; - $args->{_select_field} = $select; - $args->{full_field} = $field; - return "dependencies"; - } - elsif ($field eq 'longdesc') { - $args->{_extra_where} = " AND isprivate = 0" - if !$self->_user->is_insider; - $args->{full_field} = 'thetext'; - return "longdescs"; - } - elsif ($field eq 'longdescs.isprivate') { - ThrowUserError('auth_failure', { action => 'search', - object => 'bug_fields', - field => 'longdescs.isprivate' }) - if !$self->_user->is_insider; - $args->{full_field} = 'isprivate'; - return "longdescs"; - } - elsif ($field =~ /^attachments/) { - $args->{_extra_where} = " AND isprivate = 0" - if !$self->_user->is_insider; - $field =~ /^attachments\.(.+)$/; - $args->{full_field} = $1; - return "attachments"; - } - elsif ($field eq 'attach_data.thedata') { - $args->{_extra_where} = " AND attachments.isprivate = 0" - if !$self->_user->is_insider; - return "attachments INNER JOIN attach_data " - . " ON attachments.attach_id = attach_data.id" - } - elsif ($field eq 'comment_tag') { - $args->{_extra_where} = " AND longdescs.isprivate = 0" - if !$self->_user->is_insider; - $args->{full_field} = 'longdescs_tags.tag'; - return "longdescs INNER JOIN longdescs_tags". - " ON longdescs.comment_id = longdescs_tags.comment_id"; - } - my $table = "bug_$field"; - $args->{full_field} = "bug_$field.value"; - return $table; + } + elsif ($field eq 'blocked' or $field eq 'dependson') { + my $select = $field eq 'blocked' ? 'dependson' : 'blocked'; + $args->{_select_field} = $select; + $args->{full_field} = $field; + return "dependencies"; + } + elsif ($field eq 'longdesc') { + $args->{_extra_where} = " AND isprivate = 0" if !$self->_user->is_insider; + $args->{full_field} = 'thetext'; + return "longdescs"; + } + elsif ($field eq 'longdescs.isprivate') { + ThrowUserError('auth_failure', + {action => 'search', object => 'bug_fields', field => 'longdescs.isprivate'}) + if !$self->_user->is_insider; + $args->{full_field} = 'isprivate'; + return "longdescs"; + } + elsif ($field =~ /^attachments/) { + $args->{_extra_where} = " AND isprivate = 0" if !$self->_user->is_insider; + $field =~ /^attachments\.(.+)$/; + $args->{full_field} = $1; + return "attachments"; + } + elsif ($field eq 'attach_data.thedata') { + $args->{_extra_where} = " AND attachments.isprivate = 0" + if !$self->_user->is_insider; + return "attachments INNER JOIN attach_data " + . " ON attachments.attach_id = attach_data.id"; + } + elsif ($field eq 'comment_tag') { + $args->{_extra_where} = " AND longdescs.isprivate = 0" + if !$self->_user->is_insider; + $args->{full_field} = 'longdescs_tags.tag'; + return "longdescs INNER JOIN longdescs_tags" + . " ON longdescs.comment_id = longdescs_tags.comment_id"; + } + my $table = "bug_$field"; + $args->{full_field} = "bug_$field.value"; + return $table; } sub _multiselect_term { - my ($self, $args, $not) = @_; - my ($operator) = $args->{operator}; - my $value = $args->{value} || ''; - # 'empty' operators require special handling - return $self->_multiselect_isempty($args, $not) - if ($operator =~ /^is(not)?empty$/ || $value eq '---'); - my $table = $self->_multiselect_table($args); - $self->_do_operator_function($args); - my $term = $args->{term}; - $term .= $args->{_extra_where} || ''; - my $select = $args->{_select_field} || 'bug_id'; - return build_subselect("$args->{bugs_table}.bug_id", $select, $table, $term, $not); + my ($self, $args, $not) = @_; + my ($operator) = $args->{operator}; + my $value = $args->{value} || ''; + + # 'empty' operators require special handling + return $self->_multiselect_isempty($args, $not) + if ($operator =~ /^is(not)?empty$/ || $value eq '---'); + my $table = $self->_multiselect_table($args); + $self->_do_operator_function($args); + my $term = $args->{term}; + $term .= $args->{_extra_where} || ''; + my $select = $args->{_select_field} || 'bug_id'; + return build_subselect("$args->{bugs_table}.bug_id", $select, $table, $term, + $not); } # We can't use the normal operator_functions to build isempty queries which # join to different tables. sub _multiselect_isempty { - my ($self, $args, $not) = @_; - my ($field, $operator, $joins, $chart_id) = @$args{qw(field operator joins chart_id)}; - my $dbh = Bugzilla->dbh; - $operator = $self->_reverse_operator($operator) if $not; - $not = $operator eq 'isnotempty' ? 'NOT' : ''; - - if ($field eq 'keywords') { - push @$joins, { - table => 'keywords', - as => "keywords_$chart_id", - from => 'bug_id', - to => 'bug_id', - }; - return "keywords_$chart_id.bug_id IS $not NULL"; - } - elsif ($field eq 'bug_group') { - push @$joins, { - table => 'bug_group_map', - as => "bug_group_map_$chart_id", - from => 'bug_id', - to => 'bug_id', - }; - return "bug_group_map_$chart_id.bug_id IS $not NULL"; - } - elsif ($field eq 'flagtypes.name') { - push @$joins, { - table => 'flags', - as => "flags_$chart_id", - from => 'bug_id', - to => 'bug_id', - }; - return "flags_$chart_id.bug_id IS $not NULL"; - } - elsif ($field eq 'blocked' or $field eq 'dependson') { - my $to = $field eq 'blocked' ? 'dependson' : 'blocked'; - push @$joins, { - table => 'dependencies', - as => "dependencies_$chart_id", - from => 'bug_id', - to => $to, - }; - return "dependencies_$chart_id.$to IS $not NULL"; - } - elsif ($field eq 'longdesc') { - my @extra = ( "longdescs_$chart_id.type != " . CMT_HAS_DUPE ); - push @extra, "longdescs_$chart_id.isprivate = 0" - unless $self->_user->is_insider; - push @$joins, { - table => 'longdescs', - as => "longdescs_$chart_id", - from => 'bug_id', - to => 'bug_id', - extra => \@extra, - }; - return $not - ? "longdescs_$chart_id.thetext != ''" - : "longdescs_$chart_id.thetext = ''"; - } - elsif ($field eq 'longdescs.isprivate') { - ThrowUserError('search_field_operator_invalid', { field => $field, - operator => $operator }); - } - elsif ($field =~ /^attachments\.(.+)/) { - my $sub_field = $1; - if ($sub_field eq 'description' || $sub_field eq 'filename' || $sub_field eq 'mimetype') { - # can't be null/empty - return $not ? '1=1' : '1=2'; - } else { - # all other fields which get here are boolean - ThrowUserError('search_field_operator_invalid', { field => $field, - operator => $operator }); - } - } - elsif ($field eq 'attach_data.thedata') { - push @$joins, { - table => 'attachments', - as => "attachments_$chart_id", - from => 'bug_id', - to => 'bug_id', - extra => [ $self->_user->is_insider ? '' : "attachments_$chart_id.isprivate = 0" ], - }; - push @$joins, { - table => 'attach_data', - as => "attach_data_$chart_id", - from => "attachments_$chart_id.attach_id", - to => 'id', - }; - return "attach_data_$chart_id.thedata IS $not NULL"; - } - elsif ($field eq 'tag') { - push @$joins, { - table => 'bug_tag', - as => "bug_tag_$chart_id", - from => 'bug_id', - to => 'bug_id', - }; - push @$joins, { - table => 'tag', - as => "tag_$chart_id", - from => "bug_tag_$chart_id.tag_id", - to => 'id', - extra => [ "tag_$chart_id.user_id = " . ($self->_sharer_id || $self->_user->id) ], - }; - return "tag_$chart_id.id IS $not NULL"; - } - elsif ($self->_multi_select_fields->{$field}) { - push @$joins, { - table => "bug_$field", - as => "bug_${field}_$chart_id", - from => 'bug_id', - to => 'bug_id', - }; - return "bug_${field}_$chart_id.bug_id IS $not NULL"; + my ($self, $args, $not) = @_; + my ($field, $operator, $joins, $chart_id) + = @$args{qw(field operator joins chart_id)}; + my $dbh = Bugzilla->dbh; + $operator = $self->_reverse_operator($operator) if $not; + $not = $operator eq 'isnotempty' ? 'NOT' : ''; + + if ($field eq 'keywords') { + push @$joins, + { + table => 'keywords', + as => "keywords_$chart_id", + from => 'bug_id', + to => 'bug_id', + }; + return "keywords_$chart_id.bug_id IS $not NULL"; + } + elsif ($field eq 'bug_group') { + push @$joins, + { + table => 'bug_group_map', + as => "bug_group_map_$chart_id", + from => 'bug_id', + to => 'bug_id', + }; + return "bug_group_map_$chart_id.bug_id IS $not NULL"; + } + elsif ($field eq 'flagtypes.name') { + push @$joins, + { + table => 'flags', + as => "flags_$chart_id", + from => 'bug_id', + to => 'bug_id', + }; + return "flags_$chart_id.bug_id IS $not NULL"; + } + elsif ($field eq 'blocked' or $field eq 'dependson') { + my $to = $field eq 'blocked' ? 'dependson' : 'blocked'; + push @$joins, + { + table => 'dependencies', + as => "dependencies_$chart_id", + from => 'bug_id', + to => $to, + }; + return "dependencies_$chart_id.$to IS $not NULL"; + } + elsif ($field eq 'longdesc') { + my @extra = ("longdescs_$chart_id.type != " . CMT_HAS_DUPE); + push @extra, "longdescs_$chart_id.isprivate = 0" + unless $self->_user->is_insider; + push @$joins, + { + table => 'longdescs', + as => "longdescs_$chart_id", + from => 'bug_id', + to => 'bug_id', + extra => \@extra, + }; + return $not + ? "longdescs_$chart_id.thetext != ''" + : "longdescs_$chart_id.thetext = ''"; + } + elsif ($field eq 'longdescs.isprivate') { + ThrowUserError('search_field_operator_invalid', + {field => $field, operator => $operator}); + } + elsif ($field =~ /^attachments\.(.+)/) { + my $sub_field = $1; + if ( $sub_field eq 'description' + || $sub_field eq 'filename' + || $sub_field eq 'mimetype') + { + # can't be null/empty + return $not ? '1=1' : '1=2'; } + else { + # all other fields which get here are boolean + ThrowUserError('search_field_operator_invalid', + {field => $field, operator => $operator}); + } + } + elsif ($field eq 'attach_data.thedata') { + push @$joins, + { + table => 'attachments', + as => "attachments_$chart_id", + from => 'bug_id', + to => 'bug_id', + extra => + [$self->_user->is_insider ? '' : "attachments_$chart_id.isprivate = 0"], + }; + push @$joins, + { + table => 'attach_data', + as => "attach_data_$chart_id", + from => "attachments_$chart_id.attach_id", + to => 'id', + }; + return "attach_data_$chart_id.thedata IS $not NULL"; + } + elsif ($field eq 'tag') { + push @$joins, + { + table => 'bug_tag', + as => "bug_tag_$chart_id", + from => 'bug_id', + to => 'bug_id', + }; + push @$joins, + { + table => 'tag', + as => "tag_$chart_id", + from => "bug_tag_$chart_id.tag_id", + to => 'id', + extra => ["tag_$chart_id.user_id = " . ($self->_sharer_id || $self->_user->id)], + }; + return "tag_$chart_id.id IS $not NULL"; + } + elsif ($self->_multi_select_fields->{$field}) { + push @$joins, + { + table => "bug_$field", + as => "bug_${field}_$chart_id", + from => 'bug_id', + to => 'bug_id', + }; + return "bug_${field}_$chart_id.bug_id IS $not NULL"; + } } ############################### @@ -3144,234 +3159,236 @@ sub _multiselect_isempty { ############################### sub _simple_operator { - my ($self, $args) = @_; - my ($full_field, $quoted, $operator) = - @$args{qw(full_field quoted operator)}; - my $sql_operator = SIMPLE_OPERATORS->{$operator}; - $args->{term} = "$full_field $sql_operator $quoted"; + my ($self, $args) = @_; + my ($full_field, $quoted, $operator) = @$args{qw(full_field quoted operator)}; + my $sql_operator = SIMPLE_OPERATORS->{$operator}; + $args->{term} = "$full_field $sql_operator $quoted"; } sub _casesubstring { - my ($self, $args) = @_; - my ($full_field, $value) = @$args{qw(full_field value)}; - my $dbh = Bugzilla->dbh; + my ($self, $args) = @_; + my ($full_field, $value) = @$args{qw(full_field value)}; + my $dbh = Bugzilla->dbh; - $args->{term} = $dbh->sql_like($value, $full_field); + $args->{term} = $dbh->sql_like($value, $full_field); } sub _substring { - my ($self, $args) = @_; - my ($full_field, $value) = @$args{qw(full_field value)}; - my $dbh = Bugzilla->dbh; + my ($self, $args) = @_; + my ($full_field, $value) = @$args{qw(full_field value)}; + my $dbh = Bugzilla->dbh; - $args->{term} = $dbh->sql_ilike($value, $full_field); + $args->{term} = $dbh->sql_ilike($value, $full_field); } sub _notsubstring { - my ($self, $args) = @_; - my ($full_field, $value) = @$args{qw(full_field value)}; - my $dbh = Bugzilla->dbh; + my ($self, $args) = @_; + my ($full_field, $value) = @$args{qw(full_field value)}; + my $dbh = Bugzilla->dbh; - $args->{term} = $dbh->sql_not_ilike($value, $full_field); + $args->{term} = $dbh->sql_not_ilike($value, $full_field); } sub _regexp { - my ($self, $args) = @_; - my ($full_field, $quoted) = @$args{qw(full_field quoted)}; - my $dbh = Bugzilla->dbh; - - $args->{term} = $dbh->sql_regexp($full_field, $quoted); + my ($self, $args) = @_; + my ($full_field, $quoted) = @$args{qw(full_field quoted)}; + my $dbh = Bugzilla->dbh; + + $args->{term} = $dbh->sql_regexp($full_field, $quoted); } sub _notregexp { - my ($self, $args) = @_; - my ($full_field, $quoted) = @$args{qw(full_field quoted)}; - my $dbh = Bugzilla->dbh; - - $args->{term} = $dbh->sql_not_regexp($full_field, $quoted); + my ($self, $args) = @_; + my ($full_field, $quoted) = @$args{qw(full_field quoted)}; + my $dbh = Bugzilla->dbh; + + $args->{term} = $dbh->sql_not_regexp($full_field, $quoted); } sub _anyexact { - my ($self, $args) = @_; - my ($field, $full_field) = @$args{qw(field full_field)}; - my $dbh = Bugzilla->dbh; - - my @list = $self->_all_values($args, ','); - @list = map { $self->_quote_unless_numeric($args, $_) } @list; - - if (@list) { - $args->{term} = $dbh->sql_in($full_field, \@list); - } - else { - $args->{term} = ''; - } + my ($self, $args) = @_; + my ($field, $full_field) = @$args{qw(field full_field)}; + my $dbh = Bugzilla->dbh; + + my @list = $self->_all_values($args, ','); + @list = map { $self->_quote_unless_numeric($args, $_) } @list; + + if (@list) { + $args->{term} = $dbh->sql_in($full_field, \@list); + } + else { + $args->{term} = ''; + } } sub _anywordsubstr { - my ($self, $args) = @_; + my ($self, $args) = @_; - my @terms = $self->_substring_terms($args); - $args->{term} = @terms ? '(' . join("\n\tOR ", @terms) . ')' : ''; + my @terms = $self->_substring_terms($args); + $args->{term} = @terms ? '(' . join("\n\tOR ", @terms) . ')' : ''; } sub _allwordssubstr { - my ($self, $args) = @_; + my ($self, $args) = @_; - my @terms = $self->_substring_terms($args); - $args->{term} = @terms ? '(' . join("\n\tAND ", @terms) . ')' : ''; + my @terms = $self->_substring_terms($args); + $args->{term} = @terms ? '(' . join("\n\tAND ", @terms) . ')' : ''; } sub _nowordssubstr { - my ($self, $args) = @_; - $self->_anywordsubstr($args); - my $term = $args->{term}; - $args->{term} = "NOT($term)"; + my ($self, $args) = @_; + $self->_anywordsubstr($args); + my $term = $args->{term}; + $args->{term} = "NOT($term)"; } sub _anywords { - my ($self, $args) = @_; + my ($self, $args) = @_; + + my @terms = $self->_word_terms($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} = @terms ? '(' . join("\n\tOR ", @terms) . ')' : ''; + # 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} = @terms ? '(' . join("\n\tOR ", @terms) . ')' : ''; } sub _allwords { - my ($self, $args) = @_; + my ($self, $args) = @_; - my @terms = $self->_word_terms($args); - $args->{term} = @terms ? '(' . join("\n\tAND ", @terms) . ')' : ''; + my @terms = $self->_word_terms($args); + $args->{term} = @terms ? '(' . join("\n\tAND ", @terms) . ')' : ''; } sub _nowords { - my ($self, $args) = @_; - $self->_anywords($args); - my $term = $args->{term}; - $args->{term} = "NOT($term)"; + my ($self, $args) = @_; + $self->_anywords($args); + my $term = $args->{term}; + $args->{term} = "NOT($term)"; } sub _changedbefore_changedafter { - my ($self, $args) = @_; - my ($chart_id, $joins, $field, $operator, $value) = - @$args{qw(chart_id joins field operator value)}; - my $dbh = Bugzilla->dbh; - - my $field_object = $self->_chart_fields->{$field} - || ThrowCodeError("invalid_field_name", { field => $field }); - - # Asking when creation_ts changed is just asking when the bug was created. - if ($field_object->name eq 'creation_ts') { - $args->{operator} = - $operator eq 'changedbefore' ? 'lessthaneq' : 'greaterthaneq'; - return $self->_do_operator_function($args); - } - - my $sql_operator = ($operator =~ /before/) ? '<=' : '>='; - my $field_id = $field_object->id; - # Charts on changed* fields need to be field-specific. Otherwise, - # OR chart rows make no sense if they contain multiple fields. - my $table = "act_${field_id}_$chart_id"; - - my $sql_date = $dbh->quote(SqlifyDate($value)); - my $join = { - table => 'bugs_activity', - as => $table, - extra => ["$table.fieldid = $field_id", - "$table.bug_when $sql_operator $sql_date"], - }; + my ($self, $args) = @_; + my ($chart_id, $joins, $field, $operator, $value) + = @$args{qw(chart_id joins field operator value)}; + my $dbh = Bugzilla->dbh; - $args->{term} = "$table.bug_when IS NOT NULL"; - $self->_changed_security_check($args, $join); - push(@$joins, $join); + my $field_object = $self->_chart_fields->{$field} + || ThrowCodeError("invalid_field_name", {field => $field}); + + # Asking when creation_ts changed is just asking when the bug was created. + if ($field_object->name eq 'creation_ts') { + $args->{operator} + = $operator eq 'changedbefore' ? 'lessthaneq' : 'greaterthaneq'; + return $self->_do_operator_function($args); + } + + my $sql_operator = ($operator =~ /before/) ? '<=' : '>='; + my $field_id = $field_object->id; + + # Charts on changed* fields need to be field-specific. Otherwise, + # OR chart rows make no sense if they contain multiple fields. + my $table = "act_${field_id}_$chart_id"; + + my $sql_date = $dbh->quote(SqlifyDate($value)); + my $join = { + table => 'bugs_activity', + as => $table, + extra => + ["$table.fieldid = $field_id", "$table.bug_when $sql_operator $sql_date"], + }; + + $args->{term} = "$table.bug_when IS NOT NULL"; + $self->_changed_security_check($args, $join); + push(@$joins, $join); } sub _changedfrom_changedto { - my ($self, $args) = @_; - my ($chart_id, $joins, $field, $operator, $quoted) = - @$args{qw(chart_id joins field operator quoted)}; - - my $column = ($operator =~ /from/) ? 'removed' : 'added'; - my $field_object = $self->_chart_fields->{$field} - || ThrowCodeError("invalid_field_name", { field => $field }); - my $field_id = $field_object->id; - my $table = "act_${field_id}_$chart_id"; - my $join = { - table => 'bugs_activity', - as => $table, - extra => ["$table.fieldid = $field_id", - "$table.$column = $quoted"], - }; - - $args->{term} = "$table.bug_when IS NOT NULL"; - $self->_changed_security_check($args, $join); - push(@$joins, $join); + my ($self, $args) = @_; + my ($chart_id, $joins, $field, $operator, $quoted) + = @$args{qw(chart_id joins field operator quoted)}; + + my $column = ($operator =~ /from/) ? 'removed' : 'added'; + my $field_object = $self->_chart_fields->{$field} + || ThrowCodeError("invalid_field_name", {field => $field}); + my $field_id = $field_object->id; + my $table = "act_${field_id}_$chart_id"; + my $join = { + table => 'bugs_activity', + as => $table, + extra => ["$table.fieldid = $field_id", "$table.$column = $quoted"], + }; + + $args->{term} = "$table.bug_when IS NOT NULL"; + $self->_changed_security_check($args, $join); + push(@$joins, $join); } sub _changedby { - my ($self, $args) = @_; - my ($chart_id, $joins, $field, $operator, $value) = - @$args{qw(chart_id joins field operator value)}; - - my $field_object = $self->_chart_fields->{$field} - || ThrowCodeError("invalid_field_name", { field => $field }); - my $field_id = $field_object->id; - my $table = "act_${field_id}_$chart_id"; - my $user_id = $self->_get_user_id($value); - my $join = { - table => 'bugs_activity', - as => $table, - extra => ["$table.fieldid = $field_id", - "$table.who = $user_id"], - }; - - $args->{term} = "$table.bug_when IS NOT NULL"; - $self->_changed_security_check($args, $join); - push(@$joins, $join); + my ($self, $args) = @_; + my ($chart_id, $joins, $field, $operator, $value) + = @$args{qw(chart_id joins field operator value)}; + + my $field_object = $self->_chart_fields->{$field} + || ThrowCodeError("invalid_field_name", {field => $field}); + my $field_id = $field_object->id; + my $table = "act_${field_id}_$chart_id"; + my $user_id = $self->_get_user_id($value); + my $join = { + table => 'bugs_activity', + as => $table, + extra => ["$table.fieldid = $field_id", "$table.who = $user_id"], + }; + + $args->{term} = "$table.bug_when IS NOT NULL"; + $self->_changed_security_check($args, $join); + push(@$joins, $join); } sub _changed_security_check { - my ($self, $args, $join) = @_; - my ($chart_id, $field) = @$args{qw(chart_id field)}; - - my $field_object = $self->_chart_fields->{$field} - || ThrowCodeError("invalid_field_name", { field => $field }); - my $field_id = $field_object->id; - - # If the user is not part of the insiders group, they cannot see - # changes to attachments (including attachment flags) that are private - if ($field =~ /^(?:flagtypes\.name$|attach)/ and !$self->_user->is_insider) { - $join->{then_to} = { - as => "attach_${field_id}_$chart_id", - table => 'attachments', - from => "act_${field_id}_$chart_id.attach_id", - to => 'attach_id', - }; - - $args->{term} .= " AND COALESCE(attach_${field_id}_$chart_id.isprivate, 0) = 0"; - } + my ($self, $args, $join) = @_; + my ($chart_id, $field) = @$args{qw(chart_id field)}; + + my $field_object = $self->_chart_fields->{$field} + || ThrowCodeError("invalid_field_name", {field => $field}); + my $field_id = $field_object->id; + + # If the user is not part of the insiders group, they cannot see + # changes to attachments (including attachment flags) that are private + if ($field =~ /^(?:flagtypes\.name$|attach)/ and !$self->_user->is_insider) { + $join->{then_to} = { + as => "attach_${field_id}_$chart_id", + table => 'attachments', + from => "act_${field_id}_$chart_id.attach_id", + to => 'attach_id', + }; + + $args->{term} .= " AND COALESCE(attach_${field_id}_$chart_id.isprivate, 0) = 0"; + } } sub _isempty { - my ($self, $args) = @_; - my $full_field = $args->{full_field}; - $args->{term} = "$full_field IS NULL OR $full_field = " . $self->_empty_value($args->{field}); + my ($self, $args) = @_; + my $full_field = $args->{full_field}; + $args->{term} = "$full_field IS NULL OR $full_field = " + . $self->_empty_value($args->{field}); } sub _isnotempty { - my ($self, $args) = @_; - my $full_field = $args->{full_field}; - $args->{term} = "$full_field IS NOT NULL AND $full_field != " . $self->_empty_value($args->{field}); + my ($self, $args) = @_; + my $full_field = $args->{full_field}; + $args->{term} = "$full_field IS NOT NULL AND $full_field != " + . $self->_empty_value($args->{field}); } sub _empty_value { - my ($self, $field) = @_; - my $field_obj = $self->_chart_fields->{$field}; - return "0" if $field_obj->type == FIELD_TYPE_BUG_ID; - return Bugzilla->dbh->quote(EMPTY_DATETIME) if $field_obj->type == FIELD_TYPE_DATETIME; - return Bugzilla->dbh->quote(EMPTY_DATE) if $field_obj->type == FIELD_TYPE_DATE; - return "''"; + my ($self, $field) = @_; + my $field_obj = $self->_chart_fields->{$field}; + return "0" if $field_obj->type == FIELD_TYPE_BUG_ID; + return Bugzilla->dbh->quote(EMPTY_DATETIME) + if $field_obj->type == FIELD_TYPE_DATETIME; + return Bugzilla->dbh->quote(EMPTY_DATE) if $field_obj->type == FIELD_TYPE_DATE; + return "''"; } ###################### @@ -3379,46 +3396,47 @@ sub _empty_value { ###################### # Validate that the query type is one we can deal with -sub IsValidQueryType -{ - my ($queryType) = @_; - if (grep { $_ eq $queryType } qw(specific advanced)) { - return 1; - } - return 0; +sub IsValidQueryType { + my ($queryType) = @_; + if (grep { $_ eq $queryType } qw(specific advanced)) { + return 1; + } + return 0; } # Splits out "asc|desc" from a sort order item. sub split_order_term { - my $fragment = shift; - $fragment =~ /^(.+?)(?:\s+(ASC|DESC))?$/i; - my ($column_name, $direction) = (lc($1), uc($2 || '')); - return wantarray ? ($column_name, $direction) : $column_name; + my $fragment = shift; + $fragment =~ /^(.+?)(?:\s+(ASC|DESC))?$/i; + my ($column_name, $direction) = (lc($1), uc($2 || '')); + return wantarray ? ($column_name, $direction) : $column_name; } # Used to translate old SQL fragments from buglist.cgi's "order" argument # into our modern field IDs. sub _translate_old_column { - my ($self, $column) = @_; - # All old SQL fragments have a period in them somewhere. - return $column if $column !~ /\./; + my ($self, $column) = @_; - if ($column =~ /\bAS\s+(\w+)$/i) { - return $1; - } - # product, component, classification, assigned_to, qa_contact, reporter - elsif ($column =~ /map_(\w+?)s?\.(login_)?name/i) { - return $1; - } - - # If it doesn't match the regexps above, check to see if the old - # SQL fragment matches the SQL of an existing column - foreach my $key (%{ $self->COLUMNS }) { - next unless exists $self->COLUMNS->{$key}->{name}; - return $key if $self->COLUMNS->{$key}->{name} eq $column; - } + # All old SQL fragments have a period in them somewhere. + return $column if $column !~ /\./; + + if ($column =~ /\bAS\s+(\w+)$/i) { + return $1; + } + + # product, component, classification, assigned_to, qa_contact, reporter + elsif ($column =~ /map_(\w+?)s?\.(login_)?name/i) { + return $1; + } + + # If it doesn't match the regexps above, check to see if the old + # SQL fragment matches the SQL of an existing column + foreach my $key (%{$self->COLUMNS}) { + next unless exists $self->COLUMNS->{$key}->{name}; + return $key if $self->COLUMNS->{$key}->{name} eq $column; + } - return $column; + return $column; } 1; diff --git a/Bugzilla/Search/Clause.pm b/Bugzilla/Search/Clause.pm index 1d7872c78..940f88ff3 100644 --- a/Bugzilla/Search/Clause.pm +++ b/Bugzilla/Search/Clause.pm @@ -16,121 +16,123 @@ use Bugzilla::Search::Condition qw(condition); use Bugzilla::Util qw(trick_taint); sub new { - my ($class, $joiner) = @_; - if ($joiner and $joiner ne 'OR' and $joiner ne 'AND') { - ThrowCodeError('search_invalid_joiner', { joiner => $joiner }); - } - # This will go into SQL directly so needs to be untainted. - trick_taint($joiner) if $joiner; - bless { joiner => $joiner || 'AND' }, $class; + my ($class, $joiner) = @_; + if ($joiner and $joiner ne 'OR' and $joiner ne 'AND') { + ThrowCodeError('search_invalid_joiner', {joiner => $joiner}); + } + + # This will go into SQL directly so needs to be untainted. + trick_taint($joiner) if $joiner; + bless {joiner => $joiner || 'AND'}, $class; } sub children { - my ($self) = @_; - $self->{children} ||= []; - return $self->{children}; + my ($self) = @_; + $self->{children} ||= []; + return $self->{children}; } sub update_search_args { - my ($self, $search_args) = @_; - # abstract + my ($self, $search_args) = @_; + + # abstract } sub joiner { return $_[0]->{joiner} } sub has_translated_conditions { - my ($self) = @_; - my $children = $self->children; - return 1 if grep { $_->isa('Bugzilla::Search::Condition') - && $_->translated } @$children; - foreach my $child (@$children) { - next if $child->isa('Bugzilla::Search::Condition'); - return 1 if $child->has_translated_conditions; - } - return 0; + my ($self) = @_; + my $children = $self->children; + return 1 + if grep { $_->isa('Bugzilla::Search::Condition') && $_->translated } + @$children; + foreach my $child (@$children) { + next if $child->isa('Bugzilla::Search::Condition'); + return 1 if $child->has_translated_conditions; + } + return 0; } sub add { - my $self = shift; - my $children = $self->children; - if (@_ == 3) { - push(@$children, condition(@_)); - return; - } - - my ($child) = @_; - return if !defined $child; - $child->isa(__PACKAGE__) || $child->isa('Bugzilla::Search::Condition') - || die 'child not the right type: ' . $child; - push(@{ $self->children }, $child); + my $self = shift; + my $children = $self->children; + if (@_ == 3) { + push(@$children, condition(@_)); + return; + } + + my ($child) = @_; + return if !defined $child; + $child->isa(__PACKAGE__) + || $child->isa('Bugzilla::Search::Condition') + || die 'child not the right type: ' . $child; + push(@{$self->children}, $child); } sub negate { - my ($self, $value) = @_; - if (@_ == 2) { - $self->{negate} = $value ? 1 : 0; - } - return $self->{negate}; + my ($self, $value) = @_; + if (@_ == 2) { + $self->{negate} = $value ? 1 : 0; + } + return $self->{negate}; } sub walk_conditions { - my ($self, $callback) = @_; - foreach my $child (@{ $self->children }) { - if ($child->isa('Bugzilla::Search::Condition')) { - $callback->($self, $child); - } - else { - $child->walk_conditions($callback); - } + my ($self, $callback) = @_; + foreach my $child (@{$self->children}) { + if ($child->isa('Bugzilla::Search::Condition')) { + $callback->($self, $child); + } + else { + $child->walk_conditions($callback); } + } } sub as_string { - my ($self) = @_; - if (!$self->{sql}) { - my @strings; - foreach my $child (@{ $self->children }) { - next if $child->isa(__PACKAGE__) && !$child->has_translated_conditions; - next if $child->isa('Bugzilla::Search::Condition') - && !$child->translated; - - my $string = $child->as_string; - next unless $string; - if ($self->joiner eq 'AND') { - $string = "( $string )" if $string =~ /OR/; - } - else { - $string = "( $string )" if $string =~ /AND/; - } - push(@strings, $string); - } - - my $sql = join(' ' . $self->joiner . ' ', @strings); - $sql = "NOT( $sql )" if $sql && $self->negate; - $self->{sql} = $sql; + my ($self) = @_; + if (!$self->{sql}) { + my @strings; + foreach my $child (@{$self->children}) { + next if $child->isa(__PACKAGE__) && !$child->has_translated_conditions; + next if $child->isa('Bugzilla::Search::Condition') && !$child->translated; + + my $string = $child->as_string; + next unless $string; + if ($self->joiner eq 'AND') { + $string = "( $string )" if $string =~ /OR/; + } + else { + $string = "( $string )" if $string =~ /AND/; + } + push(@strings, $string); } - return $self->{sql}; + + my $sql = join(' ' . $self->joiner . ' ', @strings); + $sql = "NOT( $sql )" if $sql && $self->negate; + $self->{sql} = $sql; + } + return $self->{sql}; } # Search.pm converts URL parameters to Clause objects. This helps do the # reverse. sub as_params { - my ($self) = @_; - my @params; - foreach my $child (@{ $self->children }) { - if ($child->isa(__PACKAGE__)) { - my %open_paren = (f => 'OP', n => scalar $child->negate, - j => $child->joiner); - push(@params, \%open_paren); - push(@params, $child->as_params); - my %close_paren = (f => 'CP'); - push(@params, \%close_paren); - } - else { - push(@params, $child->as_params); - } + my ($self) = @_; + my @params; + foreach my $child (@{$self->children}) { + if ($child->isa(__PACKAGE__)) { + my %open_paren = (f => 'OP', n => scalar $child->negate, j => $child->joiner); + push(@params, \%open_paren); + push(@params, $child->as_params); + my %close_paren = (f => 'CP'); + push(@params, \%close_paren); + } + else { + push(@params, $child->as_params); } - return @params; + } + return @params; } 1; diff --git a/Bugzilla/Search/ClauseGroup.pm b/Bugzilla/Search/ClauseGroup.pm index 590c737fa..5c7791734 100644 --- a/Bugzilla/Search/ClauseGroup.pm +++ b/Bugzilla/Search/ClauseGroup.pm @@ -19,83 +19,88 @@ use Bugzilla::Util qw(trick_taint); use List::MoreUtils qw(uniq); use constant UNSUPPORTED_FIELDS => qw( - attach_data.thedata - classification - commenter - component - longdescs.count - product - owner_idle_time + attach_data.thedata + classification + commenter + component + longdescs.count + product + owner_idle_time ); sub new { - my ($class) = @_; - my $self = bless({ joiner => 'AND' }, $class); - # Add a join back to the bugs table which will be used to group conditions - # for this clause - my $condition = Bugzilla::Search::Condition->new({}); - $condition->translated({ - joins => [{ - table => 'bugs', - as => 'bugs_g0', - from => 'bug_id', - to => 'bug_id', - extra => [], - }], - term => '1 = 1', - }); - $self->SUPER::add($condition); - $self->{group_condition} = $condition; - return $self; + my ($class) = @_; + my $self = bless({joiner => 'AND'}, $class); + + # Add a join back to the bugs table which will be used to group conditions + # for this clause + my $condition = Bugzilla::Search::Condition->new({}); + $condition->translated({ + joins => [{ + table => 'bugs', + as => 'bugs_g0', + from => 'bug_id', + to => 'bug_id', + extra => [], + }], + term => '1 = 1', + }); + $self->SUPER::add($condition); + $self->{group_condition} = $condition; + return $self; } sub add { - my ($self, @args) = @_; - my $field = scalar(@args) == 3 ? $args[0] : $args[0]->{field}; - - # We don't support nesting of conditions under this clause - if (scalar(@args) == 1 && !$args[0]->isa('Bugzilla::Search::Condition')) { - ThrowUserError('search_grouped_invalid_nesting'); - } - - # Ensure all conditions use the same field - if (!$self->{_field}) { - $self->{_field} = $field; - } elsif ($field ne $self->{_field}) { - ThrowUserError('search_grouped_field_mismatch'); - } - - # Unsupported fields - if (grep { $_ eq $field } UNSUPPORTED_FIELDS ) { - # 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); + my ($self, @args) = @_; + my $field = scalar(@args) == 3 ? $args[0] : $args[0]->{field}; + + # We don't support nesting of conditions under this clause + if (scalar(@args) == 1 && !$args[0]->isa('Bugzilla::Search::Condition')) { + ThrowUserError('search_grouped_invalid_nesting'); + } + + # Ensure all conditions use the same field + if (!$self->{_field}) { + $self->{_field} = $field; + } + elsif ($field ne $self->{_field}) { + ThrowUserError('search_grouped_field_mismatch'); + } + + # Unsupported fields + if (grep { $_ eq $field } UNSUPPORTED_FIELDS) { + + # 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); } sub update_search_args { - my ($self, $search_args) = @_; + my ($self, $search_args) = @_; - # No need to change things if there's only one child condition - return unless scalar(@{ $self->children }) > 1; + # No need to change things if there's only one child condition + return unless scalar(@{$self->children}) > 1; - # we want all the terms to use the same join table - if (!exists $self->{_first_chart_id}) { - $self->{_first_chart_id} = $search_args->{chart_id}; - } else { - $search_args->{chart_id} = $self->{_first_chart_id}; - } + # we want all the terms to use the same join table + if (!exists $self->{_first_chart_id}) { + $self->{_first_chart_id} = $search_args->{chart_id}; + } + else { + $search_args->{chart_id} = $self->{_first_chart_id}; + } - my $suffix = '_g' . $self->{_first_chart_id}; - $self->{group_condition}->{translated}->{joins}->[0]->{as} = "bugs$suffix"; + my $suffix = '_g' . $self->{_first_chart_id}; + $self->{group_condition}->{translated}->{joins}->[0]->{as} = "bugs$suffix"; - $search_args->{full_field} =~ s/^bugs\./bugs$suffix\./; + $search_args->{full_field} =~ s/^bugs\./bugs$suffix\./; - $search_args->{table_suffix} = $suffix; - $search_args->{bugs_table} = "bugs$suffix"; + $search_args->{table_suffix} = $suffix; + $search_args->{bugs_table} = "bugs$suffix"; } 1; diff --git a/Bugzilla/Search/Condition.pm b/Bugzilla/Search/Condition.pm index 306a63eed..9ddb6a898 100644 --- a/Bugzilla/Search/Condition.pm +++ b/Bugzilla/Search/Condition.pm @@ -15,55 +15,59 @@ use parent qw(Exporter); our @EXPORT_OK = qw(condition); sub new { - my ($class, $params) = @_; - my %self = %$params; - bless \%self, $class; - return \%self; + my ($class, $params) = @_; + my %self = %$params; + bless \%self, $class; + return \%self; } -sub field { return $_[0]->{field} } -sub value { return $_[0]->{value} } +sub field { return $_[0]->{field} } +sub value { return $_[0]->{value} } sub operator { - my ($self, $value) = @_; - if (@_ == 2) { - $self->{operator} = $value; - } - return $self->{operator}; + my ($self, $value) = @_; + if (@_ == 2) { + $self->{operator} = $value; + } + return $self->{operator}; } sub fov { - my ($self) = @_; - return ($self->field, $self->operator, $self->value); + my ($self) = @_; + return ($self->field, $self->operator, $self->value); } sub translated { - my ($self, $params) = @_; - if (@_ == 2) { - $self->{translated} = $params; - } - return $self->{translated}; + my ($self, $params) = @_; + if (@_ == 2) { + $self->{translated} = $params; + } + return $self->{translated}; } sub as_string { - my ($self) = @_; - my $term = $self->translated->{term}; - $term = "NOT( $term )" if $term && $self->negate; - return $term; + my ($self) = @_; + my $term = $self->translated->{term}; + $term = "NOT( $term )" if $term && $self->negate; + return $term; } sub as_params { - my ($self) = @_; - return { f => $self->field, o => $self->operator, v => $self->value, - n => scalar $self->negate }; + my ($self) = @_; + return { + f => $self->field, + o => $self->operator, + v => $self->value, + n => scalar $self->negate + }; } sub negate { - my ($self, $value) = @_; - if (@_ == 2) { - $self->{negate} = $value ? 1 : 0; - } - return $self->{negate}; + my ($self, $value) = @_; + if (@_ == 2) { + $self->{negate} = $value ? 1 : 0; + } + return $self->{negate}; } ########################### @@ -71,9 +75,9 @@ sub negate { ########################### sub condition { - my ($field, $operator, $value) = @_; - return __PACKAGE__->new({ field => $field, operator => $operator, - value => $value }); + my ($field, $operator, $value) = @_; + return __PACKAGE__->new( + {field => $field, operator => $operator, value => $value}); } 1; diff --git a/Bugzilla/Search/Quicksearch.pm b/Bugzilla/Search/Quicksearch.pm index 830177f8b..10472b0c3 100644 --- a/Bugzilla/Search/Quicksearch.pm +++ b/Bugzilla/Search/Quicksearch.pm @@ -27,246 +27,251 @@ use parent qw(Exporter); # Custom mappings for some fields. use constant MAPPINGS => { - # Status, Resolution, Platform, OS, Priority, Severity - "status" => "bug_status", - "platform" => "rep_platform", - "os" => "op_sys", - "severity" => "bug_severity", - - # People: AssignedTo, Reporter, QA Contact, CC, etc. - "assignee" => "assigned_to", - "owner" => "assigned_to", - - # Product, Version, Component, Target Milestone - "milestone" => "target_milestone", - - # Summary, Description, URL, Status whiteboard, Keywords - "summary" => "short_desc", - "description" => "longdesc", - "comment" => "longdesc", - "url" => "bug_file_loc", - "whiteboard" => "status_whiteboard", - "sw" => "status_whiteboard", - "kw" => "keywords", - "group" => "bug_group", - - # Flags - "flag" => "flagtypes.name", - "requestee" => "requestees.login_name", - "setter" => "setters.login_name", - - # Attachments - "attachment" => "attachments.description", - "attachmentdesc" => "attachments.description", - "attachdesc" => "attachments.description", - "attachmentdata" => "attach_data.thedata", - "attachdata" => "attach_data.thedata", - "attachmentmimetype" => "attachments.mimetype", - "attachmimetype" => "attachments.mimetype" + + # Status, Resolution, Platform, OS, Priority, Severity + "status" => "bug_status", + "platform" => "rep_platform", + "os" => "op_sys", + "severity" => "bug_severity", + + # People: AssignedTo, Reporter, QA Contact, CC, etc. + "assignee" => "assigned_to", + "owner" => "assigned_to", + + # Product, Version, Component, Target Milestone + "milestone" => "target_milestone", + + # Summary, Description, URL, Status whiteboard, Keywords + "summary" => "short_desc", + "description" => "longdesc", + "comment" => "longdesc", + "url" => "bug_file_loc", + "whiteboard" => "status_whiteboard", + "sw" => "status_whiteboard", + "kw" => "keywords", + "group" => "bug_group", + + # Flags + "flag" => "flagtypes.name", + "requestee" => "requestees.login_name", + "setter" => "setters.login_name", + + # Attachments + "attachment" => "attachments.description", + "attachmentdesc" => "attachments.description", + "attachdesc" => "attachments.description", + "attachmentdata" => "attach_data.thedata", + "attachdata" => "attach_data.thedata", + "attachmentmimetype" => "attachments.mimetype", + "attachmimetype" => "attachments.mimetype" }; sub FIELD_MAP { - my $cache = Bugzilla->request_cache; - return $cache->{quicksearch_fields} if $cache->{quicksearch_fields}; - - # Get all the fields whose names don't contain periods. (Fields that - # contain periods are always handled in MAPPINGS.) - my @db_fields = grep { $_->name !~ /\./ } - @{ Bugzilla->fields({ obsolete => 0 }) }; - my %full_map = (%{ MAPPINGS() }, map { $_->name => $_->name } @db_fields); - - # Eliminate the fields that start with bug_ or rep_, because those are - # handled by the MAPPINGS instead, and we don't want too many names - # for them. (Also, otherwise "rep" doesn't match "reporter".) - # - # Remove "status_whiteboard" because we have "whiteboard" for it in - # the mappings, and otherwise "stat" can't match "status". - # - # Also, don't allow searching the _accessible stuff via quicksearch - # (both because it's unnecessary and because otherwise - # "reporter_accessible" and "reporter" both match "rep". - delete @full_map{qw(rep_platform bug_status bug_file_loc bug_group - bug_severity bug_status - status_whiteboard - cclist_accessible reporter_accessible)}; - - Bugzilla::Hook::process('quicksearch_map', {'map' => \%full_map} ); - - $cache->{quicksearch_fields} = \%full_map; - - return $cache->{quicksearch_fields}; + my $cache = Bugzilla->request_cache; + return $cache->{quicksearch_fields} if $cache->{quicksearch_fields}; + + # Get all the fields whose names don't contain periods. (Fields that + # contain periods are always handled in MAPPINGS.) + my @db_fields = grep { $_->name !~ /\./ } @{Bugzilla->fields({obsolete => 0})}; + my %full_map = (%{MAPPINGS()}, map { $_->name => $_->name } @db_fields); + + # Eliminate the fields that start with bug_ or rep_, because those are + # handled by the MAPPINGS instead, and we don't want too many names + # for them. (Also, otherwise "rep" doesn't match "reporter".) + # + # Remove "status_whiteboard" because we have "whiteboard" for it in + # the mappings, and otherwise "stat" can't match "status". + # + # Also, don't allow searching the _accessible stuff via quicksearch + # (both because it's unnecessary and because otherwise + # "reporter_accessible" and "reporter" both match "rep". + delete @full_map{ + qw(rep_platform bug_status bug_file_loc bug_group + bug_severity bug_status + status_whiteboard + cclist_accessible reporter_accessible) + }; + + Bugzilla::Hook::process('quicksearch_map', {'map' => \%full_map}); + + $cache->{quicksearch_fields} = \%full_map; + + return $cache->{quicksearch_fields}; } # Certain fields, when specified like "field:value" get an operator other # than "substring" -use constant FIELD_OPERATOR => { - content => 'matches', - owner_idle_time => 'greaterthan', -}; +use constant FIELD_OPERATOR => + {content => 'matches', owner_idle_time => 'greaterthan',}; # Mappings for operators symbols to support operators other than "substring" use constant OPERATOR_SYMBOLS => { - ':' => 'substring', - '=' => 'equals', - '!=' => 'notequals', - '>=' => 'greaterthaneq', - '<=' => 'lessthaneq', - '>' => 'greaterthan', - '<' => 'lessthan', + ':' => 'substring', + '=' => 'equals', + '!=' => 'notequals', + '>=' => 'greaterthaneq', + '<=' => 'lessthaneq', + '>' => 'greaterthan', + '<' => 'lessthan', }; # We might want to put this into localconfig or somewhere use constant PRODUCT_EXCEPTIONS => ( - 'row', # [Browser] - # ^^^ - 'new', # [MailNews] - # ^^^ + 'row', # [Browser] + # ^^^ + 'new', # [MailNews] + # ^^^ ); use constant COMPONENT_EXCEPTIONS => ( - 'hang' # [Bugzilla: Component/Keyword Changes] - # ^^^^ + 'hang' # [Bugzilla: Component/Keyword Changes] + # ^^^^ ); # Quicksearch-wide globals for boolean charts. our ($chart, $and, $or, $fulltext, $bug_status_set); sub quicksearch { - my ($searchstring) = (@_); - my $cgi = Bugzilla->cgi; - - $chart = 0; - $and = 0; - $or = 0; + my ($searchstring) = (@_); + my $cgi = Bugzilla->cgi; + + $chart = 0; + $and = 0; + $or = 0; + + # Remove leading and trailing commas and whitespace. + $searchstring =~ s/(^[\s,]+|[\s,]+$)//g; + ThrowUserError('buglist_parameters_required') unless ($searchstring); + + if ($searchstring =~ m/^[0-9,\s]*$/) { + _bug_numbers_only($searchstring); + } + else { + _handle_alias($searchstring); + + # Retain backslashes and quotes, to know which strings are quoted, + # and which ones are not. + 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) + || ThrowUserError('quicksearch_unbalanced_quotes', {string => $searchstring}); + + # A query cannot start with AND or OR, nor can it end with AND, OR or NOT. + ThrowUserError('quicksearch_invalid_query') + if ($words[0] =~ /^(?:AND|OR)$/ || $words[$#words] =~ /^(?:AND|OR|NOT)$/); + + my (@qswords, @or_group); + while (scalar @words) { + my $word = shift @words; + + # AND is the default word separator, similar to a whitespace, + # but |a AND OR b| is not a valid combination. + if ($word eq 'AND') { + ThrowUserError('quicksearch_invalid_query', {operators => ['AND', 'OR']}) + if $words[0] eq 'OR'; + } + + # |a OR AND b| is not a valid combination. + # |a OR OR b| is equivalent to |a OR b| and so is harmless. + elsif ($word eq 'OR') { + ThrowUserError('quicksearch_invalid_query', {operators => ['OR', 'AND']}) + if $words[0] eq 'AND'; + } + + # NOT negates the following word. + # |NOT AND| and |NOT OR| are not valid combinations. + # |NOT NOT| is fine but has no effect as they cancel themselves. + elsif ($word eq 'NOT') { + $word = shift @words; + next if $word eq 'NOT'; + if ($word eq 'AND' || $word eq 'OR') { + ThrowUserError('quicksearch_invalid_query', {operators => ['NOT', $word]}); + } + unshift(@words, "-$word"); + } + else { + # OR groups words together, as OR has higher precedence than AND. + push(@or_group, $word); + + # If the next word is not OR, then we are not in a OR group, + # or we are leaving it. + if (!defined $words[0] || $words[0] ne 'OR') { + push(@qswords, join('|', @or_group)); + @or_group = (); + } + } + } - # Remove leading and trailing commas and whitespace. - $searchstring =~ s/(^[\s,]+|[\s,]+$)//g; - ThrowUserError('buglist_parameters_required') unless ($searchstring); + _handle_status_and_resolution($qswords[0]); + shift(@qswords) if $bug_status_set; - if ($searchstring =~ m/^[0-9,\s]*$/) { - _bug_numbers_only($searchstring); - } - else { - _handle_alias($searchstring); - - # Retain backslashes and quotes, to know which strings are quoted, - # and which ones are not. - 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) - || ThrowUserError('quicksearch_unbalanced_quotes', {string => $searchstring}); - - # A query cannot start with AND or OR, nor can it end with AND, OR or NOT. - ThrowUserError('quicksearch_invalid_query') - if ($words[0] =~ /^(?:AND|OR)$/ || $words[$#words] =~ /^(?:AND|OR|NOT)$/); - - my (@qswords, @or_group); - while (scalar @words) { - my $word = shift @words; - # AND is the default word separator, similar to a whitespace, - # but |a AND OR b| is not a valid combination. - if ($word eq 'AND') { - ThrowUserError('quicksearch_invalid_query', {operators => ['AND', 'OR']}) - if $words[0] eq 'OR'; - } - # |a OR AND b| is not a valid combination. - # |a OR OR b| is equivalent to |a OR b| and so is harmless. - elsif ($word eq 'OR') { - ThrowUserError('quicksearch_invalid_query', {operators => ['OR', 'AND']}) - if $words[0] eq 'AND'; - } - # NOT negates the following word. - # |NOT AND| and |NOT OR| are not valid combinations. - # |NOT NOT| is fine but has no effect as they cancel themselves. - elsif ($word eq 'NOT') { - $word = shift @words; - next if $word eq 'NOT'; - if ($word eq 'AND' || $word eq 'OR') { - ThrowUserError('quicksearch_invalid_query', {operators => ['NOT', $word]}); - } - unshift(@words, "-$word"); - } - else { - # OR groups words together, as OR has higher precedence than AND. - push(@or_group, $word); - # If the next word is not OR, then we are not in a OR group, - # or we are leaving it. - if (!defined $words[0] || $words[0] ne 'OR') { - push(@qswords, join('|', @or_group)); - @or_group = (); - } - } - } + my (@unknownFields, %ambiguous_fields); + $fulltext = Bugzilla->user->setting('quicksearch_fulltext') eq 'on' ? 1 : 0; - _handle_status_and_resolution($qswords[0]); - shift(@qswords) if $bug_status_set; - - my (@unknownFields, %ambiguous_fields); - $fulltext = Bugzilla->user->setting('quicksearch_fulltext') eq 'on' ? 1 : 0; - - # Loop over all main-level QuickSearch words. - foreach my $qsword (@qswords) { - my @or_operand = _parse_line('\|', 1, $qsword); - foreach my $term (@or_operand) { - next unless defined $term; - my $negate = substr($term, 0, 1) eq '-'; - if ($negate) { - $term = substr($term, 1); - } - - next if _handle_special_first_chars($term, $negate); - next if _handle_field_names($term, $negate, \@unknownFields, - \%ambiguous_fields); - - # 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); - foreach my $word (@words) { - if (!_special_field_syntax($word, $negate)) { - _default_quicksearch_word($word, $negate); - } - _handle_urls($word, $negate); - } - } - $chart++; - $and = 0; - $or = 0; + # Loop over all main-level QuickSearch words. + foreach my $qsword (@qswords) { + my @or_operand = _parse_line('\|', 1, $qsword); + foreach my $term (@or_operand) { + next unless defined $term; + my $negate = substr($term, 0, 1) eq '-'; + if ($negate) { + $term = substr($term, 1); } - # If there is no mention of a bug status, we restrict the query - # to open bugs by default. - unless ($bug_status_set) { - $cgi->param('bug_status', BUG_STATE_OPEN); + next if _handle_special_first_chars($term, $negate); + next + if _handle_field_names($term, $negate, \@unknownFields, \%ambiguous_fields); + + # 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); + foreach my $word (@words) { + if (!_special_field_syntax($word, $negate)) { + _default_quicksearch_word($word, $negate); + } + _handle_urls($word, $negate); } + } + $chart++; + $and = 0; + $or = 0; + } - # Inform user about any unknown fields - if (scalar(@unknownFields) || scalar(keys %ambiguous_fields)) { - ThrowUserError("quicksearch_unknown_field", - { unknown => \@unknownFields, - ambiguous => \%ambiguous_fields }); - } + # If there is no mention of a bug status, we restrict the query + # to open bugs by default. + unless ($bug_status_set) { + $cgi->param('bug_status', BUG_STATE_OPEN); + } - # Make sure we have some query terms left - scalar($cgi->param())>0 || ThrowUserError("buglist_parameters_required"); + # Inform user about any unknown fields + if (scalar(@unknownFields) || scalar(keys %ambiguous_fields)) { + ThrowUserError("quicksearch_unknown_field", + {unknown => \@unknownFields, ambiguous => \%ambiguous_fields}); } - # List of quicksearch-specific CGI parameters to get rid of. - my @params_to_strip = ('quicksearch', 'load', 'run'); - my $modified_query_string = $cgi->canonicalise_query(@params_to_strip); + # Make sure we have some query terms left + scalar($cgi->param()) > 0 || ThrowUserError("buglist_parameters_required"); + } - if ($cgi->param('load')) { - my $urlbase = correct_urlbase(); - # Param 'load' asks us to display the query in the advanced search form. - print $cgi->redirect(-uri => "${urlbase}query.cgi?format=advanced&" - . $modified_query_string); - } + # List of quicksearch-specific CGI parameters to get rid of. + my @params_to_strip = ('quicksearch', 'load', 'run'); + my $modified_query_string = $cgi->canonicalise_query(@params_to_strip); + + if ($cgi->param('load')) { + my $urlbase = correct_urlbase(); - # Otherwise, pass the modified query string to the caller. - # We modified $cgi->params, so the caller can choose to look at that, too, - # and disregard the return value. - $cgi->delete(@params_to_strip); - return $modified_query_string; + # Param 'load' asks us to display the query in the advanced search form. + print $cgi->redirect( + -uri => "${urlbase}query.cgi?format=advanced&" . $modified_query_string); + } + + # Otherwise, pass the modified query string to the caller. + # We modified $cgi->params, so the caller can choose to look at that, too, + # and disregard the return value. + $cgi->delete(@params_to_strip); + return $modified_query_string; } ########################## @@ -274,335 +279,351 @@ sub quicksearch { ########################## sub _parse_line { - my ($delim, $keep, $line) = @_; - return () unless defined $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/'/ if defined $word; - } - return @words; + my ($delim, $keep, $line) = @_; + return () unless defined $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/'/ if defined $word; + } + return @words; } sub _bug_numbers_only { - my $searchstring = shift; - my $cgi = Bugzilla->cgi; - # Allow separation by comma or whitespace. - $searchstring =~ s/[,\s]+/,/g; - - if ($searchstring !~ /,/ && !i_am_webservice()) { - # Single bug number; shortcut to show_bug.cgi. - print $cgi->redirect( - -uri => correct_urlbase() . "show_bug.cgi?id=$searchstring"); - exit; - } - else { - # List of bug numbers. - $cgi->param('bug_id', $searchstring); - $cgi->param('order', 'bugs.bug_id'); - $cgi->param('bug_id_type', 'anyexact'); - } + my $searchstring = shift; + my $cgi = Bugzilla->cgi; + + # Allow separation by comma or whitespace. + $searchstring =~ s/[,\s]+/,/g; + + if ($searchstring !~ /,/ && !i_am_webservice()) { + + # Single bug number; shortcut to show_bug.cgi. + print $cgi->redirect( + -uri => correct_urlbase() . "show_bug.cgi?id=$searchstring"); + exit; + } + else { + # List of bug numbers. + $cgi->param('bug_id', $searchstring); + $cgi->param('order', 'bugs.bug_id'); + $cgi->param('bug_id_type', 'anyexact'); + } } sub _handle_alias { - my $searchstring = shift; - if ($searchstring =~ /^([^,\s]+)$/) { - my $alias = $1; - # We use this direct SQL because we want quicksearch to be VERY fast. - my $bug_id = Bugzilla->dbh->selectrow_array( - q{SELECT bug_id FROM bugs_aliases WHERE alias = ?}, undef, $alias); - # If the user cannot see the bug or if we are using a webservice, - # do not resolve its alias. - if ($bug_id && Bugzilla->user->can_see_bug($bug_id) && !i_am_webservice()) { - $alias = url_quote($alias); - print Bugzilla->cgi->redirect( - -uri => correct_urlbase() . "show_bug.cgi?id=$alias"); - exit; - } - } + my $searchstring = shift; + if ($searchstring =~ /^([^,\s]+)$/) { + my $alias = $1; + + # We use this direct SQL because we want quicksearch to be VERY fast. + my $bug_id + = Bugzilla->dbh->selectrow_array( + q{SELECT bug_id FROM bugs_aliases WHERE alias = ?}, + undef, $alias); + + # If the user cannot see the bug or if we are using a webservice, + # do not resolve its alias. + if ($bug_id && Bugzilla->user->can_see_bug($bug_id) && !i_am_webservice()) { + $alias = url_quote($alias); + print Bugzilla->cgi->redirect( + -uri => correct_urlbase() . "show_bug.cgi?id=$alias"); + exit; + } + } } sub _handle_status_and_resolution { - my $word = shift; - my $legal_statuses = get_legal_field_values('bug_status'); - my (%states, %resolutions); - $bug_status_set = 1; - - if ($word eq 'OPEN') { - $states{$_} = 1 foreach BUG_STATE_OPEN; - } - # If we want all bugs, then there is nothing to do. - elsif ($word ne 'ALL' - && !matchPrefixes(\%states, \%resolutions, $word, $legal_statuses)) - { - $bug_status_set = 0; - } - - # If we have wanted resolutions, allow closed states - if (keys(%resolutions)) { - foreach my $status (@$legal_statuses) { - $states{$status} = 1 unless is_open_state($status); - } - } - - Bugzilla->cgi->param('bug_status', keys(%states)); - Bugzilla->cgi->param('resolution', keys(%resolutions)); + my $word = shift; + my $legal_statuses = get_legal_field_values('bug_status'); + my (%states, %resolutions); + $bug_status_set = 1; + + if ($word eq 'OPEN') { + $states{$_} = 1 foreach BUG_STATE_OPEN; + } + + # If we want all bugs, then there is nothing to do. + elsif ($word ne 'ALL' + && !matchPrefixes(\%states, \%resolutions, $word, $legal_statuses)) + { + $bug_status_set = 0; + } + + # If we have wanted resolutions, allow closed states + if (keys(%resolutions)) { + foreach my $status (@$legal_statuses) { + $states{$status} = 1 unless is_open_state($status); + } + } + + Bugzilla->cgi->param('bug_status', keys(%states)); + Bugzilla->cgi->param('resolution', keys(%resolutions)); } sub _handle_special_first_chars { - my ($qsword, $negate) = @_; - return 0 if !defined $qsword || length($qsword) <= 1; - - my $firstChar = substr($qsword, 0, 1); - my $baseWord = substr($qsword, 1); - my @subWords = split(/,/, $baseWord); - - if ($firstChar eq '#') { - addChart('short_desc', 'substring', $baseWord, $negate); - addChart('content', 'matches', _matches_phrase($baseWord), $negate) if $fulltext; - return 1; - } - if ($firstChar eq ':') { - foreach (@subWords) { - addChart('product', 'substring', $_, $negate); - addChart('component', 'substring', $_, $negate); - } - return 1; - } - if ($firstChar eq '@') { - addChart('assigned_to', 'substring', $_, $negate) foreach (@subWords); - return 1; - } - if ($firstChar eq '[') { - addChart('short_desc', 'substring', $baseWord, $negate); - addChart('status_whiteboard', 'substring', $baseWord, $negate); - return 1; - } - if ($firstChar eq '!') { - addChart('keywords', 'anywords', $baseWord, $negate); - return 1; - } - return 0; + my ($qsword, $negate) = @_; + return 0 if !defined $qsword || length($qsword) <= 1; + + my $firstChar = substr($qsword, 0, 1); + my $baseWord = substr($qsword, 1); + my @subWords = split(/,/, $baseWord); + + if ($firstChar eq '#') { + addChart('short_desc', 'substring', $baseWord, $negate); + addChart('content', 'matches', _matches_phrase($baseWord), $negate) + if $fulltext; + return 1; + } + if ($firstChar eq ':') { + foreach (@subWords) { + addChart('product', 'substring', $_, $negate); + addChart('component', 'substring', $_, $negate); + } + return 1; + } + if ($firstChar eq '@') { + addChart('assigned_to', 'substring', $_, $negate) foreach (@subWords); + return 1; + } + if ($firstChar eq '[') { + addChart('short_desc', 'substring', $baseWord, $negate); + addChart('status_whiteboard', 'substring', $baseWord, $negate); + return 1; + } + if ($firstChar eq '!') { + addChart('keywords', 'anywords', $baseWord, $negate); + return 1; + } + return 0; } sub _handle_field_names { - my ($or_operand, $negate, $unknownFields, $ambiguous_fields) = @_; - - # Generic field1,field2,field3:value1,value2 notation. - # We have to correctly ignore commas and colons in quotes. - # Longer operators must be tested first as we don't want single character - # operators such as <, > and = to be tested before <=, >= and !=. - my @operators = sort { length($b) <=> length($a) } keys %{ OPERATOR_SYMBOLS() }; - - foreach my $symbol (@operators) { - my @field_values = _parse_line($symbol, 1, $or_operand); - next unless scalar @field_values == 2; - 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 - if (!defined $translated) { - push(@$unknownFields, $field); - } - # If we got back an array, that means the substring is - # ambiguous and could match more than field name - elsif (ref $translated) { - $ambiguous_fields->{$field} = $translated; - } - else { - if ($translated eq 'bug_status' || $translated eq 'resolution') { - $bug_status_set = 1; - } - foreach my $value (@values) { - my $operator = FIELD_OPERATOR->{$translated} - || OPERATOR_SYMBOLS->{$symbol} - || 'substring'; - # If the string was quoted to protect some special - # characters such as commas and colons, we need - # to remove quotes. - if ($value =~ /^(["'])(.+)\1$/) { - $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); - } - } + my ($or_operand, $negate, $unknownFields, $ambiguous_fields) = @_; + + # Generic field1,field2,field3:value1,value2 notation. + # We have to correctly ignore commas and colons in quotes. + # Longer operators must be tested first as we don't want single character + # operators such as <, > and = to be tested before <=, >= and !=. + my @operators = sort { length($b) <=> length($a) } keys %{OPERATOR_SYMBOLS()}; + + foreach my $symbol (@operators) { + my @field_values = _parse_line($symbol, 1, $or_operand); + next unless scalar @field_values == 2; + 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 + if (!defined $translated) { + push(@$unknownFields, $field); + } + + # If we got back an array, that means the substring is + # ambiguous and could match more than field name + elsif (ref $translated) { + $ambiguous_fields->{$field} = $translated; + } + else { + if ($translated eq 'bug_status' || $translated eq 'resolution') { + $bug_status_set = 1; + } + foreach my $value (@values) { + my $operator + = FIELD_OPERATOR->{$translated} || OPERATOR_SYMBOLS->{$symbol} || 'substring'; + + # If the string was quoted to protect some special + # characters such as commas and colons, we need + # to remove quotes. + if ($value =~ /^(["'])(.+)\1$/) { + $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; + } } + return 1; + } - # Do not look inside quoted strings. - return 0 if ($or_operand =~ /^(["']).*\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; - } + # Flag and requestee shortcut. + if ($or_operand =~ /^([^\?]+\?)([^\?]*)$/) { + _handle_flags($1, $2, $negate); + return 1; + } - return 0; + 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, they must use the Custom Search section of - # the Advanced Search page. - $chart++; - $and = $or = 0; - addChart('requestees.login_name', 'substring', $requestee, $negate); - } + 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, they 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); - my $field_map = FIELD_MAP; - - # If the field exactly matches a mapping, just return right now. - return $field_map->{$field} if exists $field_map->{$field}; - - # Check if we match, as a starting substring, exactly one field. - my @field_names = keys %$field_map; - my @matches = grep { $_ =~ /^\Q$field\E/ } @field_names; - # Eliminate duplicates that are actually the same field - # (otherwise "assi" matches both "assignee" and "assigned_to", and - # the lines below fail when they shouldn't.) - my %match_unique = map { $field_map->{$_} => $_ } @matches; - @matches = values %match_unique; - - if (scalar(@matches) == 1) { - return $field_map->{$matches[0]}; - } - elsif (scalar(@matches) > 1) { - return \@matches; - } + my $field = shift; + $field = lc($field); + my $field_map = FIELD_MAP; + + # If the field exactly matches a mapping, just return right now. + return $field_map->{$field} if exists $field_map->{$field}; + + # Check if we match, as a starting substring, exactly one field. + my @field_names = keys %$field_map; + my @matches = grep { $_ =~ /^\Q$field\E/ } @field_names; + + # Eliminate duplicates that are actually the same field + # (otherwise "assi" matches both "assignee" and "assigned_to", and + # the lines below fail when they shouldn't.) + my %match_unique = map { $field_map->{$_} => $_ } @matches; + @matches = values %match_unique; + + if (scalar(@matches) == 1) { + return $field_map->{$matches[0]}; + } + elsif (scalar(@matches) > 1) { + return \@matches; + } + + # Check if we match exactly one custom field, ignoring the cf_ on the + # custom fields (to allow people to type things like "build" for + # "cf_build"). + my %cfless; + foreach my $name (@field_names) { + my $no_cf = $name; + if ($no_cf =~ s/^cf_//) { + if ($field eq $no_cf) { + return $field_map->{$name}; + } + $cfless{$no_cf} = $name; + } + } + + # See if we match exactly one substring of any of the cf_-less fields. + my @cfless_matches = grep { $_ =~ /^\Q$field\E/ } (keys %cfless); + + if (scalar(@cfless_matches) == 1) { + my $match = $cfless_matches[0]; + my $actual_field = $cfless{$match}; + return $field_map->{$actual_field}; + } + elsif (scalar(@matches) > 1) { + return \@matches; + } + + return undef; +} - # Check if we match exactly one custom field, ignoring the cf_ on the - # custom fields (to allow people to type things like "build" for - # "cf_build"). - my %cfless; - foreach my $name (@field_names) { - my $no_cf = $name; - if ($no_cf =~ s/^cf_//) { - if ($field eq $no_cf) { - return $field_map->{$name}; - } - $cfless{$no_cf} = $name; - } - } +sub _special_field_syntax { + my ($word, $negate) = @_; - # See if we match exactly one substring of any of the cf_-less fields. - my @cfless_matches = grep { $_ =~ /^\Q$field\E/ } (keys %cfless); + # P1-5 Syntax + if ($word =~ m/^P(\d+)(?:-(\d+))?$/i) { + my ($p_start, $p_end) = ($1, $2); + my $legal_priorities = get_legal_field_values('priority'); - if (scalar(@cfless_matches) == 1) { - my $match = $cfless_matches[0]; - my $actual_field = $cfless{$match}; - return $field_map->{$actual_field}; - } - elsif (scalar(@matches) > 1) { - return \@matches; - } + # If Pn exists explicitly, use it. + my $start = firstidx { $_ eq "P$p_start" } @$legal_priorities; + my $end; + $end = firstidx { $_ eq "P$p_end" } @$legal_priorities if defined $p_end; - return undef; -} + # If Pn doesn't exist explicitly, then we mean the nth priority. + if ($start == -1) { + $start = max(0, $p_start - 1); + } + my $prios = $legal_priorities->[$start]; -sub _special_field_syntax { - my ($word, $negate) = @_; - - # P1-5 Syntax - if ($word =~ m/^P(\d+)(?:-(\d+))?$/i) { - my ($p_start, $p_end) = ($1, $2); - my $legal_priorities = get_legal_field_values('priority'); - - # If Pn exists explicitly, use it. - my $start = firstidx { $_ eq "P$p_start" } @$legal_priorities; - my $end; - $end = firstidx { $_ eq "P$p_end" } @$legal_priorities if defined $p_end; - - # If Pn doesn't exist explicitly, then we mean the nth priority. - if ($start == -1) { - $start = max(0, $p_start - 1); - } - my $prios = $legal_priorities->[$start]; - - if (defined $end) { - # If Pn doesn't exist explicitly, then we mean the nth priority. - if ($end == -1) { - $end = min(scalar(@$legal_priorities), $p_end) - 1; - $end = max(0, $end); # Just in case the user typed P0. - } - ($start, $end) = ($end, $start) if $end < $start; - $prios = join(',', @$legal_priorities[$start..$end]) - } + if (defined $end) { - addChart('priority', 'anyexact', $prios, $negate); - return 1; + # If Pn doesn't exist explicitly, then we mean the nth priority. + if ($end == -1) { + $end = min(scalar(@$legal_priorities), $p_end) - 1; + $end = max(0, $end); # Just in case the user typed P0. + } + ($start, $end) = ($end, $start) if $end < $start; + $prios = join(',', @$legal_priorities[$start .. $end]); } - return 0; + + addChart('priority', 'anyexact', $prios, $negate); + return 1; + } + return 0; } sub _default_quicksearch_word { - my ($word, $negate) = @_; - - if (!grep { lc($word) eq $_ } PRODUCT_EXCEPTIONS and length($word) > 2) { - addChart('product', 'substring', $word, $negate); - } - - if (!grep { lc($word) eq $_ } COMPONENT_EXCEPTIONS and length($word) > 2) { - addChart('component', 'substring', $word, $negate); - } - - my @legal_keywords = map($_->name, Bugzilla::Keyword->get_all); - if (grep { lc($word) eq lc($_) } @legal_keywords) { - addChart('keywords', 'substring', $word, $negate); - } - - addChart('alias', 'substring', $word, $negate); - addChart('short_desc', 'substring', $word, $negate); - addChart('status_whiteboard', 'substring', $word, $negate); - addChart('content', 'matches', _matches_phrase($word), $negate) if $fulltext; + my ($word, $negate) = @_; + + if (!grep { lc($word) eq $_ } PRODUCT_EXCEPTIONS and length($word) > 2) { + addChart('product', 'substring', $word, $negate); + } + + if (!grep { lc($word) eq $_ } COMPONENT_EXCEPTIONS and length($word) > 2) { + addChart('component', 'substring', $word, $negate); + } + + my @legal_keywords = map($_->name, Bugzilla::Keyword->get_all); + if (grep { lc($word) eq lc($_) } @legal_keywords) { + addChart('keywords', 'substring', $word, $negate); + } + + addChart('alias', 'substring', $word, $negate); + addChart('short_desc', 'substring', $word, $negate); + addChart('status_whiteboard', 'substring', $word, $negate); + addChart('content', 'matches', _matches_phrase($word), $negate) if $fulltext; } sub _handle_urls { - my ($word, $negate) = @_; - # URL field (for IP addrs, host.names, - # scheme://urls) - if ($word =~ m/[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/ - || $word =~ /^[A-Za-z]+(\.[A-Za-z]+)+/ - || $word =~ /:[\\\/][\\\/]/ - || $word =~ /localhost/ - || $word =~ /mailto[:]?/) - # || $word =~ /[A-Za-z]+[:][0-9]+/ #host:port - { - addChart('bug_file_loc', 'substring', $word, $negate); - } + my ($word, $negate) = @_; + + # URL field (for IP addrs, host.names, + # scheme://urls) + if ( $word =~ m/[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/ + || $word =~ /^[A-Za-z]+(\.[A-Za-z]+)+/ + || $word =~ /:[\\\/][\\\/]/ + || $word =~ /localhost/ + || $word =~ /mailto[:]?/) + + # || $word =~ /[A-Za-z]+[:][0-9]+/ #host:port + { + addChart('bug_file_loc', 'substring', $word, $negate); + } } ########################################################################### @@ -611,70 +632,70 @@ sub _handle_urls { # Quote and escape a phrase appropriately for a "content matches" search. sub _matches_phrase { - my ($phrase) = @_; - $phrase =~ s/"/\\"/g; - return "\"$phrase\""; + my ($phrase) = @_; + $phrase =~ s/"/\\"/g; + return "\"$phrase\""; } # Expand found prefixes to states or resolutions sub matchPrefixes { - my ($hr_states, $hr_resolutions, $word, $ar_check_states) = @_; - return unless $word =~ /^[A-Z_]+(,[A-Z_]+)*$/; - - my @ar_prefixes = split(/,/, $word); - my $ar_check_resolutions = get_legal_field_values('resolution'); - my $foundMatch = 0; - - foreach my $prefix (@ar_prefixes) { - foreach (@$ar_check_states) { - if (/^$prefix/) { - $$hr_states{$_} = 1; - $foundMatch = 1; - } - } - foreach (@$ar_check_resolutions) { - if (/^$prefix/) { - $$hr_resolutions{$_} = 1; - $foundMatch = 1; - } - } - } - return $foundMatch; + my ($hr_states, $hr_resolutions, $word, $ar_check_states) = @_; + return unless $word =~ /^[A-Z_]+(,[A-Z_]+)*$/; + + my @ar_prefixes = split(/,/, $word); + my $ar_check_resolutions = get_legal_field_values('resolution'); + my $foundMatch = 0; + + foreach my $prefix (@ar_prefixes) { + foreach (@$ar_check_states) { + if (/^$prefix/) { + $$hr_states{$_} = 1; + $foundMatch = 1; + } + } + foreach (@$ar_check_resolutions) { + if (/^$prefix/) { + $$hr_resolutions{$_} = 1; + $foundMatch = 1; + } + } + } + return $foundMatch; } # Negate comparison type sub negateComparisonType { - my $comparisonType = shift; + my $comparisonType = shift; - if ($comparisonType eq 'anywords') { - return 'nowords'; - } - return "not$comparisonType"; + if ($comparisonType eq 'anywords') { + return 'nowords'; + } + return "not$comparisonType"; } # Add a boolean chart sub addChart { - my ($field, $comparisonType, $value, $negate) = @_; - - $negate && ($comparisonType = negateComparisonType($comparisonType)); - makeChart("$chart-$and-$or", $field, $comparisonType, $value); - if ($negate) { - $and++; - $or = 0; - } - else { - $or++; - } + my ($field, $comparisonType, $value, $negate) = @_; + + $negate && ($comparisonType = negateComparisonType($comparisonType)); + makeChart("$chart-$and-$or", $field, $comparisonType, $value); + if ($negate) { + $and++; + $or = 0; + } + else { + $or++; + } } # Create the CGI parameters for a boolean chart sub makeChart { - my ($expr, $field, $type, $value) = @_; + my ($expr, $field, $type, $value) = @_; - my $cgi = Bugzilla->cgi; - $cgi->param("field$expr", $field); - $cgi->param("type$expr", $type); - $cgi->param("value$expr", $value); + my $cgi = Bugzilla->cgi; + $cgi->param("field$expr", $field); + $cgi->param("type$expr", $type); + $cgi->param("value$expr", $value); } 1; diff --git a/Bugzilla/Search/Recent.pm b/Bugzilla/Search/Recent.pm index e774c7fe0..5c12db156 100644 --- a/Bugzilla/Search/Recent.pm +++ b/Bugzilla/Search/Recent.pm @@ -21,24 +21,25 @@ use Bugzilla::Util; # Constants # ############# -use constant DB_TABLE => 'profile_search'; +use constant DB_TABLE => 'profile_search'; use constant LIST_ORDER => 'id DESC'; + # Do not track buglists viewed by users. use constant AUDIT_CREATES => 0; use constant AUDIT_UPDATES => 0; use constant AUDIT_REMOVES => 0; use constant DB_COLUMNS => qw( - id - user_id - bug_list - list_order + id + user_id + bug_list + list_order ); use constant VALIDATORS => { - user_id => \&_check_user_id, - bug_list => \&_check_bug_list, - list_order => \&_check_list_order, + user_id => \&_check_user_id, + bug_list => \&_check_bug_list, + list_order => \&_check_list_order, }; use constant UPDATE_COLUMNS => qw(bug_list list_order); @@ -51,29 +52,30 @@ use constant USE_MEMCACHED => 0; ################### sub create { - my $class = shift; - my $dbh = Bugzilla->dbh; - $dbh->bz_start_transaction(); - my $search = $class->SUPER::create(@_); - my $user_id = $search->user_id; - - # Enforce there only being SAVE_NUM_SEARCHES per user. - my @ids = @{ $dbh->selectcol_arrayref( - "SELECT id FROM profile_search WHERE user_id = ? ORDER BY id", - undef, $user_id) }; - if (scalar(@ids) > SAVE_NUM_SEARCHES) { - splice(@ids, - SAVE_NUM_SEARCHES); - $dbh->do( - "DELETE FROM profile_search WHERE id IN (" . join(',', @ids) . ")"); - } - $dbh->bz_commit_transaction(); - return $search; + my $class = shift; + my $dbh = Bugzilla->dbh; + $dbh->bz_start_transaction(); + my $search = $class->SUPER::create(@_); + my $user_id = $search->user_id; + + # Enforce there only being SAVE_NUM_SEARCHES per user. + my @ids = @{ + $dbh->selectcol_arrayref( + "SELECT id FROM profile_search WHERE user_id = ? ORDER BY id", undef, + $user_id + ) + }; + if (scalar(@ids) > SAVE_NUM_SEARCHES) { + splice(@ids, - SAVE_NUM_SEARCHES); + $dbh->do("DELETE FROM profile_search WHERE id IN (" . join(',', @ids) . ")"); + } + $dbh->bz_commit_transaction(); + return $search; } sub create_placeholder { - my $class = shift; - return $class->create({ user_id => Bugzilla->user->id, - bug_list => '' }); + my $class = shift; + return $class->create({user_id => Bugzilla->user->id, bug_list => ''}); } ############### @@ -81,41 +83,43 @@ sub create_placeholder { ############### sub check { - my $class = shift; - my $search = $class->SUPER::check(@_); - my $user = Bugzilla->user; - if ($search->user_id != $user->id) { - ThrowUserError('object_does_not_exist', { id => $search->id }); - } - return $search; + my $class = shift; + my $search = $class->SUPER::check(@_); + my $user = Bugzilla->user; + if ($search->user_id != $user->id) { + ThrowUserError('object_does_not_exist', {id => $search->id}); + } + return $search; } sub check_quietly { - my $class = shift; - my $error_mode = Bugzilla->error_mode; - Bugzilla->error_mode(ERROR_MODE_DIE); - my $search = eval { $class->check(@_) }; - Bugzilla->error_mode($error_mode); - return $search; + my $class = shift; + my $error_mode = Bugzilla->error_mode; + Bugzilla->error_mode(ERROR_MODE_DIE); + my $search = eval { $class->check(@_) }; + Bugzilla->error_mode($error_mode); + return $search; } sub new_from_cookie { - my ($invocant, $bug_ids) = @_; - my $class = ref($invocant) || $invocant; + my ($invocant, $bug_ids) = @_; + my $class = ref($invocant) || $invocant; - my $search = { id => 'cookie', - user_id => Bugzilla->user->id, - bug_list => join(',', @$bug_ids) }; + my $search = { + id => 'cookie', + user_id => Bugzilla->user->id, + bug_list => join(',', @$bug_ids) + }; - bless $search, $class; - return $search; + bless $search, $class; + return $search; } #################### # Simple Accessors # #################### -sub bug_list { return [split(',', $_[0]->{'bug_list'})]; } +sub bug_list { return [split(',', $_[0]->{'bug_list'})]; } sub list_order { return $_[0]->{'list_order'}; } sub user_id { return $_[0]->{'user_id'}; } @@ -131,17 +135,17 @@ sub set_list_order { $_[0]->set('list_order', $_[1]); } ############## sub _check_user_id { - my ($invocant, $id) = @_; - require Bugzilla::User; - return Bugzilla::User->check({ id => $id })->id; + my ($invocant, $id) = @_; + require Bugzilla::User; + return Bugzilla::User->check({id => $id})->id; } sub _check_bug_list { - my ($invocant, $list) = @_; + my ($invocant, $list) = @_; - my @bug_ids = ref($list) ? @$list : split(',', $list || ''); - detaint_natural($_) foreach @bug_ids; - return join(',', @bug_ids); + my @bug_ids = ref($list) ? @$list : split(',', $list || ''); + detaint_natural($_) foreach @bug_ids; + return join(',', @bug_ids); } sub _check_list_order { defined $_[1] ? trim($_[1]) : '' } diff --git a/Bugzilla/Search/Saved.pm b/Bugzilla/Search/Saved.pm index 50a9cdd67..1611cea56 100644 --- a/Bugzilla/Search/Saved.pm +++ b/Bugzilla/Search/Saved.pm @@ -28,22 +28,23 @@ use Scalar::Util qw(blessed); ############# use constant DB_TABLE => 'namedqueries'; + # Do not track buglists saved by users. use constant AUDIT_CREATES => 0; use constant AUDIT_UPDATES => 0; use constant AUDIT_REMOVES => 0; use constant DB_COLUMNS => qw( - id - userid - name - query + id + userid + name + query ); use constant VALIDATORS => { - name => \&_check_name, - query => \&_check_query, - link_in_footer => \&_check_link_in_footer, + name => \&_check_name, + query => \&_check_query, + link_in_footer => \&_check_link_in_footer, }; use constant UPDATE_COLUMNS => qw(name query); @@ -53,56 +54,53 @@ use constant UPDATE_COLUMNS => qw(name query); ############### sub new { - my $class = shift; - my $param = shift; - my $dbh = Bugzilla->dbh; - - my $user; - if (ref $param) { - $user = $param->{user} || Bugzilla->user; - my $name = $param->{name}; - if (!defined $name) { - ThrowCodeError('bad_arg', - {argument => 'name', - function => "${class}::new"}); - } - my $condition = 'userid = ? AND name = ?'; - my $user_id = blessed $user ? $user->id : $user; - detaint_natural($user_id) - || ThrowCodeError('param_must_be_numeric', - {function => $class . '::_init', param => 'user'}); - my @values = ($user_id, $name); - $param = { condition => $condition, values => \@values }; - } - - unshift @_, $param; - my $self = $class->SUPER::new(@_); - if ($self) { - $self->{user} = $user if blessed $user; - - # Some DBs (read: Oracle) incorrectly mark the query string as UTF-8 - # when it's coming out of the database, even though it has no UTF-8 - # characters in it, which prevents Bugzilla::CGI from later reading - # it correctly. - utf8::downgrade($self->{query}) if utf8::is_utf8($self->{query}); + my $class = shift; + my $param = shift; + my $dbh = Bugzilla->dbh; + + my $user; + if (ref $param) { + $user = $param->{user} || Bugzilla->user; + my $name = $param->{name}; + if (!defined $name) { + ThrowCodeError('bad_arg', {argument => 'name', function => "${class}::new"}); } - return $self; + my $condition = 'userid = ? AND name = ?'; + my $user_id = blessed $user ? $user->id : $user; + detaint_natural($user_id) + || ThrowCodeError('param_must_be_numeric', + {function => $class . '::_init', param => 'user'}); + my @values = ($user_id, $name); + $param = {condition => $condition, values => \@values}; + } + + unshift @_, $param; + my $self = $class->SUPER::new(@_); + if ($self) { + $self->{user} = $user if blessed $user; + + # Some DBs (read: Oracle) incorrectly mark the query string as UTF-8 + # when it's coming out of the database, even though it has no UTF-8 + # characters in it, which prevents Bugzilla::CGI from later reading + # it correctly. + utf8::downgrade($self->{query}) if utf8::is_utf8($self->{query}); + } + return $self; } sub check { - my $class = shift; - my $search = $class->SUPER::check(@_); - my $user = Bugzilla->user; - return $search if $search->user->id == $user->id; - - if (!$search->shared_with_group - or !$user->in_group($search->shared_with_group)) - { - ThrowUserError('missing_query', { name => $search->name, - sharer_id => $search->user->id }); - } - - return $search; + my $class = shift; + my $search = $class->SUPER::check(@_); + my $user = Bugzilla->user; + return $search if $search->user->id == $user->id; + + if (!$search->shared_with_group or !$user->in_group($search->shared_with_group)) + { + ThrowUserError('missing_query', + {name => $search->name, sharer_id => $search->user->id}); + } + + return $search; } ############## @@ -112,24 +110,25 @@ sub check { sub _check_link_in_footer { return $_[1] ? 1 : 0; } sub _check_name { - my ($invocant, $name) = @_; - $name = trim($name); - $name || ThrowUserError("query_name_missing"); - $name !~ /[<>&]/ || ThrowUserError("illegal_query_name"); - if (length($name) > MAX_LEN_QUERY_NAME) { - ThrowUserError("query_name_too_long"); - } - return $name; + my ($invocant, $name) = @_; + $name = trim($name); + $name || ThrowUserError("query_name_missing"); + $name !~ /[<>&]/ || ThrowUserError("illegal_query_name"); + if (length($name) > MAX_LEN_QUERY_NAME) { + ThrowUserError("query_name_too_long"); + } + return $name; } sub _check_query { - my ($invocant, $query) = @_; - $query || ThrowUserError("buglist_parameters_required"); - my $cgi = new Bugzilla::CGI($query); - $cgi->clean_search_url; - # Don't store the query name as a parameter. - $cgi->delete('known_name'); - return $cgi->query_string; + my ($invocant, $query) = @_; + $query || ThrowUserError("buglist_parameters_required"); + my $cgi = new Bugzilla::CGI($query); + $cgi->clean_search_url; + + # Don't store the query name as a parameter. + $cgi->delete('known_name'); + return $cgi->query_string; } ######################### @@ -137,170 +136,180 @@ sub _check_query { ######################### sub create { - my $class = shift; - Bugzilla->login(LOGIN_REQUIRED); - my $dbh = Bugzilla->dbh; - $class->check_required_create_fields(@_); - $dbh->bz_start_transaction(); - my $params = $class->run_create_validators(@_); - - # Right now you can only create a Saved Search for the current user. - $params->{userid} = Bugzilla->user->id; - - my $lif = delete $params->{link_in_footer}; - my $obj = $class->insert_create_data($params); - if ($lif) { - $dbh->do('INSERT INTO namedqueries_link_in_footer - (user_id, namedquery_id) VALUES (?,?)', - undef, $params->{userid}, $obj->id); - } - $dbh->bz_commit_transaction(); - - return $obj; + my $class = shift; + Bugzilla->login(LOGIN_REQUIRED); + my $dbh = Bugzilla->dbh; + $class->check_required_create_fields(@_); + $dbh->bz_start_transaction(); + my $params = $class->run_create_validators(@_); + + # Right now you can only create a Saved Search for the current user. + $params->{userid} = Bugzilla->user->id; + + my $lif = delete $params->{link_in_footer}; + my $obj = $class->insert_create_data($params); + if ($lif) { + $dbh->do( + 'INSERT INTO namedqueries_link_in_footer + (user_id, namedquery_id) VALUES (?,?)', undef, $params->{userid}, + $obj->id + ); + } + $dbh->bz_commit_transaction(); + + return $obj; } sub rename_field_value { - my ($class, $field, $old_value, $new_value) = @_; - - my $old = url_quote($old_value); - my $new = url_quote($new_value); - my $old_sql = $old; - $old_sql =~ s/([_\%])/\\$1/g; - - my $table = $class->DB_TABLE; - my $id_field = $class->ID_FIELD; - - my $dbh = Bugzilla->dbh; - $dbh->bz_start_transaction(); - - my %queries = @{ $dbh->selectcol_arrayref( - "SELECT $id_field, query FROM $table WHERE query LIKE ?", - {Columns=>[1,2]}, "\%$old_sql\%") }; - foreach my $id (keys %queries) { - my $query = $queries{$id}; - $query =~ s/\b$field=\Q$old\E\b/$field=$new/gi; - # Fix boolean charts. - while ($query =~ /\bfield(\d+-\d+-\d+)=\Q$field\E\b/gi) { - my $chart_id = $1; - # Note that this won't handle lists or substrings inside of - # boolean charts. Users will have to fix those themselves. - $query =~ s/\bvalue\Q$chart_id\E=\Q$old\E\b/value$chart_id=$new/i; - } - $dbh->do("UPDATE $table SET query = ? WHERE $id_field = ?", - undef, $query, $id); - Bugzilla->memcached->clear({ table => $table, id => $id }); + my ($class, $field, $old_value, $new_value) = @_; + + my $old = url_quote($old_value); + my $new = url_quote($new_value); + my $old_sql = $old; + $old_sql =~ s/([_\%])/\\$1/g; + + my $table = $class->DB_TABLE; + my $id_field = $class->ID_FIELD; + + my $dbh = Bugzilla->dbh; + $dbh->bz_start_transaction(); + + my %queries = @{ + $dbh->selectcol_arrayref( + "SELECT $id_field, query FROM $table WHERE query LIKE ?", + {Columns => [1, 2]}, + "\%$old_sql\%" + ) + }; + foreach my $id (keys %queries) { + my $query = $queries{$id}; + $query =~ s/\b$field=\Q$old\E\b/$field=$new/gi; + + # Fix boolean charts. + while ($query =~ /\bfield(\d+-\d+-\d+)=\Q$field\E\b/gi) { + my $chart_id = $1; + + # Note that this won't handle lists or substrings inside of + # boolean charts. Users will have to fix those themselves. + $query =~ s/\bvalue\Q$chart_id\E=\Q$old\E\b/value$chart_id=$new/i; } + $dbh->do("UPDATE $table SET query = ? WHERE $id_field = ?", undef, $query, $id); + Bugzilla->memcached->clear({table => $table, id => $id}); + } - $dbh->bz_commit_transaction(); + $dbh->bz_commit_transaction(); } sub preload { - my ($searches) = @_; - my $dbh = Bugzilla->dbh; + my ($searches) = @_; + my $dbh = Bugzilla->dbh; - return unless scalar @$searches; + return unless scalar @$searches; - my @query_ids = map { $_->id } @$searches; - my $queries_in_footer = $dbh->selectcol_arrayref( - 'SELECT namedquery_id + my @query_ids = map { $_->id } @$searches; + my $queries_in_footer = $dbh->selectcol_arrayref( + 'SELECT namedquery_id FROM namedqueries_link_in_footer WHERE ' . $dbh->sql_in('namedquery_id', \@query_ids) . ' AND user_id = ?', - undef, Bugzilla->user->id); + undef, Bugzilla->user->id + ); - my %links_in_footer = map { $_ => 1 } @$queries_in_footer; - foreach my $query (@$searches) { - $query->{link_in_footer} = ($links_in_footer{$query->id}) ? 1 : 0; - } + my %links_in_footer = map { $_ => 1 } @$queries_in_footer; + foreach my $query (@$searches) { + $query->{link_in_footer} = ($links_in_footer{$query->id}) ? 1 : 0; + } } ##################### # Complex Accessors # ##################### sub edit_link { - my ($self) = @_; - return $self->{edit_link} if defined $self->{edit_link}; - my $cgi = new Bugzilla::CGI($self->url); - if (!$cgi->param('query_type') - || !IsValidQueryType($cgi->param('query_type'))) - { - $cgi->param('query_type', 'advanced'); - } - $self->{edit_link} = $cgi->canonicalise_query; - return $self->{edit_link}; + my ($self) = @_; + return $self->{edit_link} if defined $self->{edit_link}; + my $cgi = new Bugzilla::CGI($self->url); + if (!$cgi->param('query_type') || !IsValidQueryType($cgi->param('query_type'))) + { + $cgi->param('query_type', 'advanced'); + } + $self->{edit_link} = $cgi->canonicalise_query; + return $self->{edit_link}; } sub used_in_whine { - my ($self) = @_; - return $self->{used_in_whine} if exists $self->{used_in_whine}; - ($self->{used_in_whine}) = Bugzilla->dbh->selectrow_array( - 'SELECT 1 FROM whine_events INNER JOIN whine_queries + my ($self) = @_; + return $self->{used_in_whine} if exists $self->{used_in_whine}; + ($self->{used_in_whine}) = Bugzilla->dbh->selectrow_array( + 'SELECT 1 FROM whine_events INNER JOIN whine_queries ON whine_events.id = whine_queries.eventid - WHERE whine_events.owner_userid = ? AND query_name = ?', undef, - $self->{userid}, $self->name) || 0; - return $self->{used_in_whine}; + WHERE whine_events.owner_userid = ? AND query_name = ?', undef, + $self->{userid}, $self->name + ) || 0; + return $self->{used_in_whine}; } sub link_in_footer { - my ($self, $user) = @_; - # We only cache link_in_footer for the current Bugzilla->user. - return $self->{link_in_footer} if exists $self->{link_in_footer} && !$user; - my $user_id = $user ? $user->id : Bugzilla->user->id; - my $link_in_footer = Bugzilla->dbh->selectrow_array( - 'SELECT 1 FROM namedqueries_link_in_footer - WHERE namedquery_id = ? AND user_id = ?', - undef, $self->id, $user_id) || 0; - $self->{link_in_footer} = $link_in_footer if !$user; - return $link_in_footer; + my ($self, $user) = @_; + + # We only cache link_in_footer for the current Bugzilla->user. + return $self->{link_in_footer} if exists $self->{link_in_footer} && !$user; + my $user_id = $user ? $user->id : Bugzilla->user->id; + my $link_in_footer = Bugzilla->dbh->selectrow_array( + 'SELECT 1 FROM namedqueries_link_in_footer + WHERE namedquery_id = ? AND user_id = ?', undef, $self->id, $user_id + ) || 0; + $self->{link_in_footer} = $link_in_footer if !$user; + return $link_in_footer; } sub shared_with_group { - my ($self) = @_; - return $self->{shared_with_group} if exists $self->{shared_with_group}; - # Bugzilla only currently supports sharing with one group, even - # though the database backend allows for an infinite number. - my ($group_id) = Bugzilla->dbh->selectrow_array( - 'SELECT group_id FROM namedquery_group_map WHERE namedquery_id = ?', - undef, $self->id); - $self->{shared_with_group} = $group_id ? new Bugzilla::Group($group_id) - : undef; - return $self->{shared_with_group}; + my ($self) = @_; + return $self->{shared_with_group} if exists $self->{shared_with_group}; + + # Bugzilla only currently supports sharing with one group, even + # though the database backend allows for an infinite number. + my ($group_id) + = Bugzilla->dbh->selectrow_array( + 'SELECT group_id FROM namedquery_group_map WHERE namedquery_id = ?', + undef, $self->id); + $self->{shared_with_group} = $group_id ? new Bugzilla::Group($group_id) : undef; + return $self->{shared_with_group}; } sub shared_with_users { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - if (!exists $self->{shared_with_users}) { - $self->{shared_with_users} = - $dbh->selectrow_array('SELECT COUNT(*) + if (!exists $self->{shared_with_users}) { + $self->{shared_with_users} = $dbh->selectrow_array( + 'SELECT COUNT(*) FROM namedqueries_link_in_footer INNER JOIN namedqueries ON namedquery_id = id WHERE namedquery_id = ? - AND user_id != userid', - undef, $self->id); - } - return $self->{shared_with_users}; + AND user_id != userid', undef, $self->id + ); + } + return $self->{shared_with_users}; } #################### # Simple Accessors # #################### -sub url { return $_[0]->{'query'}; } +sub url { return $_[0]->{'query'}; } sub user { - my ($self) = @_; - return $self->{user} ||= - Bugzilla::User->new({ id => $self->{userid}, cache => 1 }); + my ($self) = @_; + return $self->{user} + ||= Bugzilla::User->new({id => $self->{userid}, cache => 1}); } ############ # Mutators # ############ -sub set_name { $_[0]->set('name', $_[1]); } -sub set_url { $_[0]->set('query', $_[1]); } +sub set_name { $_[0]->set('name', $_[1]); } +sub set_url { $_[0]->set('query', $_[1]); } 1; diff --git a/Bugzilla/Sender/Transport/Sendmail.pm b/Bugzilla/Sender/Transport/Sendmail.pm index 49f00777f..d2be2cded 100644 --- a/Bugzilla/Sender/Transport/Sendmail.pm +++ b/Bugzilla/Sender/Transport/Sendmail.pm @@ -16,72 +16,93 @@ use parent qw(Email::Sender::Transport::Sendmail); use Email::Sender::Failure; sub send_email { - my ($self, $email, $envelope) = @_; - - my $pipe = $self->_sendmail_pipe($envelope); - - my $string = $email->as_string; - $string =~ s/\x0D\x0A/\x0A/g unless $^O eq 'MSWin32'; - - print $pipe $string - or Email::Sender::Failure->throw("couldn't send message to sendmail: $!"); - - unless (close $pipe) { - Email::Sender::Failure->throw("error when closing pipe to sendmail: $!") if $!; - my ($error_message, $is_transient) = _map_exitcode($? >> 8); - if (Bugzilla->params->{'use_mailer_queue'}) { - # Return success for errors which are fatal so Bugzilla knows to - # remove them from the queue. - if ($is_transient) { - Email::Sender::Failure->throw("error when closing pipe to sendmail: $error_message"); - } else { - warn "error when closing pipe to sendmail: $error_message\n"; - return $self->success; - } - } else { - Email::Sender::Failure->throw("error when closing pipe to sendmail: $error_message"); - } + my ($self, $email, $envelope) = @_; + + my $pipe = $self->_sendmail_pipe($envelope); + + my $string = $email->as_string; + $string =~ s/\x0D\x0A/\x0A/g unless $^O eq 'MSWin32'; + + print $pipe $string + or Email::Sender::Failure->throw("couldn't send message to sendmail: $!"); + + unless (close $pipe) { + Email::Sender::Failure->throw("error when closing pipe to sendmail: $!") if $!; + my ($error_message, $is_transient) = _map_exitcode($? >> 8); + if (Bugzilla->params->{'use_mailer_queue'}) { + + # Return success for errors which are fatal so Bugzilla knows to + # remove them from the queue. + if ($is_transient) { + Email::Sender::Failure->throw( + "error when closing pipe to sendmail: $error_message"); + } + else { + warn "error when closing pipe to sendmail: $error_message\n"; + return $self->success; + } + } + else { + Email::Sender::Failure->throw( + "error when closing pipe to sendmail: $error_message"); } - return $self->success; + } + return $self->success; } sub _map_exitcode { - # Returns (error message, is_transient) - # from the sendmail source (sendmail/sysexits.h) - my $code = shift; - if ($code == 64) { - return ("Command line usage error (EX_USAGE)", 1); - } elsif ($code == 65) { - return ("Data format error (EX_DATAERR)", 1); - } elsif ($code == 66) { - return ("Cannot open input (EX_NOINPUT)", 1); - } elsif ($code == 67) { - return ("Addressee unknown (EX_NOUSER)", 0); - } elsif ($code == 68) { - return ("Host name unknown (EX_NOHOST)", 0); - } elsif ($code == 69) { - return ("Service unavailable (EX_UNAVAILABLE)", 1); - } elsif ($code == 70) { - return ("Internal software error (EX_SOFTWARE)", 1); - } elsif ($code == 71) { - return ("System error (EX_OSERR)", 1); - } elsif ($code == 72) { - return ("Critical OS file missing (EX_OSFILE)", 1); - } elsif ($code == 73) { - return ("Can't create output file (EX_CANTCREAT)", 1); - } elsif ($code == 74) { - return ("Input/output error (EX_IOERR)", 1); - } elsif ($code == 75) { - return ("Temp failure (EX_TEMPFAIL)", 1); - } elsif ($code == 76) { - return ("Remote error in protocol (EX_PROTOCOL)", 1); - } elsif ($code == 77) { - return ("Permission denied (EX_NOPERM)", 1); - } elsif ($code == 78) { - return ("Configuration error (EX_CONFIG)", 1); - } else { - return ("Unknown Error ($code)", 1); - } + + # Returns (error message, is_transient) + # from the sendmail source (sendmail/sysexits.h) + my $code = shift; + if ($code == 64) { + return ("Command line usage error (EX_USAGE)", 1); + } + elsif ($code == 65) { + return ("Data format error (EX_DATAERR)", 1); + } + elsif ($code == 66) { + return ("Cannot open input (EX_NOINPUT)", 1); + } + elsif ($code == 67) { + return ("Addressee unknown (EX_NOUSER)", 0); + } + elsif ($code == 68) { + return ("Host name unknown (EX_NOHOST)", 0); + } + elsif ($code == 69) { + return ("Service unavailable (EX_UNAVAILABLE)", 1); + } + elsif ($code == 70) { + return ("Internal software error (EX_SOFTWARE)", 1); + } + elsif ($code == 71) { + return ("System error (EX_OSERR)", 1); + } + elsif ($code == 72) { + return ("Critical OS file missing (EX_OSFILE)", 1); + } + elsif ($code == 73) { + return ("Can't create output file (EX_CANTCREAT)", 1); + } + elsif ($code == 74) { + return ("Input/output error (EX_IOERR)", 1); + } + elsif ($code == 75) { + return ("Temp failure (EX_TEMPFAIL)", 1); + } + elsif ($code == 76) { + return ("Remote error in protocol (EX_PROTOCOL)", 1); + } + elsif ($code == 77) { + return ("Permission denied (EX_NOPERM)", 1); + } + elsif ($code == 78) { + return ("Configuration error (EX_CONFIG)", 1); + } + else { + return ("Unknown Error ($code)", 1); + } } 1; diff --git a/Bugzilla/Series.pm b/Bugzilla/Series.pm index 22202c6f1..36b6d13ad 100644 --- a/Bugzilla/Series.pm +++ b/Bugzilla/Series.pm @@ -7,9 +7,9 @@ # This module implements a series - a set of data to be plotted on a chart. # -# This Series is in the database if and only if self->{'series_id'} is defined. -# Note that the series being in the database does not mean that the fields of -# this object are the same as the DB entries, as the object may have been +# This Series is in the database if and only if self->{'series_id'} is defined. +# Note that the series being in the database does not mean that the fields of +# this object are the same as the DB entries, as the object may have been # altered. package Bugzilla::Series; @@ -27,224 +27,255 @@ use constant DB_TABLE => 'series'; use constant ID_FIELD => 'series_id'; sub new { - my $invocant = shift; - my $class = ref($invocant) || $invocant; - - # Create a ref to an empty hash and bless it - my $self = {}; - bless($self, $class); - - my $arg_count = scalar(@_); - - # new() can return undef if you pass in a series_id and the user doesn't - # have sufficient permissions. If you create a new series in this way, - # you need to check for an undef return, and act appropriately. - my $retval = $self; - - # There are three ways of creating Series objects. Two (CGI and Parameters) - # are for use when creating a new series. One (Database) is for retrieving - # information on existing series. - if ($arg_count == 1) { - if (ref($_[0])) { - # We've been given a CGI object to create a new Series from. - # This series may already exist - external code needs to check - # before it calls writeToDatabase(). - $self->initFromCGI($_[0]); - } - else { - # We've been given a series_id, which should represent an existing - # Series. - $retval = $self->initFromDatabase($_[0]); - } - } - elsif ($arg_count >= 6 && $arg_count <= 8) { - # We've been given a load of parameters to create a new Series from. - # Currently, undef is always passed as the first parameter; this allows - # you to call writeToDatabase() unconditionally. - # XXX - You cannot set category_id and subcategory_id from here. - $self->initFromParameters(@_); + my $invocant = shift; + my $class = ref($invocant) || $invocant; + + # Create a ref to an empty hash and bless it + my $self = {}; + bless($self, $class); + + my $arg_count = scalar(@_); + + # new() can return undef if you pass in a series_id and the user doesn't + # have sufficient permissions. If you create a new series in this way, + # you need to check for an undef return, and act appropriately. + my $retval = $self; + + # There are three ways of creating Series objects. Two (CGI and Parameters) + # are for use when creating a new series. One (Database) is for retrieving + # information on existing series. + if ($arg_count == 1) { + if (ref($_[0])) { + + # We've been given a CGI object to create a new Series from. + # This series may already exist - external code needs to check + # before it calls writeToDatabase(). + $self->initFromCGI($_[0]); } else { - die("Bad parameters passed in - invalid number of args: $arg_count"); + # We've been given a series_id, which should represent an existing + # Series. + $retval = $self->initFromDatabase($_[0]); } - - return $retval; + } + elsif ($arg_count >= 6 && $arg_count <= 8) { + + # We've been given a load of parameters to create a new Series from. + # Currently, undef is always passed as the first parameter; this allows + # you to call writeToDatabase() unconditionally. + # XXX - You cannot set category_id and subcategory_id from here. + $self->initFromParameters(@_); + } + else { + die("Bad parameters passed in - invalid number of args: $arg_count"); + } + + return $retval; } sub initFromDatabase { - my ($self, $series_id) = @_; - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; - - detaint_natural($series_id) - || ThrowCodeError("invalid_series_id", { 'series_id' => $series_id }); - - my $grouplist = $user->groups_as_string; - - my @series = $dbh->selectrow_array("SELECT series.series_id, cc1.name, " . - "cc2.name, series.name, series.creator, series.frequency, " . - "series.query, series.is_public, series.category, series.subcategory " . - "FROM series " . - "INNER JOIN series_categories AS cc1 " . - " ON series.category = cc1.id " . - "INNER JOIN series_categories AS cc2 " . - " ON series.subcategory = cc2.id " . - "LEFT JOIN category_group_map AS cgm " . - " ON series.category = cgm.category_id " . - " AND cgm.group_id NOT IN($grouplist) " . - "WHERE series.series_id = ? " . - " AND (creator = ? OR (is_public = 1 AND cgm.category_id IS NULL))", - undef, ($series_id, $user->id)); - - if (@series) { - $self->initFromParameters(@series); - return $self; - } - else { - return undef; - } + my ($self, $series_id) = @_; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + + detaint_natural($series_id) + || ThrowCodeError("invalid_series_id", {'series_id' => $series_id}); + + my $grouplist = $user->groups_as_string; + + my @series = $dbh->selectrow_array( + "SELECT series.series_id, cc1.name, " + . "cc2.name, series.name, series.creator, series.frequency, " + . "series.query, series.is_public, series.category, series.subcategory " + . "FROM series " + . "INNER JOIN series_categories AS cc1 " + . " ON series.category = cc1.id " + . "INNER JOIN series_categories AS cc2 " + . " ON series.subcategory = cc2.id " + . "LEFT JOIN category_group_map AS cgm " + . " ON series.category = cgm.category_id " + . " AND cgm.group_id NOT IN($grouplist) " + . "WHERE series.series_id = ? " + . " AND (creator = ? OR (is_public = 1 AND cgm.category_id IS NULL))", + undef, + ($series_id, $user->id) + ); + + if (@series) { + $self->initFromParameters(@series); + return $self; + } + else { + return undef; + } } sub initFromParameters { - # Pass undef as the first parameter if you are creating a new series. - my $self = shift; - ($self->{'series_id'}, $self->{'category'}, $self->{'subcategory'}, - $self->{'name'}, $self->{'creator_id'}, $self->{'frequency'}, - $self->{'query'}, $self->{'public'}, $self->{'category_id'}, - $self->{'subcategory_id'}) = @_; + # Pass undef as the first parameter if you are creating a new series. + my $self = shift; + + ( + $self->{'series_id'}, $self->{'category'}, $self->{'subcategory'}, + $self->{'name'}, $self->{'creator_id'}, $self->{'frequency'}, + $self->{'query'}, $self->{'public'}, $self->{'category_id'}, + $self->{'subcategory_id'} + ) = @_; - # If the first parameter is undefined, check if this series already - # exists and update it series_id accordingly - $self->{'series_id'} ||= $self->existsInDatabase(); + # If the first parameter is undefined, check if this series already + # exists and update it series_id accordingly + $self->{'series_id'} ||= $self->existsInDatabase(); } sub initFromCGI { - my $self = shift; - my $cgi = shift; - - $self->{'series_id'} = $cgi->param('series_id') || undef; - if (defined($self->{'series_id'})) { - detaint_natural($self->{'series_id'}) - || ThrowCodeError("invalid_series_id", - { 'series_id' => $self->{'series_id'} }); - } - - $self->{'category'} = $cgi->param('category') - || $cgi->param('newcategory') - || ThrowUserError("missing_category"); - - $self->{'subcategory'} = $cgi->param('subcategory') - || $cgi->param('newsubcategory') - || ThrowUserError("missing_subcategory"); - - $self->{'name'} = $cgi->param('name') - || ThrowUserError("missing_name"); - - $self->{'creator_id'} = Bugzilla->user->id; - - $self->{'frequency'} = $cgi->param('frequency'); - detaint_natural($self->{'frequency'}) - || ThrowUserError("missing_frequency"); - - $self->{'query'} = $cgi->canonicalise_query("format", "ctype", "action", - "category", "subcategory", "name", - "frequency", "public", "query_format"); - trick_taint($self->{'query'}); - - $self->{'public'} = $cgi->param('public') ? 1 : 0; - - # Change 'admin' here and in series.html.tmpl, or remove the check - # completely, if you want to change who can make series public. - $self->{'public'} = 0 unless Bugzilla->user->in_group('admin'); + my $self = shift; + my $cgi = shift; + + $self->{'series_id'} = $cgi->param('series_id') || undef; + if (defined($self->{'series_id'})) { + detaint_natural($self->{'series_id'}) + || ThrowCodeError("invalid_series_id", {'series_id' => $self->{'series_id'}}); + } + + $self->{'category'} + = $cgi->param('category') + || $cgi->param('newcategory') + || ThrowUserError("missing_category"); + + $self->{'subcategory'} + = $cgi->param('subcategory') + || $cgi->param('newsubcategory') + || ThrowUserError("missing_subcategory"); + + $self->{'name'} = $cgi->param('name') || ThrowUserError("missing_name"); + + $self->{'creator_id'} = Bugzilla->user->id; + + $self->{'frequency'} = $cgi->param('frequency'); + detaint_natural($self->{'frequency'}) || ThrowUserError("missing_frequency"); + + $self->{'query'} = $cgi->canonicalise_query( + "format", "ctype", "action", "category", + "subcategory", "name", "frequency", "public", + "query_format" + ); + trick_taint($self->{'query'}); + + $self->{'public'} = $cgi->param('public') ? 1 : 0; + + # Change 'admin' here and in series.html.tmpl, or remove the check + # completely, if you want to change who can make series public. + $self->{'public'} = 0 unless Bugzilla->user->in_group('admin'); } sub writeToDatabase { - my $self = shift; + my $self = shift; - my $dbh = Bugzilla->dbh; - $dbh->bz_start_transaction(); + my $dbh = Bugzilla->dbh; + $dbh->bz_start_transaction(); - my $category_id = getCategoryID($self->{'category'}); - my $subcategory_id = getCategoryID($self->{'subcategory'}); + my $category_id = getCategoryID($self->{'category'}); + my $subcategory_id = getCategoryID($self->{'subcategory'}); - my $exists; - if ($self->{'series_id'}) { - $exists = - $dbh->selectrow_array("SELECT series_id FROM series - WHERE series_id = $self->{'series_id'}"); - } - - # Is this already in the database? - if ($exists) { - # Update existing series - my $dbh = Bugzilla->dbh; - $dbh->do("UPDATE series SET " . - "category = ?, subcategory = ?," . - "name = ?, frequency = ?, is_public = ? " . - "WHERE series_id = ?", undef, - $category_id, $subcategory_id, $self->{'name'}, - $self->{'frequency'}, $self->{'public'}, - $self->{'series_id'}); - } - else { - # Insert the new series into the series table - $dbh->do("INSERT INTO series (creator, category, subcategory, " . - "name, frequency, query, is_public) VALUES " . - "(?, ?, ?, ?, ?, ?, ?)", undef, - $self->{'creator_id'}, $category_id, $subcategory_id, $self->{'name'}, - $self->{'frequency'}, $self->{'query'}, $self->{'public'}); - - # Retrieve series_id - $self->{'series_id'} = $dbh->selectrow_array("SELECT MAX(series_id) " . - "FROM series"); - $self->{'series_id'} - || ThrowCodeError("missing_series_id", { 'series' => $self }); - } - - $dbh->bz_commit_transaction(); + my $exists; + if ($self->{'series_id'}) { + $exists = $dbh->selectrow_array( + "SELECT series_id FROM series + WHERE series_id = $self->{'series_id'}" + ); + } + + # Is this already in the database? + if ($exists) { + + # Update existing series + my $dbh = Bugzilla->dbh; + $dbh->do( + "UPDATE series SET " + . "category = ?, subcategory = ?," + . "name = ?, frequency = ?, is_public = ? " + . "WHERE series_id = ?", + undef, + $category_id, + $subcategory_id, + $self->{'name'}, + $self->{'frequency'}, + $self->{'public'}, + $self->{'series_id'} + ); + } + else { + # Insert the new series into the series table + $dbh->do( + "INSERT INTO series (creator, category, subcategory, " + . "name, frequency, query, is_public) VALUES " + . "(?, ?, ?, ?, ?, ?, ?)", + undef, + $self->{'creator_id'}, + $category_id, + $subcategory_id, + $self->{'name'}, + $self->{'frequency'}, + $self->{'query'}, + $self->{'public'} + ); + + # Retrieve series_id + $self->{'series_id'} + = $dbh->selectrow_array("SELECT MAX(series_id) " . "FROM series"); + $self->{'series_id'} + || ThrowCodeError("missing_series_id", {'series' => $self}); + } + + $dbh->bz_commit_transaction(); } # Check whether a series with this name, category and subcategory exists in # the DB and, if so, returns its series_id. sub existsInDatabase { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; + + my $category_id = getCategoryID($self->{'category'}); + my $subcategory_id = getCategoryID($self->{'subcategory'}); - my $category_id = getCategoryID($self->{'category'}); - my $subcategory_id = getCategoryID($self->{'subcategory'}); - - trick_taint($self->{'name'}); - my $series_id = $dbh->selectrow_array("SELECT series_id " . - "FROM series WHERE category = $category_id " . - "AND subcategory = $subcategory_id AND name = " . - $dbh->quote($self->{'name'})); - - return($series_id); + trick_taint($self->{'name'}); + my $series_id + = $dbh->selectrow_array("SELECT series_id " + . "FROM series WHERE category = $category_id " + . "AND subcategory = $subcategory_id AND name = " + . $dbh->quote($self->{'name'})); + + return ($series_id); } # Get a category or subcategory IDs, creating the category if it doesn't exist. sub getCategoryID { - my ($category) = @_; - my $category_id; - my $dbh = Bugzilla->dbh; + my ($category) = @_; + my $category_id; + my $dbh = Bugzilla->dbh; - # This seems for the best idiom for "Do A. Then maybe do B and A again." - while (1) { - # We are quoting this to put it in the DB, so we can remove taint - trick_taint($category); + # This seems for the best idiom for "Do A. Then maybe do B and A again." + while (1) { - $category_id = $dbh->selectrow_array("SELECT id " . - "from series_categories " . - "WHERE name =" . $dbh->quote($category)); + # We are quoting this to put it in the DB, so we can remove taint + trick_taint($category); - last if defined($category_id); + $category_id + = $dbh->selectrow_array("SELECT id " + . "from series_categories " + . "WHERE name =" + . $dbh->quote($category)); - $dbh->do("INSERT INTO series_categories (name) " . - "VALUES (" . $dbh->quote($category) . ")"); - } + last if defined($category_id); + + $dbh->do("INSERT INTO series_categories (name) " + . "VALUES (" + . $dbh->quote($category) + . ")"); + } - return $category_id; + return $category_id; } ########## @@ -254,20 +285,20 @@ sub id { return $_[0]->{'series_id'}; } sub name { return $_[0]->{'name'}; } sub creator { - my $self = shift; + my $self = shift; - if (!$self->{creator} && $self->{creator_id}) { - require Bugzilla::User; - $self->{creator} = new Bugzilla::User($self->{creator_id}); - } - return $self->{creator}; + if (!$self->{creator} && $self->{creator_id}) { + require Bugzilla::User; + $self->{creator} = new Bugzilla::User($self->{creator_id}); + } + return $self->{creator}; } sub remove_from_db { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - $dbh->do('DELETE FROM series WHERE series_id = ?', undef, $self->id); + $dbh->do('DELETE FROM series WHERE series_id = ?', undef, $self->id); } 1; diff --git a/Bugzilla/Status.pm b/Bugzilla/Status.pm index 275510216..615c7b250 100644 --- a/Bugzilla/Status.pm +++ b/Bugzilla/Status.pm @@ -11,17 +11,17 @@ use 5.10.1; use strict; use warnings; -# This subclasses Bugzilla::Field::Choice instead of implementing +# This subclasses Bugzilla::Field::Choice instead of implementing # ChoiceInterface, because a bug status literally is a special type # of Field::Choice, not just an object that happens to have the same # methods. use parent qw(Bugzilla::Field::Choice Exporter); @Bugzilla::Status::EXPORT = qw( - BUG_STATE_OPEN - SPECIAL_STATUS_WORKFLOW_ACTIONS + BUG_STATE_OPEN + SPECIAL_STATUS_WORKFLOW_ACTIONS - is_open_state - closed_bug_statuses + is_open_state + closed_bug_statuses ); use Bugzilla::Error; @@ -31,25 +31,25 @@ use Bugzilla::Error; ################################ use constant SPECIAL_STATUS_WORKFLOW_ACTIONS => qw( - none - duplicate - change_resolution - clearresolution + none + duplicate + change_resolution + clearresolution ); use constant DB_TABLE => 'bug_status'; # This has all the standard Bugzilla::Field::Choice columns plus "is_open" sub DB_COLUMNS { - return ($_[0]->SUPER::DB_COLUMNS, 'is_open'); + return ($_[0]->SUPER::DB_COLUMNS, 'is_open'); } sub VALIDATORS { - my $invocant = shift; - my $validators = $invocant->SUPER::VALIDATORS; - $validators->{is_open} = \&Bugzilla::Object::check_boolean; - $validators->{value} = \&_check_value; - return $validators; + my $invocant = shift; + my $validators = $invocant->SUPER::VALIDATORS; + $validators->{is_open} = \&Bugzilla::Object::check_boolean; + $validators->{value} = \&_check_value; + return $validators; } ######################### @@ -57,17 +57,17 @@ sub VALIDATORS { ######################### sub create { - my $class = shift; - my $self = $class->SUPER::create(@_); - delete Bugzilla->request_cache->{status_bug_state_open}; - add_missing_bug_status_transitions(); - return $self; + my $class = shift; + my $self = $class->SUPER::create(@_); + delete Bugzilla->request_cache->{status_bug_state_open}; + add_missing_bug_status_transitions(); + return $self; } sub remove_from_db { - my $self = shift; - $self->SUPER::remove_from_db(); - delete Bugzilla->request_cache->{status_bug_state_open}; + my $self = shift; + $self->SUPER::remove_from_db(); + delete Bugzilla->request_cache->{status_bug_state_open}; } ############################### @@ -75,16 +75,16 @@ sub remove_from_db { ############################### sub is_active { return $_[0]->{'isactive'}; } -sub is_open { return $_[0]->{'is_open'}; } +sub is_open { return $_[0]->{'is_open'}; } sub is_static { - my $self = shift; - if ($self->name eq 'UNCONFIRMED' - || $self->name eq Bugzilla->params->{'duplicate_or_move_bug_status'}) - { - return 1; - } - return 0; + my $self = shift; + if ( $self->name eq 'UNCONFIRMED' + || $self->name eq Bugzilla->params->{'duplicate_or_move_bug_status'}) + { + return 1; + } + return 0; } ############## @@ -92,14 +92,14 @@ sub is_static { ############## sub _check_value { - my $invocant = shift; - my $value = $invocant->SUPER::_check_value(@_); - - if (grep { lc($value) eq lc($_) } SPECIAL_STATUS_WORKFLOW_ACTIONS) { - ThrowUserError('fieldvalue_reserved_word', - { field => $invocant->field, value => $value }); - } - return $value; + my $invocant = shift; + my $value = $invocant->SUPER::_check_value(@_); + + if (grep { lc($value) eq lc($_) } SPECIAL_STATUS_WORKFLOW_ACTIONS) { + ThrowUserError('fieldvalue_reserved_word', + {field => $invocant->field, value => $value}); + } + return $value; } @@ -108,118 +108,125 @@ sub _check_value { ############################### sub BUG_STATE_OPEN { - my $dbh = Bugzilla->dbh; - my $request_cache = Bugzilla->request_cache; - my $cache_key = 'status_bug_state_open'; - return @{ $request_cache->{$cache_key} } - if exists $request_cache->{$cache_key}; - - my $rows = Bugzilla->memcached->get_config({ key => $cache_key }); - if (!$rows) { - $rows = $dbh->selectcol_arrayref( - 'SELECT value FROM bug_status WHERE is_open = 1' - ); - Bugzilla->memcached->set_config({ key => $cache_key, data => $rows }); - } - - $request_cache->{$cache_key} = $rows; - return @$rows; + my $dbh = Bugzilla->dbh; + my $request_cache = Bugzilla->request_cache; + my $cache_key = 'status_bug_state_open'; + return @{$request_cache->{$cache_key}} if exists $request_cache->{$cache_key}; + + my $rows = Bugzilla->memcached->get_config({key => $cache_key}); + if (!$rows) { + $rows + = $dbh->selectcol_arrayref('SELECT value FROM bug_status WHERE is_open = 1'); + Bugzilla->memcached->set_config({key => $cache_key, data => $rows}); + } + + $request_cache->{$cache_key} = $rows; + return @$rows; } # Tells you whether or not the argument is a valid "open" state. sub is_open_state { - my ($state) = @_; - return (grep($_ eq $state, BUG_STATE_OPEN) ? 1 : 0); + my ($state) = @_; + return (grep($_ eq $state, BUG_STATE_OPEN) ? 1 : 0); } sub closed_bug_statuses { - my @bug_statuses = Bugzilla::Status->get_all; - @bug_statuses = grep { !$_->is_open } @bug_statuses; - return @bug_statuses; + my @bug_statuses = Bugzilla::Status->get_all; + @bug_statuses = grep { !$_->is_open } @bug_statuses; + return @bug_statuses; } sub can_change_to { - my $self = shift; - my $dbh = Bugzilla->dbh; - - if (!ref($self) || !defined $self->{'can_change_to'}) { - my ($cond, @args, $self_exists); - if (ref($self)) { - $cond = '= ?'; - push(@args, $self->id); - $self_exists = 1; - } - else { - $cond = 'IS NULL'; - # Let's do it so that the code below works in all cases. - $self = {}; - } - - my $new_status_ids = $dbh->selectcol_arrayref("SELECT new_status + my $self = shift; + my $dbh = Bugzilla->dbh; + + if (!ref($self) || !defined $self->{'can_change_to'}) { + my ($cond, @args, $self_exists); + if (ref($self)) { + $cond = '= ?'; + push(@args, $self->id); + $self_exists = 1; + } + else { + $cond = 'IS NULL'; + + # Let's do it so that the code below works in all cases. + $self = {}; + } + + my $new_status_ids = $dbh->selectcol_arrayref( + "SELECT new_status FROM status_workflow INNER JOIN bug_status ON id = new_status WHERE isactive = 1 AND old_status $cond - ORDER BY sortkey", - undef, @args); + ORDER BY sortkey", undef, @args + ); - # Allow the bug status to remain unchanged. - push(@$new_status_ids, $self->id) if $self_exists; - $self->{'can_change_to'} = Bugzilla::Status->new_from_list($new_status_ids); - } + # Allow the bug status to remain unchanged. + push(@$new_status_ids, $self->id) if $self_exists; + $self->{'can_change_to'} = Bugzilla::Status->new_from_list($new_status_ids); + } - return $self->{'can_change_to'}; + return $self->{'can_change_to'}; } sub comment_required_on_change_from { - my ($self, $old_status) = @_; - my ($cond, $values) = $self->_status_condition($old_status); - - my ($require_comment) = Bugzilla->dbh->selectrow_array( - "SELECT require_comment FROM status_workflow - WHERE $cond", undef, @$values); - return $require_comment; + my ($self, $old_status) = @_; + my ($cond, $values) = $self->_status_condition($old_status); + + my ($require_comment) = Bugzilla->dbh->selectrow_array( + "SELECT require_comment FROM status_workflow + WHERE $cond", undef, @$values + ); + return $require_comment; } # Used as a helper for various functions that have to deal with old_status # sometimes being NULL and sometimes having a value. sub _status_condition { - my ($self, $old_status) = @_; - my @values; - my $cond = 'old_status IS NULL'; - # We may pass a fake status object to represent the initial unset state. - if ($old_status && $old_status->id) { - $cond = 'old_status = ?'; - push(@values, $old_status->id); - } - $cond .= " AND new_status = ?"; - push(@values, $self->id); - return ($cond, \@values); + my ($self, $old_status) = @_; + my @values; + my $cond = 'old_status IS NULL'; + + # We may pass a fake status object to represent the initial unset state. + if ($old_status && $old_status->id) { + $cond = 'old_status = ?'; + push(@values, $old_status->id); + } + $cond .= " AND new_status = ?"; + push(@values, $self->id); + return ($cond, \@values); } sub add_missing_bug_status_transitions { - my $bug_status = shift || Bugzilla->params->{'duplicate_or_move_bug_status'}; - my $dbh = Bugzilla->dbh; - my $new_status = new Bugzilla::Status({name => $bug_status}); - # Silently discard invalid bug statuses. - $new_status || return; + my $bug_status = shift || Bugzilla->params->{'duplicate_or_move_bug_status'}; + my $dbh = Bugzilla->dbh; + my $new_status = new Bugzilla::Status({name => $bug_status}); + + # Silently discard invalid bug statuses. + $new_status || return; - my $missing_statuses = $dbh->selectcol_arrayref('SELECT id + my $missing_statuses = $dbh->selectcol_arrayref( + 'SELECT id FROM bug_status LEFT JOIN status_workflow ON old_status = id AND new_status = ? WHERE old_status IS NULL', - undef, $new_status->id); - - my $sth = $dbh->prepare('INSERT INTO status_workflow - (old_status, new_status) VALUES (?, ?)'); - - foreach my $old_status_id (@$missing_statuses) { - next if ($old_status_id == $new_status->id); - $sth->execute($old_status_id, $new_status->id); - } + undef, $new_status->id + ); + + my $sth = $dbh->prepare( + 'INSERT INTO status_workflow + (old_status, new_status) VALUES (?, ?)' + ); + + foreach my $old_status_id (@$missing_statuses) { + next if ($old_status_id == $new_status->id); + $sth->execute($old_status_id, $new_status->id); + } } 1; diff --git a/Bugzilla/Template.pm b/Bugzilla/Template.pm index 7294e27c1..de421a290 100644 --- a/Bugzilla/Template.pm +++ b/Bugzilla/Template.pm @@ -16,8 +16,8 @@ use Bugzilla::Constants; use Bugzilla::WebService::Constants; use Bugzilla::Hook; use Bugzilla::Install::Requirements; -use Bugzilla::Install::Util qw(install_string template_include_path - include_languages); +use Bugzilla::Install::Util qw(install_string template_include_path + include_languages); use Bugzilla::Classification; use Bugzilla::Keyword; use Bugzilla::Util; @@ -40,47 +40,49 @@ use Scalar::Util qw(blessed); use parent qw(Template); use constant FORMAT_TRIPLE => '%19s|%-28s|%-28s'; -use constant FORMAT_3_SIZE => [19,28,28]; +use constant FORMAT_3_SIZE => [19, 28, 28]; use constant FORMAT_DOUBLE => '%19s %-55s'; -use constant FORMAT_2_SIZE => [19,55]; +use constant FORMAT_2_SIZE => [19, 55]; # Pseudo-constant. sub SAFE_URL_REGEXP { - my $safe_protocols = join('|', SAFE_PROTOCOLS); - return qr/($safe_protocols):[^:\s<>\"][^\s<>\"]+[\w\/]/i; + my $safe_protocols = join('|', SAFE_PROTOCOLS); + return qr/($safe_protocols):[^:\s<>\"][^\s<>\"]+[\w\/]/i; } # Convert the constants in the Bugzilla::Constants and Bugzilla::WebService::Constants -# modules into a hash we can pass to the template object for reflection into its "constants" +# modules into a hash we can pass to the template object for reflection into its "constants" # namespace (which is like its "variables" namespace, but for constants). To do so, we # traverse the arrays of exported and exportable symbols and ignoring the rest # (which, if Constants.pm exports only constants, as it should, will be nothing else). sub _load_constants { - my %constants; - foreach my $constant (@Bugzilla::Constants::EXPORT, - @Bugzilla::Constants::EXPORT_OK) - { - if (ref Bugzilla::Constants->$constant) { - $constants{$constant} = Bugzilla::Constants->$constant; - } - else { - my @list = (Bugzilla::Constants->$constant); - $constants{$constant} = (scalar(@list) == 1) ? $list[0] : \@list; - } + my %constants; + foreach + my $constant (@Bugzilla::Constants::EXPORT, @Bugzilla::Constants::EXPORT_OK) + { + if (ref Bugzilla::Constants->$constant) { + $constants{$constant} = Bugzilla::Constants->$constant; } - - foreach my $constant (@Bugzilla::WebService::Constants::EXPORT, - @Bugzilla::WebService::Constants::EXPORT_OK) - { - if (ref Bugzilla::WebService::Constants->$constant) { - $constants{$constant} = Bugzilla::WebService::Constants->$constant; - } - else { - my @list = (Bugzilla::WebService::Constants->$constant); - $constants{$constant} = (scalar(@list) == 1) ? $list[0] : \@list; - } + else { + my @list = (Bugzilla::Constants->$constant); + $constants{$constant} = (scalar(@list) == 1) ? $list[0] : \@list; + } + } + + foreach my $constant ( + @Bugzilla::WebService::Constants::EXPORT, + @Bugzilla::WebService::Constants::EXPORT_OK + ) + { + if (ref Bugzilla::WebService::Constants->$constant) { + $constants{$constant} = Bugzilla::WebService::Constants->$constant; } - return \%constants; + else { + my @list = (Bugzilla::WebService::Constants->$constant); + $constants{$constant} = (scalar(@list) == 1) ? $list[0] : \@list; + } + } + return \%constants; } # Returns the path to the templates based on the Accept-Language @@ -88,55 +90,51 @@ sub _load_constants { # If no Accept-Language is present it uses the defined default # Templates may also be found in the extensions/ tree sub _include_path { - my $lang = shift || ''; - my $cache = Bugzilla->request_cache; - $cache->{"template_include_path_$lang"} ||= - template_include_path({ language => $lang }); - return $cache->{"template_include_path_$lang"}; + my $lang = shift || ''; + my $cache = Bugzilla->request_cache; + $cache->{"template_include_path_$lang"} + ||= template_include_path({language => $lang}); + return $cache->{"template_include_path_$lang"}; } sub get_format { - my $self = shift; - my ($template, $format, $ctype) = @_; - - $ctype //= 'html'; - $format //= ''; - - # ctype and format can have letters and a hyphen only. - if ($ctype =~ /[^a-zA-Z\-]/ || $format =~ /[^a-zA-Z\-]/) { - ThrowUserError('format_not_found', {'format' => $format, - 'ctype' => $ctype, - 'invalid' => 1}); - } - trick_taint($ctype); - trick_taint($format); - - $template .= ($format ? "-$format" : ""); - $template .= ".$ctype.tmpl"; - - # Now check that the template actually exists. We only want to check - # if the template exists; any other errors (eg parse errors) will - # end up being detected later. - eval { - $self->context->template($template); - }; - # This parsing may seem fragile, but it's OK: - # http://lists.template-toolkit.org/pipermail/templates/2003-March/004370.html - # Even if it is wrong, any sort of error is going to cause a failure - # eventually, so the only issue would be an incorrect error message - if ($@ && $@->info =~ /: not found$/) { - ThrowUserError('format_not_found', {'format' => $format, - 'ctype' => $ctype}); - } - - # Else, just return the info - return - { - 'template' => $template, - 'format' => $format, - 'extension' => $ctype, - 'ctype' => Bugzilla::Constants::contenttypes->{$ctype} - }; + my $self = shift; + my ($template, $format, $ctype) = @_; + + $ctype //= 'html'; + $format //= ''; + + # ctype and format can have letters and a hyphen only. + if ($ctype =~ /[^a-zA-Z\-]/ || $format =~ /[^a-zA-Z\-]/) { + ThrowUserError('format_not_found', + {'format' => $format, 'ctype' => $ctype, 'invalid' => 1}); + } + trick_taint($ctype); + trick_taint($format); + + $template .= ($format ? "-$format" : ""); + $template .= ".$ctype.tmpl"; + + # Now check that the template actually exists. We only want to check + # if the template exists; any other errors (eg parse errors) will + # end up being detected later. + eval { $self->context->template($template); }; + + # This parsing may seem fragile, but it's OK: + # http://lists.template-toolkit.org/pipermail/templates/2003-March/004370.html + # Even if it is wrong, any sort of error is going to cause a failure + # eventually, so the only issue would be an incorrect error message + if ($@ && $@->info =~ /: not found$/) { + ThrowUserError('format_not_found', {'format' => $format, 'ctype' => $ctype}); + } + + # Else, just return the info + return { + 'template' => $template, + 'format' => $format, + 'extension' => $ctype, + 'ctype' => Bugzilla::Constants::contenttypes->{$ctype} + }; } # This routine quoteUrls contains inspirations from the HTML::FromText CPAN @@ -147,194 +145,205 @@ sub get_format { # If you want to modify this routine, read the comments carefully sub quoteUrls { - my ($text, $bug, $comment, $user) = @_; - return $text unless $text; - $user ||= Bugzilla->user; - - # We use /g for speed, but uris can have other things inside them - # (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 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'; - - # If the comment is already wrapped, we should ignore newlines when - # looking for matching regexps. Else we should take them into account. - my $s = ($comment && $comment->already_wrapped) ? qr/\s/ : qr/\h/; - - # However, note that adding the title (for buglinks) can affect things - # In particular, attachment matches go before bug titles, so that titles - # with 'attachment 1' don't double match. - # Dupe checks go afterwards, because that uses ^ and \Z, which won't occur - # if it was substituted as a bug title (since that always involve leading - # and trailing text) - - # Because of entities, it's easier (and quicker) to do this before escaping - - my @things; - my $count = 0; - my $tmp; - - my @hook_regexes; - Bugzilla::Hook::process('bug_format_comment', - { text => \$text, bug => $bug, regexes => \@hook_regexes, - comment => $comment, user => $user }); - - foreach my $re (@hook_regexes) { - my ($match, $replace) = @$re{qw(match replace)}; - if (ref($replace) eq 'CODE') { - $text =~ s/$match/($things[$count++] = $replace->({matches => [ + my ($text, $bug, $comment, $user) = @_; + return $text unless $text; + $user ||= Bugzilla->user; + + # We use /g for speed, but uris can have other things inside them + # (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 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'; + + # If the comment is already wrapped, we should ignore newlines when + # looking for matching regexps. Else we should take them into account. + my $s = ($comment && $comment->already_wrapped) ? qr/\s/ : qr/\h/; + + # However, note that adding the title (for buglinks) can affect things + # In particular, attachment matches go before bug titles, so that titles + # with 'attachment 1' don't double match. + # Dupe checks go afterwards, because that uses ^ and \Z, which won't occur + # if it was substituted as a bug title (since that always involve leading + # and trailing text) + + # Because of entities, it's easier (and quicker) to do this before escaping + + my @things; + my $count = 0; + my $tmp; + + my @hook_regexes; + Bugzilla::Hook::process( + 'bug_format_comment', + { + text => \$text, + bug => $bug, + regexes => \@hook_regexes, + comment => $comment, + user => $user + } + ); + + foreach my $re (@hook_regexes) { + my ($match, $replace) = @$re{qw(match replace)}; + if (ref($replace) eq 'CODE') { + $text =~ s/$match/($things[$count++] = $replace->({matches => [ $1, $2, $3, $4, $5, $6, $7, $8, $9, $10]})) && ("\x{FDD2}" . ($count-1) . "\x{FDD3}")/egx; - } - else { - $text =~ s/$match/($things[$count++] = $replace) + } + else { + $text =~ s/$match/($things[$count++] = $replace) && ("\x{FDD2}" . ($count-1) . "\x{FDD3}")/egx; - } } - - # Provide tooltips for full bug links (Bug 74355) - my $urlbase_re = '(' . join('|', - map { qr/$_/ } grep($_, Bugzilla->params->{'urlbase'}, - Bugzilla->params->{'sslbase'})) . ')'; - $text =~ s~\b(${urlbase_re}\Qshow_bug.cgi?id=\E([0-9]+)(\#c([0-9]+))?)\b + } + + # Provide tooltips for full bug links (Bug 74355) + my $urlbase_re = '(' + . join('|', + map {qr/$_/} + grep($_, Bugzilla->params->{'urlbase'}, 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 })) && ("\x{FDD2}" . ($count-1) . "\x{FDD3}") ~egox; - # non-mailto protocols - my $safe_protocols = SAFE_URL_REGEXP(); - $text =~ s~\b($safe_protocols) + # non-mailto protocols + my $safe_protocols = SAFE_URL_REGEXP(); + $text =~ s~\b($safe_protocols) ~($tmp = html_quote($1)) && ($things[$count++] = "$tmp") && ("\x{FDD2}" . ($count-1) . "\x{FDD3}") ~egox; - # We have to quote now, otherwise the html itself is escaped - # THIS MEANS THAT A LITERAL ", <, >, ' MUST BE ESCAPED FOR A MATCH + # We have to quote now, otherwise the html itself is escaped + # THIS MEANS THAT A LITERAL ", <, >, ' MUST BE ESCAPED FOR A MATCH - $text = html_quote($text); + $text = html_quote($text); - # Color quoted text - $text =~ s~^(>.+)$~$1~mg; - $text =~ s~\n~\n~g; + # Color quoted text + $text =~ s~^(>.+)$~$1~mg; + $text =~ s~\n~\n~g; - # mailto: - # Use | so that $1 is defined regardless - # @ is the encoded '@' character. - $text =~ s~\b(mailto:|)?([\w\.\-\+\=]+&\#64;[\w\-]+(?:\.[\w\-]+)+)\b + # mailto: + # Use | so that $1 is defined regardless + # @ is the encoded '@' character. + $text =~ s~\b(mailto:|)?([\w\.\-\+\=]+&\#64;[\w\-]+(?:\.[\w\-]+)+)\b ~$1$2~igx; - # attachment links - $text =~ s~\b(attachment$s*\#?$s*([0-9]+)(?:$s+\[details\])?) + # attachment links + $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; - # Current bug ID this comment belongs to - my $current_bugurl = $bug ? ("show_bug.cgi?id=" . $bug->id) : ""; - - # This handles bug a, comment b type stuff. Because we're using /g - # we have to do this in one pattern, and so this is semi-messy. - # 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*([0-9]+)/i; - my $comment_word = template_var('terms')->{comment}; - 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) + # Current bug ID this comment belongs to + my $current_bugurl = $bug ? ("show_bug.cgi?id=" . $bug->id) : ""; + + # This handles bug a, comment b type stuff. Because we're using /g + # we have to do this in one pattern, and so this is semi-messy. + # 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*([0-9]+)/i; + my $comment_word = template_var('terms')->{comment}; + 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 (defined($2) ? get_bug_link($2, $1, { comment_num => $3, user => $user }) : "$1") ~egx; - # Handle a list of bug ids: bugs 1, #2, 3, 4 - # Currently, the only delimiter supported is comma. - # Concluding "and" and "or" are not supported. - my $bugs_word = template_var('terms')->{bugs}; + # Handle a list of bug ids: bugs 1, #2, 3, 4 + # Currently, the only delimiter supported is comma. + # Concluding "and" and "or" are not supported. + my $bugs_word = template_var('terms')->{bugs}; - my $bugs_re = qr/\Q$bugs_word\E$s*\#?$s* + my $bugs_re = qr/\Q$bugs_word\E$s*\#?$s* [0-9]+(?:$s*,$s*\#?$s*[0-9]+)+/ix; - $text =~ s{($bugs_re)}{ + $text =~ s{($bugs_re)}{ my $match = $1; $match =~ s/((?:#$s*)?([0-9]+))/get_bug_link($2, $1);/eg; $match; }eg; - my $comments_word = template_var('terms')->{comments}; + my $comments_word = template_var('terms')->{comments}; - my $comments_re = qr/(?:comments|\Q$comments_word\E)$s*\#?$s* + my $comments_re = qr/(?:comments|\Q$comments_word\E)$s*\#?$s* [0-9]+(?:$s*,$s*\#?$s*[0-9]+)+/ix; - $text =~ s{($comments_re)}{ + $text =~ s{($comments_re)}{ my $match = $1; $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\ ) + # 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\ ) ([0-9]+) (?=\ \*\*\*\Z) ~get_bug_link($1, $1, { user => $user }) ~egmx; - # Now remove the encoding hacks in reverse order - for (my $i = $#things; $i >= 0; $i--) { - $text =~ s/\x{FDD2}($i)\x{FDD3}/$things[$i]/eg; - } + # Now remove the encoding hacks in reverse order + for (my $i = $#things; $i >= 0; $i--) { + $text =~ s/\x{FDD2}($i)\x{FDD3}/$things[$i]/eg; + } - return $text; + return $text; } # Creates a link to an attachment, including its title. sub get_attachment_link { - my ($attachid, $link_text, $user) = @_; - $user ||= Bugzilla->user; - - my $attachment = new Bugzilla::Attachment({ id => $attachid, cache => 1 }); - - if ($attachment) { - my $title = ""; - my $className = ""; - if ($user->can_see_bug($attachment->bug_id) - && (!$attachment->isprivate || $user->is_insider)) - { - $title = $attachment->description; - } - if ($attachment->isobsolete) { - $className = "bz_obsolete"; - } - # Prevent code injection in the title. - $title = html_quote(clean_text($title)); - - $link_text =~ s/ \[details\]$//; - my $linkval = "attachment.cgi?id=$attachid"; + my ($attachid, $link_text, $user) = @_; + $user ||= Bugzilla->user; - # If the attachment is a patch, try to link to the diff rather - # than the text, by default. - my $patchlink = ""; - if ($attachment->ispatch and Bugzilla->feature('patch_viewer')) { - $patchlink = '&action=diff'; - } + my $attachment = new Bugzilla::Attachment({id => $attachid, cache => 1}); - # Whitespace matters here because these links are in
 tags.
-        return qq||
-               . qq|$link_text|
-               . qq| [details]|
-               . qq||;
+  if ($attachment) {
+    my $title     = "";
+    my $className = "";
+    if ($user->can_see_bug($attachment->bug_id)
+      && (!$attachment->isprivate || $user->is_insider))
+    {
+      $title = $attachment->description;
     }
-    else {
-        return qq{$link_text};
+    if ($attachment->isobsolete) {
+      $className = "bz_obsolete";
+    }
+
+    # Prevent code injection in the title.
+    $title = html_quote(clean_text($title));
+
+    $link_text =~ s/ \[details\]$//;
+    my $linkval = "attachment.cgi?id=$attachid";
+
+    # If the attachment is a patch, try to link to the diff rather
+    # than the text, by default.
+    my $patchlink = "";
+    if ($attachment->ispatch and Bugzilla->feature('patch_viewer')) {
+      $patchlink = '&action=diff';
     }
+
+    # Whitespace matters here because these links are in 
 tags.
+    return
+        qq||
+      . qq|$link_text|
+      . qq| [details]|
+      . qq||;
+  }
+  else {
+    return qq{$link_text};
+  }
 }
 
 # Creates a link to a bug, including its title.
@@ -345,53 +354,59 @@ sub get_attachment_link {
 #    comment in the bug
 
 sub get_bug_link {
-    my ($bug, $link_text, $options) = @_;
-    $options ||= {};
-    $options->{user} ||= Bugzilla->user;
-
-    if (defined $bug && $bug ne '') {
-        if (!blessed($bug)) {
-            require Bugzilla::Bug;
-            $bug = new Bugzilla::Bug({ id => $bug, cache => 1 });
-        }
-        return $link_text if $bug->{error};
+  my ($bug, $link_text, $options) = @_;
+  $options ||= {};
+  $options->{user} ||= Bugzilla->user;
+
+  if (defined $bug && $bug ne '') {
+    if (!blessed($bug)) {
+      require Bugzilla::Bug;
+      $bug = new Bugzilla::Bug({id => $bug, cache => 1});
     }
-
-    my $template = Bugzilla->template_inner;
-    my $linkified;
-    $template->process('bug/link.html.tmpl', 
-        { bug => $bug, link_text => $link_text, %$options }, \$linkified);
-    return $linkified;
+    return $link_text if $bug->{error};
+  }
+
+  my $template = Bugzilla->template_inner;
+  my $linkified;
+  $template->process('bug/link.html.tmpl',
+    {bug => $bug, link_text => $link_text, %$options},
+    \$linkified);
+  return $linkified;
 }
 
 # We use this instead of format because format doesn't deal well with
 # multi-byte languages.
 sub multiline_sprintf {
-    my ($format, $args, $sizes) = @_;
-    my @parts;
-    my @my_sizes = @$sizes; # Copy this so we don't modify the input array.
-    foreach my $string (@$args) {
-        my $size = shift @my_sizes;
-        my @pieces = split("\n", wrap_hard($string, $size));
-        push(@parts, \@pieces);
-    }
-
-    my $formatted;
-    while (1) {
-        # Get the first item of each part.
-        my @line = map { shift @$_ } @parts;
-        # If they're all undef, we're done.
-        last if !grep { defined $_ } @line;
-        # Make any single undef item into ''
-        @line = map { defined $_ ? $_ : '' } @line;
-        # And append a formatted line
-        $formatted .= sprintf($format, @line);
-        # Remove trailing spaces, or they become lots of =20's in
-        # quoted-printable emails.
-        $formatted =~ s/\s+$//;
-        $formatted .= "\n";
-    }
-    return $formatted;
+  my ($format, $args, $sizes) = @_;
+  my @parts;
+  my @my_sizes = @$sizes;    # Copy this so we don't modify the input array.
+  foreach my $string (@$args) {
+    my $size = shift @my_sizes;
+    my @pieces = split("\n", wrap_hard($string, $size));
+    push(@parts, \@pieces);
+  }
+
+  my $formatted;
+  while (1) {
+
+    # Get the first item of each part.
+    my @line = map { shift @$_ } @parts;
+
+    # If they're all undef, we're done.
+    last if !grep { defined $_ } @line;
+
+    # Make any single undef item into ''
+    @line = map { defined $_ ? $_ : '' } @line;
+
+    # And append a formatted line
+    $formatted .= sprintf($format, @line);
+
+    # Remove trailing spaces, or they become lots of =20's in
+    # quoted-printable emails.
+    $formatted =~ s/\s+$//;
+    $formatted .= "\n";
+  }
+  return $formatted;
 }
 
 #####################
@@ -403,17 +418,18 @@ sub multiline_sprintf {
 sub _mtime { return (stat($_[0]))[9] }
 
 sub mtime_filter {
-    my ($file_url, $mtime) = @_;
-    # This environment var is set in the .htaccess if we have mod_headers
-    # and mod_expires installed, to make sure that JS and CSS with "?"
-    # after them will still be cached by clients.
-    return $file_url if !$ENV{BZ_CACHE_CONTROL};
-    if (!$mtime) {
-        my $cgi_path = bz_locations()->{'cgi_path'};
-        my $file_path = "$cgi_path/$file_url";
-        $mtime = _mtime($file_path);
-    }
-    return "$file_url?$mtime";
+  my ($file_url, $mtime) = @_;
+
+  # This environment var is set in the .htaccess if we have mod_headers
+  # and mod_expires installed, to make sure that JS and CSS with "?"
+  # after them will still be cached by clients.
+  return $file_url if !$ENV{BZ_CACHE_CONTROL};
+  if (!$mtime) {
+    my $cgi_path  = bz_locations()->{'cgi_path'};
+    my $file_path = "$cgi_path/$file_url";
+    $mtime = _mtime($file_path);
+  }
+  return "$file_url?$mtime";
 }
 
 # Set up the skin CSS cascade:
@@ -426,183 +442,186 @@ sub mtime_filter {
 #  6. Custom Bugzilla stylesheet set
 
 sub css_files {
-    my ($style_urls, $yui, $yui_css) = @_;
+  my ($style_urls, $yui, $yui_css) = @_;
 
-    # global.css goes on every page.
-    my @requested_css = ('skins/standard/global.css', @$style_urls);
+  # global.css goes on every page.
+  my @requested_css = ('skins/standard/global.css', @$style_urls);
 
-    my @yui_required_css;
-    foreach my $yui_name (@$yui) {
-        next if !$yui_css->{$yui_name};
-        push(@yui_required_css, "js/yui/assets/skins/sam/$yui_name.css");
-    }
-    unshift(@requested_css, @yui_required_css);
-    
-    my @css_sets = map { _css_link_set($_) } @requested_css;
-    
-    my %by_type = (standard => [], skin => [], custom => []);
-    foreach my $set (@css_sets) {
-        foreach my $key (keys %$set) {
-            push(@{ $by_type{$key} }, $set->{$key});
-        }
+  my @yui_required_css;
+  foreach my $yui_name (@$yui) {
+    next if !$yui_css->{$yui_name};
+    push(@yui_required_css, "js/yui/assets/skins/sam/$yui_name.css");
+  }
+  unshift(@requested_css, @yui_required_css);
+
+  my @css_sets = map { _css_link_set($_) } @requested_css;
+
+  my %by_type = (standard => [], skin => [], custom => []);
+  foreach my $set (@css_sets) {
+    foreach my $key (keys %$set) {
+      push(@{$by_type{$key}}, $set->{$key});
     }
+  }
 
-    # build unified
-    $by_type{unified_standard_skin} = _concatenate_css($by_type{standard},
-                                                       $by_type{skin});
-    $by_type{unified_custom} = _concatenate_css($by_type{custom});
+  # build unified
+  $by_type{unified_standard_skin}
+    = _concatenate_css($by_type{standard}, $by_type{skin});
+  $by_type{unified_custom} = _concatenate_css($by_type{custom});
 
-    return \%by_type;
+  return \%by_type;
 }
 
 sub _css_link_set {
-    my ($file_name) = @_;
-
-    my %set = (standard => mtime_filter($file_name));
-
-    # We use (?:^|/) to allow Extensions to use the skins system if they want.
-    if ($file_name !~ m{(?:^|/)skins/standard/}) {
-        return \%set;
-    }
+  my ($file_name) = @_;
 
-    my $skin = Bugzilla->user->settings->{skin}->{value};
-    my $cgi_path = bz_locations()->{'cgi_path'};
-    my $skin_file_name = $file_name;
-    $skin_file_name =~ s{(?:^|/)skins/standard/}{skins/contrib/$skin/};
-    if (my $mtime = _mtime("$cgi_path/$skin_file_name")) {
-        $set{skin} = mtime_filter($skin_file_name, $mtime);
-    }
-
-    my $custom_file_name = $file_name;
-    $custom_file_name =~ s{(?:^|/)skins/standard/}{skins/custom/};
-    if (my $custom_mtime = _mtime("$cgi_path/$custom_file_name")) {
-        $set{custom} = mtime_filter($custom_file_name, $custom_mtime);
-    }
+  my %set = (standard => mtime_filter($file_name));
 
+  # We use (?:^|/) to allow Extensions to use the skins system if they want.
+  if ($file_name !~ m{(?:^|/)skins/standard/}) {
     return \%set;
+  }
+
+  my $skin           = Bugzilla->user->settings->{skin}->{value};
+  my $cgi_path       = bz_locations()->{'cgi_path'};
+  my $skin_file_name = $file_name;
+  $skin_file_name =~ s{(?:^|/)skins/standard/}{skins/contrib/$skin/};
+  if (my $mtime = _mtime("$cgi_path/$skin_file_name")) {
+    $set{skin} = mtime_filter($skin_file_name, $mtime);
+  }
+
+  my $custom_file_name = $file_name;
+  $custom_file_name =~ s{(?:^|/)skins/standard/}{skins/custom/};
+  if (my $custom_mtime = _mtime("$cgi_path/$custom_file_name")) {
+    $set{custom} = mtime_filter($custom_file_name, $custom_mtime);
+  }
+
+  return \%set;
 }
 
 sub _concatenate_css {
-    my @sources = map { @$_ } @_;
-    return unless @sources;
-
-    my %files =
-        map {
-            (my $file = $_) =~ s/(^[^\?]+)\?.+/$1/;
-            $_ => $file;
-        } @sources;
-
-    my $cgi_path   = bz_locations()->{cgi_path};
-    my $skins_path = bz_locations()->{assetsdir};
-
-    # build minified files
-    my @minified;
-    foreach my $source (@sources) {
-        next unless -e "$cgi_path/$files{$source}";
-        my $file = $skins_path . '/' . md5_hex($source) . '.css';
-        if (!-e $file) {
-            my $content = read_text("$cgi_path/$files{$source}");
-
-            # minify
-            $content =~ s{/\*.*?\*/}{}sg;   # comments
-            $content =~ s{(^\s+|\s+$)}{}mg; # leading/trailing whitespace
-            $content =~ s{\n}{}g;           # single line
-
-            # rewrite urls
-            $content =~ s{url\(([^\)]+)\)}{_css_url_rewrite($source, $1)}eig;
-
-            write_text($file, "/* $files{$source} */\n" . $content . "\n");
-        }
-        push @minified, $file;
-    }
-
-    # concat files
-    my $file = $skins_path . '/' . md5_hex(join(' ', @sources)) . '.css';
+  my @sources = map {@$_} @_;
+  return unless @sources;
+
+  my %files = map {
+    (my $file = $_) =~ s/(^[^\?]+)\?.+/$1/;
+    $_ => $file;
+  } @sources;
+
+  my $cgi_path   = bz_locations()->{cgi_path};
+  my $skins_path = bz_locations()->{assetsdir};
+
+  # build minified files
+  my @minified;
+  foreach my $source (@sources) {
+    next unless -e "$cgi_path/$files{$source}";
+    my $file = $skins_path . '/' . md5_hex($source) . '.css';
     if (!-e $file) {
-        my $content = '';
-        foreach my $source (@minified) {
-            $content .= read_text($source);
-        }
-        write_text($file, $content);
+      my $content = read_text("$cgi_path/$files{$source}");
+
+      # minify
+      $content =~ s{/\*.*?\*/}{}sg;      # comments
+      $content =~ s{(^\s+|\s+$)}{}mg;    # leading/trailing whitespace
+      $content =~ s{\n}{}g;              # single line
+
+      # rewrite urls
+      $content =~ s{url\(([^\)]+)\)}{_css_url_rewrite($source, $1)}eig;
+
+      write_text($file, "/* $files{$source} */\n" . $content . "\n");
+    }
+    push @minified, $file;
+  }
+
+  # concat files
+  my $file = $skins_path . '/' . md5_hex(join(' ', @sources)) . '.css';
+  if (!-e $file) {
+    my $content = '';
+    foreach my $source (@minified) {
+      $content .= read_text($source);
     }
+    write_text($file, $content);
+  }
 
-    $file =~ s/^\Q$cgi_path\E\///o;
-    return mtime_filter($file);
+  $file =~ s/^\Q$cgi_path\E\///o;
+  return mtime_filter($file);
 }
 
 sub _css_url_rewrite {
-    my ($source, $url) = @_;
-    # rewrite relative urls as the unified stylesheet lives in a different
-    # directory from the source
-    $url =~ s/(^['"]|['"]$)//g;
-    if (substr($url, 0, 1) eq '/' || substr($url, 0, 5) eq 'data:') {
-        return 'url(' . $url . ')';
-    }
-    return 'url(../../' . ($ENV{'PROJECT'} ? '../' : '') . dirname($source) . '/' . $url . ')';
+  my ($source, $url) = @_;
+
+  # rewrite relative urls as the unified stylesheet lives in a different
+  # directory from the source
+  $url =~ s/(^['"]|['"]$)//g;
+  if (substr($url, 0, 1) eq '/' || substr($url, 0, 5) eq 'data:') {
+    return 'url(' . $url . ')';
+  }
+  return
+      'url(../../'
+    . ($ENV{'PROJECT'} ? '../' : '')
+    . dirname($source) . '/'
+    . $url . ')';
 }
 
 sub _concatenate_js {
-    return @_ unless CONCATENATE_ASSETS;
-    my ($sources) = @_;
-    return [] unless $sources;
-    $sources = ref($sources) ? $sources : [ $sources ];
-
-    my %files =
-        map {
-            (my $file = $_) =~ s/(^[^\?]+)\?.+/$1/;
-            $_ => $file;
-        } @$sources;
-
-    my $cgi_path   = bz_locations()->{cgi_path};
-    my $skins_path = bz_locations()->{assetsdir};
-
-    # build minified files
-    my @minified;
-    foreach my $source (@$sources) {
-        next unless -e "$cgi_path/$files{$source}";
-        my $file = $skins_path . '/' . md5_hex($source) . '.js';
-        if (!-e $file) {
-            my $content = read_text("$cgi_path/$files{$source}");
-
-            # minimal minification
-            $content =~ s#/\*.*?\*/##sg;    # block comments
-            $content =~ s#(^ +| +$)##gm;    # leading/trailing spaces
-            $content =~ s#^//.+$##gm;       # single line comments
-            $content =~ s#\n{2,}#\n#g;      # blank lines
-            $content =~ s#(^\s+|\s+$)##g;   # whitespace at the start/end of file
-
-            write_text($file, ";/* $files{$source} */\n" . $content . "\n");
-        }
-        push @minified, $file;
-    }
-
-    # concat files
-    my $file = $skins_path . '/' . md5_hex(join(' ', @$sources)) . '.js';
+  return @_ unless CONCATENATE_ASSETS;
+  my ($sources) = @_;
+  return [] unless $sources;
+  $sources = ref($sources) ? $sources : [$sources];
+
+  my %files = map {
+    (my $file = $_) =~ s/(^[^\?]+)\?.+/$1/;
+    $_ => $file;
+  } @$sources;
+
+  my $cgi_path   = bz_locations()->{cgi_path};
+  my $skins_path = bz_locations()->{assetsdir};
+
+  # build minified files
+  my @minified;
+  foreach my $source (@$sources) {
+    next unless -e "$cgi_path/$files{$source}";
+    my $file = $skins_path . '/' . md5_hex($source) . '.js';
     if (!-e $file) {
-        my $content = '';
-        foreach my $source (@minified) {
-            $content .= read_text($source);
-        }
-        write_text($file, $content);
+      my $content = read_text("$cgi_path/$files{$source}");
+
+      # minimal minification
+      $content =~ s#/\*.*?\*/##sg;     # block comments
+      $content =~ s#(^ +| +$)##gm;     # leading/trailing spaces
+      $content =~ s#^//.+$##gm;        # single line comments
+      $content =~ s#\n{2,}#\n#g;       # blank lines
+      $content =~ s#(^\s+|\s+$)##g;    # whitespace at the start/end of file
+
+      write_text($file, ";/* $files{$source} */\n" . $content . "\n");
+    }
+    push @minified, $file;
+  }
+
+  # concat files
+  my $file = $skins_path . '/' . md5_hex(join(' ', @$sources)) . '.js';
+  if (!-e $file) {
+    my $content = '';
+    foreach my $source (@minified) {
+      $content .= read_text($source);
     }
+    write_text($file, $content);
+  }
 
-    $file =~ s/^\Q$cgi_path\E\///o;
-    return [ $file ];
+  $file =~ s/^\Q$cgi_path\E\///o;
+  return [$file];
 }
 
 # YUI dependency resolution
 sub yui_resolve_deps {
-    my ($yui, $yui_deps) = @_;
-    
-    my @yui_resolved;
-    foreach my $yui_name (@$yui) {
-        my $deps = $yui_deps->{$yui_name} || [];
-        foreach my $dep (reverse @$deps) {
-            push(@yui_resolved, $dep) if !grep { $_ eq $dep } @yui_resolved;
-        }
-        push(@yui_resolved, $yui_name) if !grep { $_ eq $yui_name } @yui_resolved;
+  my ($yui, $yui_deps) = @_;
+
+  my @yui_resolved;
+  foreach my $yui_name (@$yui) {
+    my $deps = $yui_deps->{$yui_name} || [];
+    foreach my $dep (reverse @$deps) {
+      push(@yui_resolved, $dep) if !grep { $_ eq $dep } @yui_resolved;
     }
-    return \@yui_resolved;
+    push(@yui_resolved, $yui_name) if !grep { $_ eq $yui_name } @yui_resolved;
+  }
+  return \@yui_resolved;
 }
 
 ###############################################################################
@@ -621,73 +640,75 @@ use Template::Stash;
 # Allow keys to start with an underscore or a dot.
 $Template::Stash::PRIVATE = undef;
 
-# Add "contains***" methods to list variables that search for one or more 
-# items in a list and return boolean values representing whether or not 
+# Add "contains***" methods to list variables that search for one or more
+# items in a list and return boolean values representing whether or not
 # one/all/any item(s) were found.
-$Template::Stash::LIST_OPS->{ contains } =
-  sub {
-      my ($list, $item) = @_;
-      if (ref $item && $item->isa('Bugzilla::Object')) {
-          return grep($_->id == $item->id, @$list);
-      } else {
-          return grep($_ eq $item, @$list);
-      }
-  };
-
-$Template::Stash::LIST_OPS->{ containsany } =
-  sub {
-      my ($list, $items) = @_;
-      foreach my $item (@$items) { 
-          if (ref $item && $item->isa('Bugzilla::Object')) {
-              return 1 if grep($_->id == $item->id, @$list);
-          } else {
-              return 1 if grep($_ eq $item, @$list);
-          }
-      }
-      return 0;
-  };
+$Template::Stash::LIST_OPS->{contains} = sub {
+  my ($list, $item) = @_;
+  if (ref $item && $item->isa('Bugzilla::Object')) {
+    return grep($_->id == $item->id, @$list);
+  }
+  else {
+    return grep($_ eq $item, @$list);
+  }
+};
+
+$Template::Stash::LIST_OPS->{containsany} = sub {
+  my ($list, $items) = @_;
+  foreach my $item (@$items) {
+    if (ref $item && $item->isa('Bugzilla::Object')) {
+      return 1 if grep($_->id == $item->id, @$list);
+    }
+    else {
+      return 1 if grep($_ eq $item, @$list);
+    }
+  }
+  return 0;
+};
 
 # Clone the array reference to leave the original one unaltered.
-$Template::Stash::LIST_OPS->{ clone } =
-  sub {
-      my $list = shift;
-      return [@$list];
-  };
+$Template::Stash::LIST_OPS->{clone} = sub {
+  my $list = shift;
+  return [@$list];
+};
 
 # Allow us to sort the list of fields correctly
-$Template::Stash::LIST_OPS->{ sort_by_field_name } =
-    sub {
-        sub field_name {
-            if ($_[0] eq 'noop') {
-                # Sort --- first
-                return '';
-            }
-            # Otherwise sort by field_desc or description
-            return $_[1]{$_[0]} || $_[0];
-        }
-        my ($list, $field_desc) = @_;
-        return [ sort { lc field_name($a, $field_desc) cmp lc field_name($b, $field_desc) } @$list ];
-    };
+$Template::Stash::LIST_OPS->{sort_by_field_name} = sub {
+
+  sub field_name {
+    if ($_[0] eq 'noop') {
+
+      # Sort --- first
+      return '';
+    }
+
+    # Otherwise sort by field_desc or description
+    return $_[1]{$_[0]} || $_[0];
+  }
+  my ($list, $field_desc) = @_;
+  return [
+    sort { lc field_name($a, $field_desc) cmp lc field_name($b, $field_desc) }
+      @$list
+  ];
+};
 
 # Allow us to still get the scalar if we use the list operation ".0" on it,
 # as we often do for defaults in query.cgi and other places.
-$Template::Stash::SCALAR_OPS->{ 0 } = 
-  sub {
-      return $_[0];
-  };
+$Template::Stash::SCALAR_OPS->{0} = sub {
+  return $_[0];
+};
 
 # Add a "truncate" method to the Template Toolkit's "scalar" object
 # that truncates a string to a certain length.
-$Template::Stash::SCALAR_OPS->{ truncate } = 
-  sub {
-      my ($string, $length, $ellipsis) = @_;
-      return $string if !$length || length($string) <= $length;
-
-      $ellipsis ||= '';
-      my $strlen = $length - length($ellipsis);
-      my $newstr = substr($string, 0, $strlen) . $ellipsis;
-      return $newstr;
-  };
+$Template::Stash::SCALAR_OPS->{truncate} = sub {
+  my ($string, $length, $ellipsis) = @_;
+  return $string if !$length || length($string) <= $length;
+
+  $ellipsis ||= '';
+  my $strlen = $length - length($ellipsis);
+  my $newstr = substr($string, 0, $strlen) . $ellipsis;
+  return $newstr;
+};
 
 # Create the template object that processes templates and specify
 # configuration parameters that apply to all templates.
@@ -695,14 +716,15 @@ $Template::Stash::SCALAR_OPS->{ truncate } =
 ###############################################################################
 
 sub process {
-    my $self = shift;
-    # All of this current_langs stuff allows template_inner to correctly
-    # determine what-language Template object it should instantiate.
-    my $current_langs = Bugzilla->request_cache->{template_current_lang} ||= [];
-    unshift(@$current_langs, $self->context->{bz_language});
-    my $retval = $self->SUPER::process(@_);
-    shift @$current_langs;
-    return $retval;
+  my $self = shift;
+
+  # All of this current_langs stuff allows template_inner to correctly
+  # determine what-language Template object it should instantiate.
+  my $current_langs = Bugzilla->request_cache->{template_current_lang} ||= [];
+  unshift(@$current_langs, $self->context->{bz_language});
+  my $retval = $self->SUPER::process(@_);
+  shift @$current_langs;
+  return $retval;
 }
 
 # Construct the Template object
@@ -711,601 +733,622 @@ sub process {
 # since we won't have a template to use...
 
 sub create {
-    my $class = shift;
-    my %opts = @_;
-
-    # IMPORTANT - If you make any FILTER changes here, make sure to
-    # make them in t/004.template.t also, if required.
-
-    my $config = {
-        # Colon-separated list of directories containing templates.
-        INCLUDE_PATH => $opts{'include_path'} 
-                        || _include_path($opts{'language'}),
-
-        # Remove white-space before template directives (PRE_CHOMP) and at the
-        # beginning and end of templates and template blocks (TRIM) for better
-        # looking, more compact content.  Use the plus sign at the beginning
-        # of directives to maintain white space (i.e. [%+ DIRECTIVE %]).
-        PRE_CHOMP => 1,
-        TRIM => 1,
-
-        # Bugzilla::Template::Plugin::Hook uses the absolute (in mod_perl)
-        # or relative (in mod_cgi) paths of hook files to explicitly compile
-        # a specific file. Also, these paths may be absolute at any time
-        # if a packager has modified bz_locations() to contain absolute
-        # paths.
-        ABSOLUTE => 1,
-        RELATIVE => $ENV{MOD_PERL} ? 0 : 1,
-
-        COMPILE_DIR => bz_locations()->{'template_cache'},
-
-        # Don't check for a template update until 1 hour has passed since the
-        # last check.
-        STAT_TTL    => 60 * 60,
-
-        # Initialize templates (f.e. by loading plugins like Hook).
-        PRE_PROCESS => ["global/variables.none.tmpl"],
-
-        ENCODING => Bugzilla->params->{'utf8'} ? 'UTF-8' : undef,
-
-        # Functions for processing text within templates in various ways.
-        # IMPORTANT!  When adding a filter here that does not override a
-        # built-in filter, please also add a stub filter to t/004template.t.
-        FILTERS => {
-
-            # Render text in required style.
-
-            inactive => [
-                sub {
-                    my($context, $isinactive) = @_;
-                    return sub {
-                        return $isinactive ? ''.$_[0].'' : $_[0];
-                    }
-                }, 1
-            ],
-
-            closed => [
-                sub {
-                    my($context, $isclosed) = @_;
-                    return sub {
-                        return $isclosed ? ''.$_[0].'' : $_[0];
-                    }
-                }, 1
-            ],
-
-            obsolete => [
-                sub {
-                    my($context, $isobsolete) = @_;
-                    return sub {
-                        return $isobsolete ? ''.$_[0].'' : $_[0];
-                    }
-                }, 1
-            ],
-
-            # Returns the text with backslashes, single/double quotes,
-            # and newlines/carriage returns escaped for use in JS strings.
-            js => sub {
-                my ($var) = @_;
-                $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//\\x3e/g;
-                return $var;
-            },
-            
-            # Converts data to base64
-            base64 => sub {
-                my ($data) = @_;
-                return encode_base64($data);
-            },
-
-            # Strips out control characters excepting whitespace
-            strip_control_chars => sub {
-                my ($data) = @_;
-                state $use_utf8 = Bugzilla->params->{'utf8'};
-                # Only run for utf8 to avoid issues with other multibyte encodings 
-                # that may be reassigning meaning to ascii characters.
-                if ($use_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
-            # to be encoded using 
-            # See bugs 4928, 22983 and 32000 for more details
-            html_linebreak => sub {
-                my ($var) = @_;
-                $var = html_quote($var);
-                $var =~ s/\r\n/\
/g;
-                $var =~ s/\n\r/\
/g;
-                $var =~ s/\r/\
/g;
-                $var =~ s/\n/\
/g;
-                return $var;
-            },
-
-            xml => \&Bugzilla::Util::xml_quote ,
-
-            # This filter is similar to url_quote but used a \ instead of a %
-            # as prefix. In addition it replaces a ' ' by a '_'.
-            css_class_quote => \&Bugzilla::Util::css_class_quote ,
-
-            # Removes control characters and trims extra whitespace.
-            clean_text => \&Bugzilla::Util::clean_text ,
-
-            quoteUrls => [ sub {
-                               my ($context, $bug, $comment, $user) = @_;
-                               return sub {
-                                   my $text = shift;
-                                   return quoteUrls($text, $bug, $comment, $user);
-                               };
-                           },
-                           1
-                         ],
-
-            bug_link => [ sub {
-                              my ($context, $bug, $options) = @_;
-                              return sub {
-                                  my $text = shift;
-                                  return get_bug_link($bug, $text, $options);
-                              };
-                          },
-                          1
-                        ],
-
-            bug_list_link => sub {
-                my ($buglist, $options) = @_;
-                return join(", ", map(get_bug_link($_, $_, $options), split(/ *, */, $buglist)));
-            },
-
-            # 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\"";
-                }
-                return $var;
-            } ,
-
-            # Format a filesize in bytes to a human readable value
-            unitconvert => sub
-            {
-                my ($data) = @_;
-                my $retval = "";
-                my %units = (
-                    'KB' => 1024,
-                    'MB' => 1024 * 1024,
-                    'GB' => 1024 * 1024 * 1024,
-                );
-
-                if ($data < 1024) {
-                    return "$data bytes";
-                } 
-                else {
-                    my $u;
-                    foreach $u ('GB', 'MB', 'KB') {
-                        if ($data >= $units{$u}) {
-                            return sprintf("%.2f %s", $data/$units{$u}, $u);
-                        }
-                    }
-                }
-            },
-
-            # Format a time for display (more info in Bugzilla::Util)
-            time => [ sub {
-                          my ($context, $format, $timezone) = @_;
-                          return sub {
-                              my $time = shift;
-                              return format_time($time, $format, $timezone);
-                          };
-                      },
-                      1
-                    ],
-
-            html => \&Bugzilla::Util::html_quote,
-
-            html_light => \&Bugzilla::Util::html_light_quote,
-
-            email => \&Bugzilla::Util::email_filter,
-            
-            mtime => \&mtime_filter,
-
-            # iCalendar contentline filter
-            ics => [ sub {
-                         my ($context, @args) = @_;
-                         return sub {
-                             my ($var) = shift;
-                             my ($par) = shift @args;
-                             my ($output) = "";
-
-                             $var =~ s/[\r\n]/ /g;
-                             $var =~ s/([;\\\",])/\\$1/g;
-
-                             if ($par) {
-                                 $output = sprintf("%s:%s", $par, $var);
-                             } else {
-                                 $output = $var;
-                             }
-                             
-                             $output =~ s/(.{75,75})/$1\n /g;
-
-                             return $output;
-                         };
-                     },
-                     1
-                     ],
-
-            # Note that using this filter is even more dangerous than
-            # using "none," and you should only use it when you're SURE
-            # the output won't be displayed directly to a web browser.
-            txt => sub {
-                my ($var) = @_;
-                # Trivial HTML tag remover
-                $var =~ s/<[^>]*>//g;
-                # And this basically reverses the html filter.
-                $var =~ s/\@/@/g;
-                $var =~ s/\<//g;
-                $var =~ s/\"/\"/g;
-                $var =~ s/\&/\&/g;
-                # Now remove extra whitespace...
-                my $collapse_filter = $Template::Filters::FILTERS->{collapse};
-                $var = $collapse_filter->($var);
-                # And if we're not in the WebService, wrap the message.
-                # (Wrapping the message in the WebService is unnecessary
-                # and causes awkward things like \n's appearing in error
-                # messages in JSON-RPC.)
-                unless (i_am_webservice()) {
-                    $var = wrap_comment($var, 72);
-                }
-                $var =~ s/\ / /g;
-
-                return $var;
-            },
-
-            # Wrap a displayed comment to the appropriate length
-            wrap_comment => [
-                sub {
-                    my ($context, $cols) = @_;
-                    return sub { wrap_comment($_[0], $cols) }
-                }, 1],
-
-            # We force filtering of every variable in key security-critical
-            # places; we have a none filter for people to use when they 
-            # really, really don't want a variable to be changed.
-            none => sub { return $_[0]; } ,
+  my $class = shift;
+  my %opts  = @_;
+
+  # IMPORTANT - If you make any FILTER changes here, make sure to
+  # make them in t/004.template.t also, if required.
+
+  my $config = {
+
+    # Colon-separated list of directories containing templates.
+    INCLUDE_PATH => $opts{'include_path'} || _include_path($opts{'language'}),
+
+    # Remove white-space before template directives (PRE_CHOMP) and at the
+    # beginning and end of templates and template blocks (TRIM) for better
+    # looking, more compact content.  Use the plus sign at the beginning
+    # of directives to maintain white space (i.e. [%+ DIRECTIVE %]).
+    PRE_CHOMP => 1,
+    TRIM      => 1,
+
+    # Bugzilla::Template::Plugin::Hook uses the absolute (in mod_perl)
+    # or relative (in mod_cgi) paths of hook files to explicitly compile
+    # a specific file. Also, these paths may be absolute at any time
+    # if a packager has modified bz_locations() to contain absolute
+    # paths.
+    ABSOLUTE => 1,
+    RELATIVE => $ENV{MOD_PERL} ? 0 : 1,
+
+    COMPILE_DIR => bz_locations()->{'template_cache'},
+
+    # Don't check for a template update until 1 hour has passed since the
+    # last check.
+    STAT_TTL => 60 * 60,
+
+    # Initialize templates (f.e. by loading plugins like Hook).
+    PRE_PROCESS => ["global/variables.none.tmpl"],
+
+    ENCODING => Bugzilla->params->{'utf8'} ? 'UTF-8' : undef,
+
+    # Functions for processing text within templates in various ways.
+    # IMPORTANT!  When adding a filter here that does not override a
+    # built-in filter, please also add a stub filter to t/004template.t.
+    FILTERS => {
+
+      # Render text in required style.
+
+      inactive => [
+        sub {
+          my ($context, $isinactive) = @_;
+          return sub {
+            return $isinactive ? '' . $_[0] . '' : $_[0];
+            }
+        },
+        1
+      ],
+
+      closed => [
+        sub {
+          my ($context, $isclosed) = @_;
+          return sub {
+            return $isclosed ? '' . $_[0] . '' : $_[0];
+            }
+        },
+        1
+      ],
+
+      obsolete => [
+        sub {
+          my ($context, $isobsolete) = @_;
+          return sub {
+            return $isobsolete ? '' . $_[0] . '' : $_[0];
+            }
+        },
+        1
+      ],
+
+      # Returns the text with backslashes, single/double quotes,
+      # and newlines/carriage returns escaped for use in JS strings.
+      js => sub {
+        my ($var) = @_;
+        $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//\\x3e/g;
+        return $var;
+      },
+
+      # Converts data to base64
+      base64 => sub {
+        my ($data) = @_;
+        return encode_base64($data);
+      },
+
+      # Strips out control characters excepting whitespace
+      strip_control_chars => sub {
+        my ($data) = @_;
+        state $use_utf8 = Bugzilla->params->{'utf8'};
+
+        # Only run for utf8 to avoid issues with other multibyte encodings
+        # that may be reassigning meaning to ascii characters.
+        if ($use_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
+      # to be encoded using 
+      # See bugs 4928, 22983 and 32000 for more details
+      html_linebreak => sub {
+        my ($var) = @_;
+        $var = html_quote($var);
+        $var =~ s/\r\n/\
/g;
+        $var =~ s/\n\r/\
/g;
+        $var =~ s/\r/\
/g;
+        $var =~ s/\n/\
/g;
+        return $var;
+      },
+
+      xml => \&Bugzilla::Util::xml_quote,
+
+      # This filter is similar to url_quote but used a \ instead of a %
+      # as prefix. In addition it replaces a ' ' by a '_'.
+      css_class_quote => \&Bugzilla::Util::css_class_quote,
+
+      # Removes control characters and trims extra whitespace.
+      clean_text => \&Bugzilla::Util::clean_text,
+
+      quoteUrls => [
+        sub {
+          my ($context, $bug, $comment, $user) = @_;
+          return sub {
+            my $text = shift;
+            return quoteUrls($text, $bug, $comment, $user);
+          };
+        },
+        1
+      ],
+
+      bug_link => [
+        sub {
+          my ($context, $bug, $options) = @_;
+          return sub {
+            my $text = shift;
+            return get_bug_link($bug, $text, $options);
+          };
         },
+        1
+      ],
+
+      bug_list_link => sub {
+        my ($buglist, $options) = @_;
+        return
+          join(", ", map(get_bug_link($_, $_, $options), split(/ *, */, $buglist)));
+      },
+
+      # 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\"";
+        }
+        return $var;
+      },
+
+      # Format a filesize in bytes to a human readable value
+      unitconvert => sub {
+        my ($data) = @_;
+        my $retval = "";
+        my %units = ('KB' => 1024, 'MB' => 1024 * 1024, 'GB' => 1024 * 1024 * 1024,);
+
+        if ($data < 1024) {
+          return "$data bytes";
+        }
+        else {
+          my $u;
+          foreach $u ('GB', 'MB', 'KB') {
+            if ($data >= $units{$u}) {
+              return sprintf("%.2f %s", $data / $units{$u}, $u);
+            }
+          }
+        }
+      },
+
+      # Format a time for display (more info in Bugzilla::Util)
+      time => [
+        sub {
+          my ($context, $format, $timezone) = @_;
+          return sub {
+            my $time = shift;
+            return format_time($time, $format, $timezone);
+          };
+        },
+        1
+      ],
+
+      html => \&Bugzilla::Util::html_quote,
+
+      html_light => \&Bugzilla::Util::html_light_quote,
+
+      email => \&Bugzilla::Util::email_filter,
+
+      mtime => \&mtime_filter,
+
+      # iCalendar contentline filter
+      ics => [
+        sub {
+          my ($context, @args) = @_;
+          return sub {
+            my ($var)    = shift;
+            my ($par)    = shift @args;
+            my ($output) = "";
+
+            $var =~ s/[\r\n]/ /g;
+            $var =~ s/([;\\\",])/\\$1/g;
+
+            if ($par) {
+              $output = sprintf("%s:%s", $par, $var);
+            }
+            else {
+              $output = $var;
+            }
+
+            $output =~ s/(.{75,75})/$1\n /g;
+
+            return $output;
+          };
+        },
+        1
+      ],
+
+      # Note that using this filter is even more dangerous than
+      # using "none," and you should only use it when you're SURE
+      # the output won't be displayed directly to a web browser.
+      txt => sub {
+        my ($var) = @_;
+
+        # Trivial HTML tag remover
+        $var =~ s/<[^>]*>//g;
+
+        # And this basically reverses the html filter.
+        $var =~ s/\@/@/g;
+        $var =~ s/\<//g;
+        $var =~ s/\"/\"/g;
+        $var =~ s/\&/\&/g;
+
+        # Now remove extra whitespace...
+        my $collapse_filter = $Template::Filters::FILTERS->{collapse};
+        $var = $collapse_filter->($var);
+
+        # And if we're not in the WebService, wrap the message.
+        # (Wrapping the message in the WebService is unnecessary
+        # and causes awkward things like \n's appearing in error
+        # messages in JSON-RPC.)
+        unless (i_am_webservice()) {
+          $var = wrap_comment($var, 72);
+        }
+        $var =~ s/\ / /g;
+
+        return $var;
+      },
 
-        PLUGIN_BASE => 'Bugzilla::Template::Plugin',
-
-        CONSTANTS => _load_constants(),
-
-        # Default variables for all templates
-        VARIABLES => {
-            # Function for retrieving global parameters.
-            'Param' => sub { return Bugzilla->params->{$_[0]}; },
-
-            # Function to create date strings
-            'time2str' => \&Date::Format::time2str,
-
-            # Fixed size column formatting for bugmail.
-            'format_columns' => sub {
-                my $cols = shift;
-                my $format = ($cols == 3) ? FORMAT_TRIPLE : FORMAT_DOUBLE;
-                my $col_size = ($cols == 3) ? FORMAT_3_SIZE : FORMAT_2_SIZE;
-                return multiline_sprintf($format, \@_, $col_size);
-            },
-
-            # Generic linear search function
-            'lsearch' => sub {
-                my ($array, $item) = @_;
-                return firstidx { $_ eq $item } @$array;
-            },
-
-            # Currently logged in user, if any
-            # If an sudo session is in progress, this is the user we're faking
-            'user' => sub { return Bugzilla->user; },
-
-            # Currenly active language
-            'current_language' => sub { return Bugzilla->current_language; },
-
-            # If an sudo session is in progress, this is the user who
-            # started the session.
-            'sudoer' => sub { return Bugzilla->sudoer; },
-
-            # Allow templates to access the "correct" URLBase value
-            'urlbase' => sub { return Bugzilla::Util::correct_urlbase(); },
-
-            # Allow templates to access docs url with users' preferred language
-            # We fall back to English if documentation in the preferred
-            # language is not available
-            'docs_urlbase' => sub {
-                my $docs_urlbase;
-                my $lang = Bugzilla->current_language;
-                # Translations currently available on readthedocs.org
-                my @rtd_translations = ('en', 'fr');
-
-                if ($lang ne 'en' && -f "docs/$lang/html/index.html") {
-                    $docs_urlbase = "docs/$lang/html/";
-                }
-                elsif (-f "docs/en/html/index.html") {
-                    $docs_urlbase = "docs/en/html/";
-                }
-                else {
-                    if (!grep { $_ eq $lang } @rtd_translations) {
-                        $lang = "en";
-                    }
-
-                    my $version = BUGZILLA_VERSION;
-                    $version =~ /^(\d+)\.(\d+)/;
-                    if ($2 % 2 == 1) {
-                        # second number is odd; development version
-                        $version = 'latest';
-                    }
-                    else {
-                        $version = "$1.$2";
-                    }
-
-                    $docs_urlbase = "https://bugzilla.readthedocs.org/$lang/$version/";
-                }
-
-                return $docs_urlbase;
-            },
-
-            # Check whether the URL is safe.
-            'is_safe_url' => sub {
-                my $url = shift;
-                return 0 unless $url;
-
-                my $safe_url_regexp = SAFE_URL_REGEXP();
-                return 1 if $url =~ /^$safe_url_regexp$/;
-                # Pointing to a local file with no colon in its name is fine.
-                return 1 if $url =~ /^[^\s<>\":]+[\w\/]$/i;
-                # If we come here, then we cannot guarantee it's safe.
-                return 0;
-            },
-
-            # 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]) : '';
-            },
-
-            'get_api_token' => sub {
-                return '' unless Bugzilla->user->id;
-                my $cache = Bugzilla->request_cache;
-                return $cache->{api_token} //= issue_api_token();
-            },
-
-            # A way for all templates to get at Field data, cached.
-            'bug_fields' => sub {
-                my $cache = Bugzilla->request_cache;
-                $cache->{template_bug_fields} ||=
-                    Bugzilla->fields({ by_name => 1 });
-                return $cache->{template_bug_fields};
-            },
-
-            # A general purpose cache to store rendered templates for reuse.
-            # Make sure to not mix language-specific data.
-            'template_cache' => sub {
-                my $cache = Bugzilla->request_cache->{template_cache} ||= {};
-                $cache->{users} ||= {};
-                return $cache;
-            },
-
-            'css_files' => \&css_files,
-            yui_resolve_deps => \&yui_resolve_deps,
-            concatenate_js => \&_concatenate_js,
-
-            # All classifications (sorted by sortkey, name)
-            'all_classifications' => sub {
-                return [map { $_->name } Bugzilla::Classification->get_all()];
-            },
-
-            # Whether or not keywords are enabled, in this Bugzilla.
-            'use_keywords' => sub { return Bugzilla::Keyword->any_exist; },
-
-            # All the keywords.
-            'all_keywords' => sub {
-                return [map { $_->name } Bugzilla::Keyword->get_all()];
-            },
-
-            'feature_enabled' => sub { return Bugzilla->feature(@_); },
-
-            # field_descs can be somewhat slow to generate, so we generate
-            # it only once per-language no matter how many times
-            # $template->process() is called.
-            'field_descs' => sub { return template_var('field_descs') },
-
-            # Calling bug/field-help.none.tmpl once per label is very
-            # expensive, so we generate it once per-language.
-            'help_html' => sub { return template_var('help_html') },
-
-            # This way we don't have to load field-descs.none.tmpl in
-            # many templates.
-            'display_value' => \&Bugzilla::Util::display_value,
-
-            'install_string' => \&Bugzilla::Install::Util::install_string,
-
-            'report_columns' => \&Bugzilla::Search::REPORT_COLUMNS,
-
-            # These don't work as normal constants.
-            DB_MODULE        => \&Bugzilla::Constants::DB_MODULE,
-            REQUIRED_MODULES => 
-                \&Bugzilla::Install::Requirements::REQUIRED_MODULES,
-            OPTIONAL_MODULES => sub {
-                my @optional = @{OPTIONAL_MODULES()};
-                foreach my $item (@optional) {
-                    my @features;
-                    foreach my $feat_id (@{ $item->{feature} }) {
-                        push(@features, install_string("feature_$feat_id"));
-                    }
-                    $item->{feature} = \@features;
-                }
-                return \@optional;
-            },
-            'default_authorizer' => sub { return Bugzilla::Auth->new() },
-
-            'login_not_email' => sub {
-                my $params = Bugzilla->params;
-                my $cache = Bugzilla->request_cache;
-
-                return $cache->{login_not_email} //=
-                  ($params->{emailsuffix}
-                     || ($params->{user_verify_class} =~ /LDAP/ && $params->{LDAPmailattribute})
-                     || ($params->{user_verify_class} =~ /RADIUS/ && $params->{RADIUS_email_suffix}))
-                  ? 1 : 0;
-            },
+      # Wrap a displayed comment to the appropriate length
+      wrap_comment => [
+        sub {
+          my ($context, $cols) = @_;
+          return sub { wrap_comment($_[0], $cols) }
         },
-    };
-    # Use a per-process provider to cache compiled templates in memory across
-    # requests.
-    my $provider_key = join(':', @{ $config->{INCLUDE_PATH} });
-    my $shared_providers = Bugzilla->process_cache->{shared_providers} ||= {};
-    $shared_providers->{$provider_key} ||= Template::Provider->new($config);
-    $config->{LOAD_TEMPLATES} = [ $shared_providers->{$provider_key} ];
-
-    local $Template::Config::CONTEXT = 'Bugzilla::Template::Context';
-
-    Bugzilla::Hook::process('template_before_create', { config => $config });
-    my $template = $class->new($config) 
-        || die("Template creation failed: " . $class->error());
-    Bugzilla::Hook::process('template_after_create', { template => $template });
-
-    # Pass on our current language to any template hooks or inner templates
-    # called by this Template object.
-    $template->context->{bz_language} = $opts{language} || '';
-
-    return $template;
+        1
+      ],
+
+      # We force filtering of every variable in key security-critical
+      # places; we have a none filter for people to use when they
+      # really, really don't want a variable to be changed.
+      none => sub { return $_[0]; },
+    },
+
+    PLUGIN_BASE => 'Bugzilla::Template::Plugin',
+
+    CONSTANTS => _load_constants(),
+
+    # Default variables for all templates
+    VARIABLES => {
+
+      # Function for retrieving global parameters.
+      'Param' => sub { return Bugzilla->params->{$_[0]}; },
+
+      # Function to create date strings
+      'time2str' => \&Date::Format::time2str,
+
+      # Fixed size column formatting for bugmail.
+      'format_columns' => sub {
+        my $cols     = shift;
+        my $format   = ($cols == 3) ? FORMAT_TRIPLE : FORMAT_DOUBLE;
+        my $col_size = ($cols == 3) ? FORMAT_3_SIZE : FORMAT_2_SIZE;
+        return multiline_sprintf($format, \@_, $col_size);
+      },
+
+      # Generic linear search function
+      'lsearch' => sub {
+        my ($array, $item) = @_;
+        return firstidx { $_ eq $item } @$array;
+      },
+
+      # Currently logged in user, if any
+      # If an sudo session is in progress, this is the user we're faking
+      'user' => sub { return Bugzilla->user; },
+
+      # Currenly active language
+      'current_language' => sub { return Bugzilla->current_language; },
+
+      # If an sudo session is in progress, this is the user who
+      # started the session.
+      'sudoer' => sub { return Bugzilla->sudoer; },
+
+      # Allow templates to access the "correct" URLBase value
+      'urlbase' => sub { return Bugzilla::Util::correct_urlbase(); },
+
+      # Allow templates to access docs url with users' preferred language
+      # We fall back to English if documentation in the preferred
+      # language is not available
+      'docs_urlbase' => sub {
+        my $docs_urlbase;
+        my $lang = Bugzilla->current_language;
+
+        # Translations currently available on readthedocs.org
+        my @rtd_translations = ('en', 'fr');
+
+        if ($lang ne 'en' && -f "docs/$lang/html/index.html") {
+          $docs_urlbase = "docs/$lang/html/";
+        }
+        elsif (-f "docs/en/html/index.html") {
+          $docs_urlbase = "docs/en/html/";
+        }
+        else {
+          if (!grep { $_ eq $lang } @rtd_translations) {
+            $lang = "en";
+          }
+
+          my $version = BUGZILLA_VERSION;
+          $version =~ /^(\d+)\.(\d+)/;
+          if ($2 % 2 == 1) {
+
+            # second number is odd; development version
+            $version = 'latest';
+          }
+          else {
+            $version = "$1.$2";
+          }
+
+          $docs_urlbase = "https://bugzilla.readthedocs.org/$lang/$version/";
+        }
+
+        return $docs_urlbase;
+      },
+
+      # Check whether the URL is safe.
+      'is_safe_url' => sub {
+        my $url = shift;
+        return 0 unless $url;
+
+        my $safe_url_regexp = SAFE_URL_REGEXP();
+        return 1 if $url =~ /^$safe_url_regexp$/;
+
+        # Pointing to a local file with no colon in its name is fine.
+        return 1 if $url =~ /^[^\s<>\":]+[\w\/]$/i;
+
+        # If we come here, then we cannot guarantee it's safe.
+        return 0;
+      },
+
+      # 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]) : '';
+      },
+
+      'get_api_token' => sub {
+        return '' unless Bugzilla->user->id;
+        my $cache = Bugzilla->request_cache;
+        return $cache->{api_token} //= issue_api_token();
+      },
+
+      # A way for all templates to get at Field data, cached.
+      'bug_fields' => sub {
+        my $cache = Bugzilla->request_cache;
+        $cache->{template_bug_fields} ||= Bugzilla->fields({by_name => 1});
+        return $cache->{template_bug_fields};
+      },
+
+      # A general purpose cache to store rendered templates for reuse.
+      # Make sure to not mix language-specific data.
+      'template_cache' => sub {
+        my $cache = Bugzilla->request_cache->{template_cache} ||= {};
+        $cache->{users} ||= {};
+        return $cache;
+      },
+
+      'css_files'      => \&css_files,
+      yui_resolve_deps => \&yui_resolve_deps,
+      concatenate_js   => \&_concatenate_js,
+
+      # All classifications (sorted by sortkey, name)
+      'all_classifications' => sub {
+        return [map { $_->name } Bugzilla::Classification->get_all()];
+      },
+
+      # Whether or not keywords are enabled, in this Bugzilla.
+      'use_keywords' => sub { return Bugzilla::Keyword->any_exist; },
+
+      # All the keywords.
+      'all_keywords' => sub {
+        return [map { $_->name } Bugzilla::Keyword->get_all()];
+      },
+
+      'feature_enabled' => sub { return Bugzilla->feature(@_); },
+
+      # field_descs can be somewhat slow to generate, so we generate
+      # it only once per-language no matter how many times
+      # $template->process() is called.
+      'field_descs' => sub { return template_var('field_descs') },
+
+      # Calling bug/field-help.none.tmpl once per label is very
+      # expensive, so we generate it once per-language.
+      'help_html' => sub { return template_var('help_html') },
+
+      # This way we don't have to load field-descs.none.tmpl in
+      # many templates.
+      'display_value' => \&Bugzilla::Util::display_value,
+
+      'install_string' => \&Bugzilla::Install::Util::install_string,
+
+      'report_columns' => \&Bugzilla::Search::REPORT_COLUMNS,
+
+      # These don't work as normal constants.
+      DB_MODULE        => \&Bugzilla::Constants::DB_MODULE,
+      REQUIRED_MODULES => \&Bugzilla::Install::Requirements::REQUIRED_MODULES,
+      OPTIONAL_MODULES => sub {
+        my @optional = @{OPTIONAL_MODULES()};
+        foreach my $item (@optional) {
+          my @features;
+          foreach my $feat_id (@{$item->{feature}}) {
+            push(@features, install_string("feature_$feat_id"));
+          }
+          $item->{feature} = \@features;
+        }
+        return \@optional;
+      },
+      'default_authorizer' => sub { return Bugzilla::Auth->new() },
+
+      'login_not_email' => sub {
+        my $params = Bugzilla->params;
+        my $cache  = Bugzilla->request_cache;
+
+        return $cache->{login_not_email}
+          //= ($params->{emailsuffix}
+            || ($params->{user_verify_class} =~ /LDAP/ && $params->{LDAPmailattribute})
+            || ($params->{user_verify_class} =~ /RADIUS/
+            && $params->{RADIUS_email_suffix})) ? 1 : 0;
+      },
+    },
+  };
+
+  # Use a per-process provider to cache compiled templates in memory across
+  # requests.
+  my $provider_key = join(':', @{$config->{INCLUDE_PATH}});
+  my $shared_providers = Bugzilla->process_cache->{shared_providers} ||= {};
+  $shared_providers->{$provider_key} ||= Template::Provider->new($config);
+  $config->{LOAD_TEMPLATES} = [$shared_providers->{$provider_key}];
+
+  local $Template::Config::CONTEXT = 'Bugzilla::Template::Context';
+
+  Bugzilla::Hook::process('template_before_create', {config => $config});
+  my $template = $class->new($config)
+    || die("Template creation failed: " . $class->error());
+  Bugzilla::Hook::process('template_after_create', {template => $template});
+
+  # Pass on our current language to any template hooks or inner templates
+  # called by this Template object.
+  $template->context->{bz_language} = $opts{language} || '';
+
+  return $template;
 }
 
 # Used as part of the two subroutines below.
 our %_templates_to_precompile;
+
 sub precompile_templates {
-    my ($output) = @_;
+  my ($output) = @_;
+
+  # Remove the compiled templates.
+  my $cache_dir = bz_locations()->{'template_cache'};
+  my $datadir   = bz_locations()->{'datadir'};
+  if (-e $cache_dir) {
+    print install_string('template_removing_dir') . "\n" if $output;
+
+    # This frequently fails if the webserver made the files, because
+    # then the webserver owns the directories.
+    rmtree($cache_dir);
 
-    # Remove the compiled templates.
-    my $cache_dir = bz_locations()->{'template_cache'};
-    my $datadir = bz_locations()->{'datadir'};
+    # Check that the directory was really removed, and if not, move it
+    # into data/deleteme/.
     if (-e $cache_dir) {
-        print install_string('template_removing_dir') . "\n" if $output;
-
-        # This frequently fails if the webserver made the files, because
-        # then the webserver owns the directories.
-        rmtree($cache_dir);
-
-        # Check that the directory was really removed, and if not, move it
-        # into data/deleteme/.
-        if (-e $cache_dir) {
-            my $deleteme = "$datadir/deleteme";
-            
-            print STDERR "\n\n",
-                install_string('template_removal_failed', 
-                               { deleteme => $deleteme, 
-                                 template_cache => $cache_dir }), "\n\n";
-            mkpath($deleteme);
-            my $random = generate_random_password();
-            rename($cache_dir, "$deleteme/$random")
-              or die "move failed: $!";
-        }
+      my $deleteme = "$datadir/deleteme";
+
+      print STDERR "\n\n",
+        install_string('template_removal_failed',
+        {deleteme => $deleteme, template_cache => $cache_dir}),
+        "\n\n";
+      mkpath($deleteme);
+      my $random = generate_random_password();
+      rename($cache_dir, "$deleteme/$random") or die "move failed: $!";
     }
+  }
 
-    print install_string('template_precompile') if $output;
+  print install_string('template_precompile') if $output;
 
-    # Pre-compile all available languages.
-    my $paths = template_include_path({ language => Bugzilla->languages });
+  # Pre-compile all available languages.
+  my $paths = template_include_path({language => Bugzilla->languages});
 
-    foreach my $dir (@$paths) {
-        my $template = Bugzilla::Template->create(include_path => [$dir]);
+  foreach my $dir (@$paths) {
+    my $template = Bugzilla::Template->create(include_path => [$dir]);
 
-        %_templates_to_precompile = ();
-        # Traverse the template hierarchy.
-        find({ wanted => \&_precompile_push, no_chdir => 1 }, $dir);
-        # The sort isn't totally necessary, but it makes debugging easier
-        # by making the templates always be compiled in the same order.
-        foreach my $file (sort keys %_templates_to_precompile) {
-            $file =~ s{^\Q$dir\E/}{};
-            # Compile the template but throw away the result. This has the side-
-            # effect of writing the compiled version to disk.
-            $template->context->template($file);
-        }
+    %_templates_to_precompile = ();
 
-        # Clear out the cached Provider object
-        Bugzilla->process_cache->{shared_providers} = undef;
-    }
+    # Traverse the template hierarchy.
+    find({wanted => \&_precompile_push, no_chdir => 1}, $dir);
 
-    # Under mod_perl, we look for templates using the absolute path of the
-    # template directory, which causes Template Toolkit to look for their 
-    # *compiled* versions using the full absolute path under the data/template
-    # directory. (Like data/template/var/www/html/bugzilla/.) To avoid
-    # re-compiling templates under mod_perl, we symlink to the
-    # already-compiled templates. This doesn't work on Windows.
-    if (!ON_WINDOWS) {
-        # We do these separately in case they're in different locations.
-        _do_template_symlink(bz_locations()->{'templatedir'});
-        _do_template_symlink(bz_locations()->{'extensionsdir'});
+    # The sort isn't totally necessary, but it makes debugging easier
+    # by making the templates always be compiled in the same order.
+    foreach my $file (sort keys %_templates_to_precompile) {
+      $file =~ s{^\Q$dir\E/}{};
+
+      # Compile the template but throw away the result. This has the side-
+      # effect of writing the compiled version to disk.
+      $template->context->template($file);
     }
 
-    # If anything created a Template object before now, clear it out.
-    delete Bugzilla->request_cache->{template};
+    # Clear out the cached Provider object
+    Bugzilla->process_cache->{shared_providers} = undef;
+  }
+
+  # Under mod_perl, we look for templates using the absolute path of the
+  # template directory, which causes Template Toolkit to look for their
+  # *compiled* versions using the full absolute path under the data/template
+  # directory. (Like data/template/var/www/html/bugzilla/.) To avoid
+  # re-compiling templates under mod_perl, we symlink to the
+  # already-compiled templates. This doesn't work on Windows.
+  if (!ON_WINDOWS) {
+
+    # We do these separately in case they're in different locations.
+    _do_template_symlink(bz_locations()->{'templatedir'});
+    _do_template_symlink(bz_locations()->{'extensionsdir'});
+  }
 
-    print install_string('done') . "\n" if $output;
+  # If anything created a Template object before now, clear it out.
+  delete Bugzilla->request_cache->{template};
+
+  print install_string('done') . "\n" if $output;
 }
 
 # Helper for precompile_templates
 sub _precompile_push {
-    my $name = $File::Find::name;
-    return if (-d $name);
-    return if ($name =~ /\/CVS\//);
-    return if ($name !~ /\.tmpl$/);
-    $_templates_to_precompile{$name} = 1;
+  my $name = $File::Find::name;
+  return if (-d $name);
+  return if ($name =~ /\/CVS\//);
+  return if ($name !~ /\.tmpl$/);
+  $_templates_to_precompile{$name} = 1;
 }
 
 # Helper for precompile_templates
 sub _do_template_symlink {
-    my $dir_to_symlink = shift;
-
-    my $abs_path = abs_path($dir_to_symlink);
-
-    # If $dir_to_symlink is already an absolute path (as might happen
-    # with packagers who set $libpath to an absolute path), then we don't
-    # need to do this symlink.
-    return if ($abs_path eq $dir_to_symlink);
-
-    my $abs_root  = dirname($abs_path);
-    my $dir_name  = basename($abs_path);
-    my $cache_dir   = bz_locations()->{'template_cache'};
-    my $container = "$cache_dir$abs_root";
-    mkpath($container);
-    my $target = "$cache_dir/$dir_name";
-    # Check if the directory exists, because if there are no extensions,
-    # there won't be an "data/template/extensions" directory to link to.
-    if (-d $target) {
-        # We use abs2rel so that the symlink will look like 
-        # "../../../../template" which works, while just 
-        # "data/template/template/" doesn't work.
-        my $relative_target = File::Spec->abs2rel($target, $container);
-
-        my $link_name = "$container/$dir_name";
-        symlink($relative_target, $link_name)
-          or warn "Could not make $link_name a symlink to $relative_target: $!";
-    }
+  my $dir_to_symlink = shift;
+
+  my $abs_path = abs_path($dir_to_symlink);
+
+  # If $dir_to_symlink is already an absolute path (as might happen
+  # with packagers who set $libpath to an absolute path), then we don't
+  # need to do this symlink.
+  return if ($abs_path eq $dir_to_symlink);
+
+  my $abs_root  = dirname($abs_path);
+  my $dir_name  = basename($abs_path);
+  my $cache_dir = bz_locations()->{'template_cache'};
+  my $container = "$cache_dir$abs_root";
+  mkpath($container);
+  my $target = "$cache_dir/$dir_name";
+
+  # Check if the directory exists, because if there are no extensions,
+  # there won't be an "data/template/extensions" directory to link to.
+  if (-d $target) {
+
+    # We use abs2rel so that the symlink will look like
+    # "../../../../template" which works, while just
+    # "data/template/template/" doesn't work.
+    my $relative_target = File::Spec->abs2rel($target, $container);
+
+    my $link_name = "$container/$dir_name";
+    symlink($relative_target, $link_name)
+      or warn "Could not make $link_name a symlink to $relative_target: $!";
+  }
 }
 
 1;
diff --git a/Bugzilla/Template/Context.pm b/Bugzilla/Template/Context.pm
index 470e6a9ee..01c8c5981 100644
--- a/Bugzilla/Template/Context.pm
+++ b/Bugzilla/Template/Context.pm
@@ -18,23 +18,24 @@ use Bugzilla::Hook;
 use Scalar::Util qw(blessed);
 
 sub process {
-    my $self = shift;
-    # We don't want to run the template_before_process hook for
-    # template hooks (but we do want it to run if a hook calls
-    # PROCESS inside itself). The problem is that the {component}->{name} of
-    # hooks is unreliable--sometimes it starts with ./ and it's the
-    # full path to the hook template, and sometimes it's just the relative
-    # name (like hook/global/field-descs-end.none.tmpl). Also, calling
-    # template_before_process for hook templates doesn't seem too useful,
-    # because that's already part of the extension and they should be able
-    # to modify their hook if they want (or just modify the variables in the
-    # calling template).
-    if (not delete $self->{bz_in_hook}) {
-        $self->{bz_in_process} = 1;
-    }
-    my $result = $self->SUPER::process(@_);
-    delete $self->{bz_in_process};
-    return $result;
+  my $self = shift;
+
+  # We don't want to run the template_before_process hook for
+  # template hooks (but we do want it to run if a hook calls
+  # PROCESS inside itself). The problem is that the {component}->{name} of
+  # hooks is unreliable--sometimes it starts with ./ and it's the
+  # full path to the hook template, and sometimes it's just the relative
+  # name (like hook/global/field-descs-end.none.tmpl). Also, calling
+  # template_before_process for hook templates doesn't seem too useful,
+  # because that's already part of the extension and they should be able
+  # to modify their hook if they want (or just modify the variables in the
+  # calling template).
+  if (not delete $self->{bz_in_hook}) {
+    $self->{bz_in_process} = 1;
+  }
+  my $result = $self->SUPER::process(@_);
+  delete $self->{bz_in_process};
+  return $result;
 }
 
 # This method is called by Template-Toolkit exactly once per template or
@@ -46,58 +47,59 @@ sub process {
 # in the PROCESS or INCLUDE directive haven't been set, and if we're
 # in an INCLUDE, the stash is not yet localized during process().
 sub stash {
-    my $self = shift;
-    my $stash = $self->SUPER::stash(@_);
-
-    my $name = $stash->{component}->{name};
-    my $pre_process = $self->config->{PRE_PROCESS};
-
-    # Checking bz_in_process tells us that we were indeed called as part of a
-    # Context::process, and not at some other point. 
-    #
-    # Checking $name makes sure that we're processing a file, and not just a
-    # block, by checking that the name has a period in it. We don't allow
-    # blocks because their names are too unreliable--an extension could have
-    # a block with the same name, or multiple files could have a same-named
-    # block, and then your extension would malfunction.
-    #
-    # We also make sure that we don't run, ever, during the PRE_PROCESS
-    # templates, because if somebody calls Throw*Error globally inside of
-    # template_before_process, that causes an infinite recursion into
-    # the PRE_PROCESS templates (because Bugzilla, while inside 
-    # global/intialize.none.tmpl, loads the template again to create the
-    # template object for Throw*Error).
-    #
-    # Checking Bugzilla::Hook::in prevents infinite recursion on this hook.
-    if ($self->{bz_in_process} and $name =~ /\./
-        and !grep($_ eq $name, @$pre_process)
-        and !Bugzilla::Hook::in('template_before_process'))
-    {
-        Bugzilla::Hook::process("template_before_process",
-                                { vars => $stash, context => $self,
-                                  file => $name });
-    }
-
-    # This prevents other calls to stash() that might somehow happen
-    # later in the file from also triggering the hook.
-    delete $self->{bz_in_process};
-
-    return $stash;
+  my $self  = shift;
+  my $stash = $self->SUPER::stash(@_);
+
+  my $name        = $stash->{component}->{name};
+  my $pre_process = $self->config->{PRE_PROCESS};
+
+  # Checking bz_in_process tells us that we were indeed called as part of a
+  # Context::process, and not at some other point.
+  #
+  # Checking $name makes sure that we're processing a file, and not just a
+  # block, by checking that the name has a period in it. We don't allow
+  # blocks because their names are too unreliable--an extension could have
+  # a block with the same name, or multiple files could have a same-named
+  # block, and then your extension would malfunction.
+  #
+  # We also make sure that we don't run, ever, during the PRE_PROCESS
+  # templates, because if somebody calls Throw*Error globally inside of
+  # template_before_process, that causes an infinite recursion into
+  # the PRE_PROCESS templates (because Bugzilla, while inside
+  # global/intialize.none.tmpl, loads the template again to create the
+  # template object for Throw*Error).
+  #
+  # Checking Bugzilla::Hook::in prevents infinite recursion on this hook.
+  if (  $self->{bz_in_process}
+    and $name =~ /\./
+    and !grep($_ eq $name, @$pre_process)
+    and !Bugzilla::Hook::in('template_before_process'))
+  {
+    Bugzilla::Hook::process("template_before_process",
+      {vars => $stash, context => $self, file => $name});
+  }
+
+  # This prevents other calls to stash() that might somehow happen
+  # later in the file from also triggering the hook.
+  delete $self->{bz_in_process};
+
+  return $stash;
 }
 
 sub filter {
-    my ($self, $name, $args) = @_;
-    # If we pass an alias for the filter name, the filter code is cached
-    # instead of looking for it at each call.
-    # If the filter has arguments, then we can't cache it.
-    $self->SUPER::filter($name, $args, $args ? undef : $name);
+  my ($self, $name, $args) = @_;
+
+  # If we pass an alias for the filter name, the filter code is cached
+  # instead of looking for it at each call.
+  # If the filter has arguments, then we can't cache it.
+  $self->SUPER::filter($name, $args, $args ? undef : $name);
 }
 
 # We need a DESTROY sub for the same reason that Bugzilla::CGI does.
 sub DESTROY {
-    my $self = shift;
-    $self->SUPER::DESTROY(@_);
-};
+  my $self = shift;
+  $self->SUPER::DESTROY(@_);
+}
 
 1;
 
diff --git a/Bugzilla/Template/Plugin/Bugzilla.pm b/Bugzilla/Template/Plugin/Bugzilla.pm
index 806dd903b..0734fb942 100644
--- a/Bugzilla/Template/Plugin/Bugzilla.pm
+++ b/Bugzilla/Template/Plugin/Bugzilla.pm
@@ -16,20 +16,20 @@ use parent qw(Template::Plugin);
 use Bugzilla;
 
 sub new {
-    my ($class, $context) = @_;
+  my ($class, $context) = @_;
 
-    return bless {}, $class;
+  return bless {}, $class;
 }
 
 sub AUTOLOAD {
-    my $class = shift;
-    our $AUTOLOAD;
+  my $class = shift;
+  our $AUTOLOAD;
 
-    $AUTOLOAD =~ s/^.*:://;
+  $AUTOLOAD =~ s/^.*:://;
 
-    return if $AUTOLOAD eq 'DESTROY';
+  return if $AUTOLOAD eq 'DESTROY';
 
-    return Bugzilla->$AUTOLOAD(@_);
+  return Bugzilla->$AUTOLOAD(@_);
 }
 
 1;
diff --git a/Bugzilla/Template/Plugin/Hook.pm b/Bugzilla/Template/Plugin/Hook.pm
index 669c77614..c57db4223 100644
--- a/Bugzilla/Template/Plugin/Hook.pm
+++ b/Bugzilla/Template/Plugin/Hook.pm
@@ -14,81 +14,80 @@ use warnings;
 use parent qw(Template::Plugin);
 
 use Bugzilla::Constants;
-use Bugzilla::Install::Util qw(template_include_path); 
+use Bugzilla::Install::Util qw(template_include_path);
 use Bugzilla::Util;
 use Bugzilla::Error;
 
 use File::Spec;
 
 sub new {
-    my ($class, $context) = @_;
-    return bless { _CONTEXT => $context }, $class;
+  my ($class, $context) = @_;
+  return bless {_CONTEXT => $context}, $class;
 }
 
 sub _context { return $_[0]->{_CONTEXT} }
 
 sub process {
-    my ($self, $hook_name, $template) = @_;
-    my $context = $self->_context();
-    $template ||= $context->stash->{component}->{name};
-
-    # sanity check:
-    if (!$template =~ /[\w\.\/\-_\\]+/) {
-        ThrowCodeError('template_invalid', { name => $template });
-    }
-
-    my (undef, $path, $filename) = File::Spec->splitpath($template);
-    $path ||= '';
-    $filename =~ m/(.+)\.(.+)\.tmpl$/;
-    my $template_name = $1;
-    my $type = $2;
-
-    # Hooks are named like this:
-    my $extension_template = "$path$template_name-$hook_name.$type.tmpl";
-
-    # Get the hooks out of the cache if they exist. Otherwise, read them
-    # from the disk.
-    my $cache = Bugzilla->request_cache->{template_plugin_hook_cache} ||= {};
-    my $lang = $context->{bz_language} || '';
-    $cache->{"${lang}__$extension_template"} 
-        ||= $self->_get_hooks($extension_template);
-
-    # process() accepts an arrayref of templates, so we just pass the whole
-    # arrayref.
-    $context->{bz_in_hook} = 1; # See Bugzilla::Template::Context
-    return $context->process($cache->{"${lang}__$extension_template"});
+  my ($self, $hook_name, $template) = @_;
+  my $context = $self->_context();
+  $template ||= $context->stash->{component}->{name};
+
+  # sanity check:
+  if (!$template =~ /[\w\.\/\-_\\]+/) {
+    ThrowCodeError('template_invalid', {name => $template});
+  }
+
+  my (undef, $path, $filename) = File::Spec->splitpath($template);
+  $path ||= '';
+  $filename =~ m/(.+)\.(.+)\.tmpl$/;
+  my $template_name = $1;
+  my $type          = $2;
+
+  # Hooks are named like this:
+  my $extension_template = "$path$template_name-$hook_name.$type.tmpl";
+
+  # Get the hooks out of the cache if they exist. Otherwise, read them
+  # from the disk.
+  my $cache = Bugzilla->request_cache->{template_plugin_hook_cache} ||= {};
+  my $lang = $context->{bz_language} || '';
+  $cache->{"${lang}__$extension_template"}
+    ||= $self->_get_hooks($extension_template);
+
+  # process() accepts an arrayref of templates, so we just pass the whole
+  # arrayref.
+  $context->{bz_in_hook} = 1;    # See Bugzilla::Template::Context
+  return $context->process($cache->{"${lang}__$extension_template"});
 }
 
 sub _get_hooks {
-    my ($self, $extension_template) = @_;
-
-    my $template_sets = $self->_template_hook_include_path();
-    my @hooks;
-    foreach my $dir_set (@$template_sets) {
-        foreach my $template_dir (@$dir_set) {
-            my $file = "$template_dir/hook/$extension_template";
-            if (-e $file) {
-                my $template = $self->_context->template($file);
-                push(@hooks, $template);
-                # Don't run the hook for more than one language.
-                last;
-            }
-        }
+  my ($self, $extension_template) = @_;
+
+  my $template_sets = $self->_template_hook_include_path();
+  my @hooks;
+  foreach my $dir_set (@$template_sets) {
+    foreach my $template_dir (@$dir_set) {
+      my $file = "$template_dir/hook/$extension_template";
+      if (-e $file) {
+        my $template = $self->_context->template($file);
+        push(@hooks, $template);
+
+        # Don't run the hook for more than one language.
+        last;
+      }
     }
+  }
 
-    return \@hooks;
+  return \@hooks;
 }
 
 sub _template_hook_include_path {
-    my $self = shift;
-    my $cache = Bugzilla->request_cache;
-    my $language = $self->_context->{bz_language} || '';
-    my $cache_key = "template_plugin_hook_include_path_$language";
-    $cache->{$cache_key} ||= template_include_path({
-        language => $language,
-        hook     => 1,
-    });
-    return $cache->{$cache_key};
+  my $self      = shift;
+  my $cache     = Bugzilla->request_cache;
+  my $language  = $self->_context->{bz_language} || '';
+  my $cache_key = "template_plugin_hook_include_path_$language";
+  $cache->{$cache_key}
+    ||= template_include_path({language => $language, hook => 1,});
+  return $cache->{$cache_key};
 }
 
 1;
diff --git a/Bugzilla/Token.pm b/Bugzilla/Token.pm
index 28122e818..62106c5e5 100644
--- a/Bugzilla/Token.pm
+++ b/Bugzilla/Token.pm
@@ -25,8 +25,8 @@ use Digest::SHA qw(hmac_sha256_base64);
 use parent qw(Exporter);
 
 @Bugzilla::Token::EXPORT = qw(issue_api_token issue_session_token
-                              check_token_data delete_token
-                              issue_hash_token check_hash_token);
+  check_token_data delete_token
+  issue_hash_token check_hash_token);
 
 use constant SEND_NOW => 1;
 
@@ -36,394 +36,420 @@ use constant SEND_NOW => 1;
 
 # Create a token used for internal API authentication
 sub issue_api_token {
-    # Generates a random token, adds it to the tokens table if one does not
-    # already exist, and returns the token to the caller.
-    my $dbh  = Bugzilla->dbh;
-    my $user = Bugzilla->user;
-    my ($token) = $dbh->selectrow_array("
+
+  # Generates a random token, adds it to the tokens table if one does not
+  # already exist, and returns the token to the caller.
+  my $dbh     = Bugzilla->dbh;
+  my $user    = Bugzilla->user;
+  my ($token) = $dbh->selectrow_array("
         SELECT token FROM tokens
          WHERE userid = ? AND tokentype = 'api_token'
-               AND (" . $dbh->sql_date_math('issuedate', '+', (MAX_TOKEN_AGE * 24 - 12), 'HOUR') . ") > NOW()",
-        undef, $user->id);
-    return $token // _create_token($user->id, 'api_token', '');
+               AND ("
+      . $dbh->sql_date_math('issuedate', '+', (MAX_TOKEN_AGE * 24 - 12), 'HOUR')
+      . ") > NOW()", undef, $user->id);
+  return $token // _create_token($user->id, 'api_token', '');
 }
 
 # Creates and sends a token to create a new user account.
 # It assumes that the login has the correct format and is not already in use.
 sub issue_new_user_account_token {
-    my $login_name = shift;
-    my $dbh = Bugzilla->dbh;
-    my $template = Bugzilla->template;
-    my $vars = {};
-
-    # Is there already a pending request for this login name? If yes, do not throw
-    # an error because the user may have lost their email with the token inside.
-    # But to prevent using this way to mailbomb an email address, make sure
-    # the last request is old enough before sending a new email (default: 10 minutes).
-
-    my $pending_requests = $dbh->selectrow_array(
-        'SELECT COUNT(*)
+  my $login_name = shift;
+  my $dbh        = Bugzilla->dbh;
+  my $template   = Bugzilla->template;
+  my $vars       = {};
+
+# Is there already a pending request for this login name? If yes, do not throw
+# an error because the user may have lost their email with the token inside.
+# But to prevent using this way to mailbomb an email address, make sure
+# the last request is old enough before sending a new email (default: 10 minutes).
+
+  my $pending_requests = $dbh->selectrow_array(
+    'SELECT COUNT(*)
            FROM tokens
           WHERE tokentype = ?
                 AND ' . $dbh->sql_istrcmp('eventdata', '?') . '
                 AND issuedate > '
-                    . $dbh->sql_date_math('NOW()', '-', ACCOUNT_CHANGE_INTERVAL, 'MINUTE'),
-        undef, ('account', $login_name));
+      . $dbh->sql_date_math('NOW()', '-', ACCOUNT_CHANGE_INTERVAL, 'MINUTE'),
+    undef, ('account', $login_name)
+  );
 
-    ThrowUserError('too_soon_for_new_token', {'type' => 'account'}) if $pending_requests;
+  ThrowUserError('too_soon_for_new_token', {'type' => 'account'})
+    if $pending_requests;
 
-    my ($token, $token_ts) = _create_token(undef, 'account', $login_name);
+  my ($token, $token_ts) = _create_token(undef, 'account', $login_name);
 
-    $vars->{'email'} = $login_name . Bugzilla->params->{'emailsuffix'};
-    $vars->{'expiration_ts'} = ctime($token_ts + MAX_TOKEN_AGE * 86400);
-    $vars->{'token'} = $token;
+  $vars->{'email'}         = $login_name . Bugzilla->params->{'emailsuffix'};
+  $vars->{'expiration_ts'} = ctime($token_ts + MAX_TOKEN_AGE * 86400);
+  $vars->{'token'}         = $token;
 
-    my $message;
-    $template->process('account/email/request-new.txt.tmpl', $vars, \$message)
-      || ThrowTemplateError($template->error());
+  my $message;
+  $template->process('account/email/request-new.txt.tmpl', $vars, \$message)
+    || ThrowTemplateError($template->error());
 
-    # In 99% of cases, the user getting the confirmation email is the same one
-    # who made the request, and so it is reasonable to send the email in the same
-    # language used to view the "Create a New Account" page (we cannot use their
-    # user prefs as the user has no account yet!).
-    MessageToMTA($message, SEND_NOW);
+  # In 99% of cases, the user getting the confirmation email is the same one
+  # who made the request, and so it is reasonable to send the email in the same
+  # language used to view the "Create a New Account" page (we cannot use their
+  # user prefs as the user has no account yet!).
+  MessageToMTA($message, SEND_NOW);
 }
 
 sub IssueEmailChangeToken {
-    my $new_email = shift;
-    my $user = Bugzilla->user;
+  my $new_email = shift;
+  my $user      = Bugzilla->user;
 
-    my ($token, $token_ts) = _create_token($user->id, 'emailold', $user->login . ":$new_email");
-    my $newtoken = _create_token($user->id, 'emailnew', $user->login . ":$new_email");
+  my ($token, $token_ts)
+    = _create_token($user->id, 'emailold', $user->login . ":$new_email");
+  my $newtoken
+    = _create_token($user->id, 'emailnew', $user->login . ":$new_email");
 
-    # Mail the user the token along with instructions for using it.
+  # Mail the user the token along with instructions for using it.
 
-    my $template = Bugzilla->template_inner($user->setting('lang'));
-    my $vars = {};
+  my $template = Bugzilla->template_inner($user->setting('lang'));
+  my $vars     = {};
 
-    $vars->{'newemailaddress'} = $new_email . Bugzilla->params->{'emailsuffix'};
-    $vars->{'expiration_ts'} = ctime($token_ts + MAX_TOKEN_AGE * 86400);
+  $vars->{'newemailaddress'} = $new_email . Bugzilla->params->{'emailsuffix'};
+  $vars->{'expiration_ts'}   = ctime($token_ts + MAX_TOKEN_AGE * 86400);
 
-    # First send an email to the new address. If this one doesn't exist,
-    # then the whole process must stop immediately. This means the email must
-    # be sent immediately and must not be stored in the queue.
-    $vars->{'token'} = $newtoken;
+  # First send an email to the new address. If this one doesn't exist,
+  # then the whole process must stop immediately. This means the email must
+  # be sent immediately and must not be stored in the queue.
+  $vars->{'token'} = $newtoken;
 
-    my $message;
-    $template->process('account/email/change-new.txt.tmpl', $vars, \$message)
-      || ThrowTemplateError($template->error());
+  my $message;
+  $template->process('account/email/change-new.txt.tmpl', $vars, \$message)
+    || ThrowTemplateError($template->error());
 
-    MessageToMTA($message, SEND_NOW);
+  MessageToMTA($message, SEND_NOW);
 
-    # If we come here, then the new address exists. We now email the current
-    # address, but we don't want to stop the process if it no longer exists,
-    # to give a chance to the user to confirm the email address change.
-    $vars->{'token'} = $token;
+  # If we come here, then the new address exists. We now email the current
+  # address, but we don't want to stop the process if it no longer exists,
+  # to give a chance to the user to confirm the email address change.
+  $vars->{'token'} = $token;
 
-    $message = '';
-    $template->process('account/email/change-old.txt.tmpl', $vars, \$message)
-      || ThrowTemplateError($template->error());
+  $message = '';
+  $template->process('account/email/change-old.txt.tmpl', $vars, \$message)
+    || ThrowTemplateError($template->error());
 
-    eval { MessageToMTA($message, SEND_NOW); };
+  eval { MessageToMTA($message, SEND_NOW); };
 
-    # Give the user a chance to cancel the process even if he never got
-    # the email above. The token is required.
-    return $token;
+  # Give the user a chance to cancel the process even if he never got
+  # the email above. The token is required.
+  return $token;
 }
 
 # Generates a random token, adds it to the tokens table, and sends it
 # to the user with instructions for using it to change their password.
 sub IssuePasswordToken {
-    my $user = shift;
-    my $dbh = Bugzilla->dbh;
+  my $user = shift;
+  my $dbh  = Bugzilla->dbh;
 
-    my $too_soon = $dbh->selectrow_array(
-        'SELECT 1 FROM tokens
+  my $too_soon = $dbh->selectrow_array(
+    'SELECT 1 FROM tokens
           WHERE userid = ? AND tokentype = ?
-                AND issuedate > ' 
-                    . $dbh->sql_date_math('NOW()', '-', ACCOUNT_CHANGE_INTERVAL, 'MINUTE'),
-        undef, ($user->id, 'password'));
+                AND issuedate > '
+      . $dbh->sql_date_math('NOW()', '-', ACCOUNT_CHANGE_INTERVAL, 'MINUTE'),
+    undef, ($user->id, 'password')
+  );
 
-    ThrowUserError('too_soon_for_new_token', {'type' => 'password'}) if $too_soon;
+  ThrowUserError('too_soon_for_new_token', {'type' => 'password'}) if $too_soon;
 
-    my $ip_addr = remote_ip();
-    my ($token, $token_ts) = _create_token($user->id, 'password', $ip_addr);
+  my $ip_addr = remote_ip();
+  my ($token, $token_ts) = _create_token($user->id, 'password', $ip_addr);
 
-    # Mail the user the token along with instructions for using it.
-    my $template = Bugzilla->template_inner($user->setting('lang'));
-    my $vars = {};
+  # Mail the user the token along with instructions for using it.
+  my $template = Bugzilla->template_inner($user->setting('lang'));
+  my $vars     = {};
 
-    $vars->{'token'} = $token;
-    $vars->{'ip_addr'} = $ip_addr;
-    $vars->{'emailaddress'} = $user->email;
-    $vars->{'expiration_ts'} = ctime($token_ts + MAX_TOKEN_AGE * 86400);
-    # The user is not logged in (else they wouldn't request a new password).
-    # So we have to pass this information to the template.
-    $vars->{'timezone'} = $user->timezone;
-
-    my $message = "";
-    $template->process("account/password/forgotten-password.txt.tmpl", 
-                                                               $vars, \$message)
-      || ThrowTemplateError($template->error());
+  $vars->{'token'}         = $token;
+  $vars->{'ip_addr'}       = $ip_addr;
+  $vars->{'emailaddress'}  = $user->email;
+  $vars->{'expiration_ts'} = ctime($token_ts + MAX_TOKEN_AGE * 86400);
+
+  # The user is not logged in (else they wouldn't request a new password).
+  # So we have to pass this information to the template.
+  $vars->{'timezone'} = $user->timezone;
 
-    MessageToMTA($message);
+  my $message = "";
+  $template->process("account/password/forgotten-password.txt.tmpl",
+    $vars, \$message)
+    || ThrowTemplateError($template->error());
+
+  MessageToMTA($message);
 }
 
 sub issue_session_token {
-    # Generates a random token, adds it to the tokens table, and returns
-    # the token to the caller.
 
-    my $data = shift;
-    return _create_token(Bugzilla->user->id, 'session', $data);
+  # Generates a random token, adds it to the tokens table, and returns
+  # the token to the caller.
+
+  my $data = shift;
+  return _create_token(Bugzilla->user->id, 'session', $data);
 }
 
 sub issue_hash_token {
-    my ($data, $time) = @_;
-    $data ||= [];
-    $time ||= time();
-
-    # For the user ID, use the actual ID if the user is logged in.
-    # Otherwise, use the remote IP, in case this is for something
-    # such as creating an account or logging in.
-    my $user_id = Bugzilla->user->id || remote_ip();
-
-    # The concatenated string is of the form
-    # token creation time + user ID (either ID or remote IP) + data
-    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;
-
-    # Prepend the token creation time, unencrypted, so that the token
-    # lifetime can be validated.
-    return $time . '-' . $token;
+  my ($data, $time) = @_;
+  $data ||= [];
+  $time ||= time();
+
+  # For the user ID, use the actual ID if the user is logged in.
+  # Otherwise, use the remote IP, in case this is for something
+  # such as creating an account or logging in.
+  my $user_id = Bugzilla->user->id || remote_ip();
+
+  # The concatenated string is of the form
+  # token creation time + user ID (either ID or remote IP) + data
+  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;
+
+  # Prepend the token creation time, unencrypted, so that the token
+  # lifetime can be validated.
+  return $time . '-' . $token;
 }
 
 sub check_hash_token {
-    my ($token, $data) = @_;
-    $data ||= [];
-    my ($time, $expected_token);
-
-    if ($token) {
-        ($time, undef) = split(/-/, $token);
-        # Regenerate the token based on the information we have.
-        $expected_token = issue_hash_token($data, $time);
-    }
+  my ($token, $data) = @_;
+  $data ||= [];
+  my ($time, $expected_token);
 
-    if (!$token
-        || $expected_token ne $token
-        || time() - $time > MAX_TOKEN_AGE * 86400)
-    {
-        my $template = Bugzilla->template;
-        my $vars = {};
-        $vars->{'script_name'} = basename($0);
-        $vars->{'token'} = issue_hash_token($data);
-        $vars->{'reason'} = (!$token) ?                   'missing_token' :
-                            ($expected_token ne $token) ? 'invalid_token' :
-                                                          'expired_token';
-        print Bugzilla->cgi->header();
-        $template->process('global/confirm-action.html.tmpl', $vars)
-          || ThrowTemplateError($template->error());
-        exit;
-    }
+  if ($token) {
+    ($time, undef) = split(/-/, $token);
+
+    # Regenerate the token based on the information we have.
+    $expected_token = issue_hash_token($data, $time);
+  }
+
+  if (!$token
+    || $expected_token ne $token
+    || time() - $time > MAX_TOKEN_AGE * 86400)
+  {
+    my $template = Bugzilla->template;
+    my $vars     = {};
+    $vars->{'script_name'} = basename($0);
+    $vars->{'token'}       = issue_hash_token($data);
+    $vars->{'reason'}
+      = (!$token) ? 'missing_token'
+      : ($expected_token ne $token) ? 'invalid_token'
+      :                               'expired_token';
+    print Bugzilla->cgi->header();
+    $template->process('global/confirm-action.html.tmpl', $vars)
+      || ThrowTemplateError($template->error());
+    exit;
+  }
 
-    # If we come here, then the token is valid and not too old.
-    return 1;
+  # If we come here, then the token is valid and not too old.
+  return 1;
 }
 
 sub CleanTokenTable {
-    my $dbh = Bugzilla->dbh;
-    $dbh->do('DELETE FROM tokens
-              WHERE ' . $dbh->sql_to_days('NOW()') . ' - ' .
-                        $dbh->sql_to_days('issuedate') . ' >= ?',
-              undef, MAX_TOKEN_AGE);
+  my $dbh = Bugzilla->dbh;
+  $dbh->do(
+    'DELETE FROM tokens
+              WHERE '
+      . $dbh->sql_to_days('NOW()') . ' - '
+      . $dbh->sql_to_days('issuedate')
+      . ' >= ?', undef, MAX_TOKEN_AGE
+  );
 }
 
 sub GenerateUniqueToken {
-    # Generates a unique random token.  Uses generate_random_password 
-    # for the tokens themselves and checks uniqueness by searching for
-    # the token in the "tokens" table.  Gives up if it can't come up
-    # with a token after about one hundred tries.
-    my ($table, $column) = @_;
-
-    my $token;
-    my $duplicate = 1;
-    my $tries = 0;
-    $table ||= "tokens";
-    $column ||= "token";
-
-    my $dbh = Bugzilla->dbh;
-    my $sth = $dbh->prepare("SELECT 1 FROM $table WHERE $column = ?");
-
-    while ($duplicate) {
-        ++$tries;
-        if ($tries > 100) {
-            ThrowCodeError("token_generation_error");
-        }
-        $token = generate_random_password();
-        $sth->execute($token);
-        $duplicate = $sth->fetchrow_array;
+
+  # Generates a unique random token.  Uses generate_random_password
+  # for the tokens themselves and checks uniqueness by searching for
+  # the token in the "tokens" table.  Gives up if it can't come up
+  # with a token after about one hundred tries.
+  my ($table, $column) = @_;
+
+  my $token;
+  my $duplicate = 1;
+  my $tries     = 0;
+  $table  ||= "tokens";
+  $column ||= "token";
+
+  my $dbh = Bugzilla->dbh;
+  my $sth = $dbh->prepare("SELECT 1 FROM $table WHERE $column = ?");
+
+  while ($duplicate) {
+    ++$tries;
+    if ($tries > 100) {
+      ThrowCodeError("token_generation_error");
     }
-    return $token;
+    $token = generate_random_password();
+    $sth->execute($token);
+    $duplicate = $sth->fetchrow_array;
+  }
+  return $token;
 }
 
 # Cancels a previously issued token and notifies the user.
 # This should only happen when the user accidentally makes a token request
 # or when a malicious hacker makes a token request on behalf of a user.
 sub Cancel {
-    my ($token, $cancelaction, $vars) = @_;
-    my $dbh = Bugzilla->dbh;
-    $vars ||= {};
-
-    # Get information about the token being canceled.
-    trick_taint($token);
-    my ($db_token, $issuedate, $tokentype, $eventdata, $userid) =
-        $dbh->selectrow_array('SELECT token, ' . $dbh->sql_date_format('issuedate') . ',
+  my ($token, $cancelaction, $vars) = @_;
+  my $dbh = Bugzilla->dbh;
+  $vars ||= {};
+
+  # Get information about the token being canceled.
+  trick_taint($token);
+  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);
-
-    $vars->{'emailaddress'} = $userid ? $user->email : $eventdata;
-    $vars->{'remoteaddress'} = remote_ip();
-    $vars->{'token'} = $token;
-    $vars->{'tokentype'} = $tokentype;
-    $vars->{'issuedate'} = $issuedate;
-    # The user is probably not logged in.
-    # So we have to pass this information to the template.
-    $vars->{'timezone'} = $user->timezone;
-    $vars->{'eventdata'} = $eventdata;
-    $vars->{'cancelaction'} = $cancelaction;
-
-    # Notify the user via email about the cancellation.
-    my $template = Bugzilla->template_inner($user->setting('lang'));
-
-    my $message;
-    $template->process("account/cancel-token.txt.tmpl", $vars, \$message)
-      || ThrowTemplateError($template->error());
+                                WHERE token = ?', undef, $token
+    );
 
-    MessageToMTA($message);
+  # 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");
 
-    # Delete the token from the database.
-    delete_token($token);
+  # 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);
+
+  $vars->{'emailaddress'}  = $userid ? $user->email : $eventdata;
+  $vars->{'remoteaddress'} = remote_ip();
+  $vars->{'token'}         = $token;
+  $vars->{'tokentype'}     = $tokentype;
+  $vars->{'issuedate'}     = $issuedate;
+
+  # The user is probably not logged in.
+  # So we have to pass this information to the template.
+  $vars->{'timezone'}     = $user->timezone;
+  $vars->{'eventdata'}    = $eventdata;
+  $vars->{'cancelaction'} = $cancelaction;
+
+  # Notify the user via email about the cancellation.
+  my $template = Bugzilla->template_inner($user->setting('lang'));
+
+  my $message;
+  $template->process("account/cancel-token.txt.tmpl", $vars, \$message)
+    || ThrowTemplateError($template->error());
+
+  MessageToMTA($message);
+
+  # Delete the token from the database.
+  delete_token($token);
 }
 
 sub DeletePasswordTokens {
-    my ($userid, $reason) = @_;
-    my $dbh = Bugzilla->dbh;
+  my ($userid, $reason) = @_;
+  my $dbh = Bugzilla->dbh;
 
-    detaint_natural($userid);
-    my $tokens = $dbh->selectcol_arrayref('SELECT token FROM tokens
+  detaint_natural($userid);
+  my $tokens = $dbh->selectcol_arrayref(
+    'SELECT token FROM tokens
                                            WHERE userid = ? AND tokentype = ?',
-                                           undef, ($userid, 'password'));
+    undef, ($userid, 'password')
+  );
 
-    foreach my $token (@$tokens) {
-        Bugzilla::Token::Cancel($token, $reason);
-    }
+  foreach my $token (@$tokens) {
+    Bugzilla::Token::Cancel($token, $reason);
+  }
 }
 
-# Returns an email change token if the user has one. 
+# Returns an email change token if the user has one.
 sub HasEmailChangeToken {
-    my $userid = shift;
-    my $dbh = Bugzilla->dbh;
+  my $userid = shift;
+  my $dbh    = Bugzilla->dbh;
 
-    my $token = $dbh->selectrow_array('SELECT token FROM tokens
+  my $token = $dbh->selectrow_array(
+    'SELECT token FROM tokens
                                        WHERE userid = ?
-                                       AND (tokentype = ? OR tokentype = ?) ' .
-                                       $dbh->sql_limit(1),
-                                       undef, ($userid, 'emailnew', 'emailold'));
-    return $token;
+                                       AND (tokentype = ? OR tokentype = ?) '
+      . $dbh->sql_limit(1), undef, ($userid, 'emailnew', 'emailold')
+  );
+  return $token;
 }
 
 # Returns the userid, issuedate and eventdata for the specified token
 sub GetTokenData {
-    my ($token) = @_;
-    my $dbh = Bugzilla->dbh;
+  my ($token) = @_;
+  my $dbh = Bugzilla->dbh;
 
-    return unless defined $token;
-    $token = clean_text($token);
-    trick_taint($token);
+  return unless defined $token;
+  $token = clean_text($token);
+  trick_taint($token);
 
-    my @token_data = $dbh->selectrow_array(
-        "SELECT token, userid, " . $dbh->sql_date_format('issuedate') . ", eventdata, tokentype
+  my @token_data = $dbh->selectrow_array(
+        "SELECT token, userid, "
+      . $dbh->sql_date_format('issuedate')
+      . ", eventdata, tokentype
          FROM   tokens
-         WHERE  token = ?", undef, $token);
+         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);
+  # 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;
+  return @token_data;
 }
 
 # Deletes specified token
 sub delete_token {
-    my ($token) = @_;
-    my $dbh = Bugzilla->dbh;
+  my ($token) = @_;
+  my $dbh = Bugzilla->dbh;
 
-    return unless defined $token;
-    trick_taint($token);
+  return unless defined $token;
+  trick_taint($token);
 
-    $dbh->do("DELETE FROM tokens WHERE token = ?", undef, $token);
+  $dbh->do("DELETE FROM tokens WHERE token = ?", undef, $token);
 }
 
 # Given a token, makes sure it comes from the currently logged in user
 # and match the expected event. Returns 1 on success, else displays a warning.
 sub check_token_data {
-    my ($token, $expected_action, $alternate_script) = @_;
-    my $user = Bugzilla->user;
-    my $template = Bugzilla->template;
-    my $cgi = Bugzilla->cgi;
-
-    my ($creator_id, $date, $token_action) = GetTokenData($token);
-    unless ($creator_id
-            && $creator_id == $user->id
-            && $token_action eq $expected_action)
-    {
-        # Something is going wrong. Ask confirmation before processing.
-        # It is possible that someone tried to trick an administrator.
-        # In this case, we want to know their name!
-        require Bugzilla::User;
-
-        my $vars = {};
-        $vars->{'abuser'} = Bugzilla::User->new($creator_id)->identity;
-        $vars->{'token_action'} = $token_action;
-        $vars->{'expected_action'} = $expected_action;
-        $vars->{'script_name'} = basename($0);
-        $vars->{'alternate_script'} = $alternate_script || basename($0);
-
-        # Now is a good time to remove old tokens from the DB.
-        CleanTokenTable();
-
-        # If no token was found, create a valid token for the given action.
-        unless ($creator_id) {
-            $token = issue_session_token($expected_action);
-            $cgi->param('token', $token);
-        }
-
-        print $cgi->header();
-
-        $template->process('admin/confirm-action.html.tmpl', $vars)
-          || ThrowTemplateError($template->error());
-        exit;
+  my ($token, $expected_action, $alternate_script) = @_;
+  my $user     = Bugzilla->user;
+  my $template = Bugzilla->template;
+  my $cgi      = Bugzilla->cgi;
+
+  my ($creator_id, $date, $token_action) = GetTokenData($token);
+  unless ($creator_id
+    && $creator_id == $user->id
+    && $token_action eq $expected_action)
+  {
+    # Something is going wrong. Ask confirmation before processing.
+    # It is possible that someone tried to trick an administrator.
+    # In this case, we want to know their name!
+    require Bugzilla::User;
+
+    my $vars = {};
+    $vars->{'abuser'}           = Bugzilla::User->new($creator_id)->identity;
+    $vars->{'token_action'}     = $token_action;
+    $vars->{'expected_action'}  = $expected_action;
+    $vars->{'script_name'}      = basename($0);
+    $vars->{'alternate_script'} = $alternate_script || basename($0);
+
+    # Now is a good time to remove old tokens from the DB.
+    CleanTokenTable();
+
+    # If no token was found, create a valid token for the given action.
+    unless ($creator_id) {
+      $token = issue_session_token($expected_action);
+      $cgi->param('token', $token);
     }
-    return 1;
+
+    print $cgi->header();
+
+    $template->process('admin/confirm-action.html.tmpl', $vars)
+      || ThrowTemplateError($template->error());
+    exit;
+  }
+  return 1;
 }
 
 ################################################################################
@@ -433,34 +459,38 @@ sub check_token_data {
 # Generates a unique token and inserts it into the database
 # Returns the token and the token timestamp
 sub _create_token {
-    my ($userid, $tokentype, $eventdata) = @_;
-    my $dbh = Bugzilla->dbh;
+  my ($userid, $tokentype, $eventdata) = @_;
+  my $dbh = Bugzilla->dbh;
 
-    detaint_natural($userid) if defined $userid;
-    trick_taint($tokentype);
-    trick_taint($eventdata);
+  detaint_natural($userid) if defined $userid;
+  trick_taint($tokentype);
+  trick_taint($eventdata);
 
-    my $is_shadow = Bugzilla->is_shadow_db;
-    $dbh = Bugzilla->switch_to_main_db() if $is_shadow;
+  my $is_shadow = Bugzilla->is_shadow_db;
+  $dbh = Bugzilla->switch_to_main_db() if $is_shadow;
 
-    $dbh->bz_start_transaction();
+  $dbh->bz_start_transaction();
 
-    my $token = GenerateUniqueToken();
+  my $token = GenerateUniqueToken();
 
-    $dbh->do("INSERT INTO tokens (userid, issuedate, token, tokentype, eventdata)
-        VALUES (?, NOW(), ?, ?, ?)", undef, ($userid, $token, $tokentype, $eventdata));
+  $dbh->do(
+    "INSERT INTO tokens (userid, issuedate, token, tokentype, eventdata)
+        VALUES (?, NOW(), ?, ?, ?)", undef,
+    ($userid, $token, $tokentype, $eventdata)
+  );
 
-    $dbh->bz_commit_transaction();
+  $dbh->bz_commit_transaction();
 
-    if (wantarray) {
-        my (undef, $token_ts, undef) = GetTokenData($token);
-        $token_ts = str2time($token_ts);
-        Bugzilla->switch_to_shadow_db() if $is_shadow;
-        return ($token, $token_ts);
-    } else {
-        Bugzilla->switch_to_shadow_db() if $is_shadow;
-        return $token;
-    }
+  if (wantarray) {
+    my (undef, $token_ts, undef) = GetTokenData($token);
+    $token_ts = str2time($token_ts);
+    Bugzilla->switch_to_shadow_db() if $is_shadow;
+    return ($token, $token_ts);
+  }
+  else {
+    Bugzilla->switch_to_shadow_db() if $is_shadow;
+    return $token;
+  }
 }
 
 1;
diff --git a/Bugzilla/Update.pm b/Bugzilla/Update.pm
index 72a7108a8..9f9288162 100644
--- a/Bugzilla/Update.pm
+++ b/Bugzilla/Update.pm
@@ -13,149 +13,159 @@ use warnings;
 
 use Bugzilla::Constants;
 
-use constant TIME_INTERVAL => 86400; # Default is one day, in seconds.
-use constant TIMEOUT       => 5; # Number of seconds before timeout.
+use constant TIME_INTERVAL => 86400;    # Default is one day, in seconds.
+use constant TIMEOUT       => 5;        # Number of seconds before timeout.
 
 # Look for new releases and notify logged in administrators about them.
 sub get_notifications {
-    return if !Bugzilla->feature('updates');
-    return if (Bugzilla->params->{'upgrade_notification'} eq 'disabled');
-
-    my $local_file = bz_locations()->{'datadir'} . '/' . LOCAL_FILE;
-    # Update the local XML file if this one doesn't exist or if
-    # the last modification time (stat[9]) is older than TIME_INTERVAL.
-    if (!-e $local_file || (time() - (stat($local_file))[9] > TIME_INTERVAL)) {
-        unlink $local_file; # Make sure the old copy is away.
-        return { 'error' => 'no_update' } if (-e $local_file);
-
-        my $error = _synchronize_data();
-        # If an error is returned, leave now.
-        return $error if $error;
+  return if !Bugzilla->feature('updates');
+  return if (Bugzilla->params->{'upgrade_notification'} eq 'disabled');
+
+  my $local_file = bz_locations()->{'datadir'} . '/' . LOCAL_FILE;
+
+  # Update the local XML file if this one doesn't exist or if
+  # the last modification time (stat[9]) is older than TIME_INTERVAL.
+  if (!-e $local_file || (time() - (stat($local_file))[9] > TIME_INTERVAL)) {
+    unlink $local_file;    # Make sure the old copy is away.
+    return {'error' => 'no_update'} if (-e $local_file);
+
+    my $error = _synchronize_data();
+
+    # If an error is returned, leave now.
+    return $error if $error;
+  }
+
+  # If we cannot access the local XML file, ignore it.
+  return {'error' => 'no_access'} unless (-r $local_file);
+
+  my $twig = XML::Twig->new();
+  $twig->safe_parsefile($local_file);
+
+  # If the XML file is invalid, return.
+  return {'error' => 'corrupted'} if $@;
+  my $root = $twig->root;
+
+  my @releases;
+  foreach my $branch ($root->children('branch')) {
+    my $release = {
+      'branch_ver' => $branch->{'att'}->{'id'},
+      'latest_ver' => $branch->{'att'}->{'vid'},
+      'status'     => $branch->{'att'}->{'status'},
+      'url'        => $branch->{'att'}->{'url'},
+      'date'       => $branch->{'att'}->{'date'}
+    };
+    push(@releases, $release);
+  }
+
+  # On which branch is the current installation running?
+  my @current_version
+    = (BUGZILLA_VERSION =~ m/^(\d+)\.(\d+)(?:(rc|\.)(\d+))?\+?$/);
+
+  my @release;
+  if (Bugzilla->params->{'upgrade_notification'} eq 'development_snapshot') {
+    @release = grep { $_->{'status'} eq 'development' } @releases;
+
+    # If there is no development snapshot available, then we are in the
+    # process of releasing a release candidate. That's the release we want.
+    unless (scalar(@release)) {
+      @release = grep { $_->{'status'} eq 'release-candidate' } @releases;
     }
-
-    # If we cannot access the local XML file, ignore it.
-    return { 'error' => 'no_access' } unless (-r $local_file);
-
-    my $twig = XML::Twig->new();
-    $twig->safe_parsefile($local_file);
-    # If the XML file is invalid, return.
-    return { 'error' => 'corrupted' } if $@;
-    my $root = $twig->root;
-
-    my @releases;
-    foreach my $branch ($root->children('branch')) {
-        my $release = {
-            'branch_ver' => $branch->{'att'}->{'id'},
-            'latest_ver' => $branch->{'att'}->{'vid'},
-            'status'     => $branch->{'att'}->{'status'},
-            'url'        => $branch->{'att'}->{'url'},
-            'date'       => $branch->{'att'}->{'date'}
-        };
-        push(@releases, $release);
-    }
-
-    # On which branch is the current installation running?
-    my @current_version =
-        (BUGZILLA_VERSION =~ m/^(\d+)\.(\d+)(?:(rc|\.)(\d+))?\+?$/);
-
-    my @release;
-    if (Bugzilla->params->{'upgrade_notification'} eq 'development_snapshot') {
-        @release = grep {$_->{'status'} eq 'development'} @releases;
-        # If there is no development snapshot available, then we are in the
-        # process of releasing a release candidate. That's the release we want.
-        unless (scalar(@release)) {
-            @release = grep {$_->{'status'} eq 'release-candidate'} @releases;
-        }
-    }
-    elsif (Bugzilla->params->{'upgrade_notification'} eq 'latest_stable_release') {
-        @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];
-
-        # 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 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};
-        }
+  }
+  elsif (Bugzilla->params->{'upgrade_notification'} eq 'latest_stable_release') {
+    @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];
+
+    # 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 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};
     }
-    else {
-      # Unknown parameter.
-      return {'error' => 'unknown_parameter'};
-    }
-
-    # Return if no new release is available.
-    return unless scalar(@release);
-
-    # 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+))?\+?$/);
-
-    # 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;
-
-    my $is_newer = _compare_versions(\@current_version, \@new_version);
-    return ($is_newer == 1) ? {'data' => $release[0]} : undef;
+  }
+  else {
+    # Unknown parameter.
+    return {'error' => 'unknown_parameter'};
+  }
+
+  # Return if no new release is available.
+  return unless scalar(@release);
+
+  # 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+))?\+?$/);
+
+  # 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;
+
+  my $is_newer = _compare_versions(\@current_version, \@new_version);
+  return ($is_newer == 1) ? {'data' => $release[0]} : undef;
 }
 
 sub _synchronize_data {
-    my $local_file = bz_locations()->{'datadir'} . '/' . LOCAL_FILE;
-
-    my $ua = LWP::UserAgent->new();
-    $ua->timeout(TIMEOUT);
-    $ua->protocols_allowed(['http', 'https']);
-    # If the URL of the proxy is given, use it, else get this information
-    # from the environment variable.
-    my $proxy_url = Bugzilla->params->{'proxy_url'};
-    if ($proxy_url) {
-        $ua->proxy(['http', 'https'], $proxy_url);
-    }
-    else {
-        $ua->env_proxy;
-    }
-    my $response = eval { $ua->mirror(REMOTE_FILE, $local_file) };
-
-    # $ua->mirror() forces the modification time of the local XML file
-    # to match the modification time of the remote one.
-    # So we have to update it manually to reflect that a newer version
-    # of the file has effectively been requested. This will avoid
-    # any new download for the next TIME_INTERVAL.
-    if (-e $local_file) {
-        # Try to alter its last modification time.
-        my $can_alter = utime(undef, undef, $local_file);
-        # This error should never happen.
-        $can_alter || return { 'error' => 'no_update' };
-    }
-    elsif ($response && $response->is_error) {
-        # We have been unable to download the file.
-        return { 'error' => 'cannot_download', 'reason' => $response->status_line };
-    }
-    else {
-        return { 'error' => 'no_write', 'reason' => $@ };
-    }
-
-    # Everything went well.
-    return 0;
+  my $local_file = bz_locations()->{'datadir'} . '/' . LOCAL_FILE;
+
+  my $ua = LWP::UserAgent->new();
+  $ua->timeout(TIMEOUT);
+  $ua->protocols_allowed(['http', 'https']);
+
+  # If the URL of the proxy is given, use it, else get this information
+  # from the environment variable.
+  my $proxy_url = Bugzilla->params->{'proxy_url'};
+  if ($proxy_url) {
+    $ua->proxy(['http', 'https'], $proxy_url);
+  }
+  else {
+    $ua->env_proxy;
+  }
+  my $response = eval { $ua->mirror(REMOTE_FILE, $local_file) };
+
+  # $ua->mirror() forces the modification time of the local XML file
+  # to match the modification time of the remote one.
+  # So we have to update it manually to reflect that a newer version
+  # of the file has effectively been requested. This will avoid
+  # any new download for the next TIME_INTERVAL.
+  if (-e $local_file) {
+
+    # Try to alter its last modification time.
+    my $can_alter = utime(undef, undef, $local_file);
+
+    # This error should never happen.
+    $can_alter || return {'error' => 'no_update'};
+  }
+  elsif ($response && $response->is_error) {
+
+    # We have been unable to download the file.
+    return {'error' => 'cannot_download', 'reason' => $response->status_line};
+  }
+  else {
+    return {'error' => 'no_write', 'reason' => $@};
+  }
+
+  # Everything went well.
+  return 0;
 }
 
 sub _compare_versions {
-    my ($old_ver, $new_ver) = @_;
-    while (scalar(@$old_ver) && scalar(@$new_ver)) {
-        my $old = shift(@$old_ver) || 0;
-        my $new = shift(@$new_ver) || 0;
-        return $new <=> $old if ($new <=> $old);
-    }
-    return scalar(@$new_ver) <=> scalar(@$old_ver);
+  my ($old_ver, $new_ver) = @_;
+  while (scalar(@$old_ver) && scalar(@$new_ver)) {
+    my $old = shift(@$old_ver) || 0;
+    my $new = shift(@$new_ver) || 0;
+    return $new <=> $old if ($new <=> $old);
+  }
+  return scalar(@$new_ver) <=> scalar(@$old_ver);
 
 }
 
diff --git a/Bugzilla/User.pm b/Bugzilla/User.pm
index 77e6cebb0..b797f364e 100644
--- a/Bugzilla/User.pm
+++ b/Bugzilla/User.pm
@@ -33,9 +33,9 @@ use URI::QueryParam;
 
 use parent qw(Bugzilla::Object Exporter);
 @Bugzilla::User::EXPORT = qw(is_available_username
-    login_to_id validate_password validate_password_check
-    USER_MATCH_MULTIPLE USER_MATCH_FAILED USER_MATCH_SUCCESS
-    MATCH_SKIP_CONFIRM
+  login_to_id validate_password validate_password_check
+  USER_MATCH_MULTIPLE USER_MATCH_FAILED USER_MATCH_SUCCESS
+  MATCH_SKIP_CONFIRM
 );
 
 #####################################################################
@@ -46,16 +46,16 @@ use constant USER_MATCH_MULTIPLE => -1;
 use constant USER_MATCH_FAILED   => 0;
 use constant USER_MATCH_SUCCESS  => 1;
 
-use constant MATCH_SKIP_CONFIRM  => 1;
+use constant MATCH_SKIP_CONFIRM => 1;
 
 use constant DEFAULT_USER => {
-    'userid'         => 0,
-    'realname'       => '',
-    'login_name'     => '',
-    'showmybugslink' => 0,
-    'disabledtext'   => '',
-    'disable_mail'   => 0,
-    'is_enabled'     => 1, 
+  'userid'         => 0,
+  'realname'       => '',
+  'login_name'     => '',
+  'showmybugslink' => 0,
+  'disabledtext'   => '',
+  'disable_mail'   => 0,
+  'is_enabled'     => 1,
 };
 
 use constant DB_TABLE => 'profiles';
@@ -65,18 +65,19 @@ use constant DB_TABLE => 'profiles';
 # Bugzilla::User used "name" for the realname field. This should be
 # fixed one day.
 sub DB_COLUMNS {
-    my $dbh = Bugzilla->dbh;
-    return (
-        'profiles.userid',
-        'profiles.login_name',
-        'profiles.realname',
-        'profiles.mybugslink AS showmybugslink',
-        'profiles.disabledtext',
-        'profiles.disable_mail',
-        'profiles.extern_id',
-        'profiles.is_enabled',
-        $dbh->sql_date_format('last_seen_date', '%Y-%m-%d') . ' AS last_seen_date',
+  my $dbh = Bugzilla->dbh;
+  return (
+    'profiles.userid',
+    'profiles.login_name',
+    'profiles.realname',
+    'profiles.mybugslink AS showmybugslink',
+    'profiles.disabledtext',
+    'profiles.disable_mail',
+    'profiles.extern_id',
+    'profiles.is_enabled',
+    $dbh->sql_date_format('last_seen_date', '%Y-%m-%d') . ' AS last_seen_date',
     ),
+    ;
 }
 
 use constant NAME_FIELD => 'login_name';
@@ -84,32 +85,30 @@ use constant ID_FIELD   => 'userid';
 use constant LIST_ORDER => NAME_FIELD;
 
 use constant VALIDATORS => {
-    cryptpassword => \&_check_password,
-    disable_mail  => \&_check_disable_mail,
-    disabledtext  => \&_check_disabledtext,
-    login_name    => \&check_login_name,
-    realname      => \&_check_realname,
-    extern_id     => \&_check_extern_id,
-    is_enabled    => \&_check_is_enabled, 
+  cryptpassword => \&_check_password,
+  disable_mail  => \&_check_disable_mail,
+  disabledtext  => \&_check_disabledtext,
+  login_name    => \&check_login_name,
+  realname      => \&_check_realname,
+  extern_id     => \&_check_extern_id,
+  is_enabled    => \&_check_is_enabled,
 };
 
 sub UPDATE_COLUMNS {
-    my $self = shift;
-    my @cols = qw(
-        disable_mail
-        disabledtext
-        login_name
-        realname
-        extern_id
-        is_enabled
-    );
-    push(@cols, 'cryptpassword') if exists $self->{cryptpassword};
-    return @cols;
-};
+  my $self = shift;
+  my @cols = qw(
+    disable_mail
+    disabledtext
+    login_name
+    realname
+    extern_id
+    is_enabled
+  );
+  push(@cols, 'cryptpassword') if exists $self->{cryptpassword};
+  return @cols;
+}
 
-use constant VALIDATOR_DEPENDENCIES => {
-    is_enabled => ['disabledtext'], 
-};
+use constant VALIDATOR_DEPENDENCIES => {is_enabled => ['disabledtext'],};
 
 use constant EXTRA_REQUIRED_FIELDS => qw(is_enabled);
 
@@ -118,129 +117,127 @@ use constant EXTRA_REQUIRED_FIELDS => qw(is_enabled);
 ################################################################################
 
 sub new {
-    my $invocant = shift;
-    my $class = ref($invocant) || $invocant;
-    my ($param) = @_;
-
-    my $user = { %{ DEFAULT_USER() } };
-    bless ($user, $class);
-    return $user unless $param;
-
-    if (ref($param) eq 'HASH') {
-        if (defined $param->{extern_id}) {
-            $param = { condition => 'extern_id = ?' , values => [$param->{extern_id}] };
-            $_[0] = $param;
-        }
+  my $invocant = shift;
+  my $class    = ref($invocant) || $invocant;
+  my ($param)  = @_;
+
+  my $user = {%{DEFAULT_USER()}};
+  bless($user, $class);
+  return $user unless $param;
+
+  if (ref($param) eq 'HASH') {
+    if (defined $param->{extern_id}) {
+      $param = {condition => 'extern_id = ?', values => [$param->{extern_id}]};
+      $_[0] = $param;
     }
-    return $class->SUPER::new(@_);
+  }
+  return $class->SUPER::new(@_);
 }
 
 sub super_user {
-    my $invocant = shift;
-    my $class = ref($invocant) || $invocant;
-    my ($param) = @_;
+  my $invocant = shift;
+  my $class    = ref($invocant) || $invocant;
+  my ($param)  = @_;
 
-    my $user = { %{ DEFAULT_USER() } };
-    $user->{groups} = [Bugzilla::Group->get_all];
-    $user->{bless_groups} = [Bugzilla::Group->get_all];
-    bless $user, $class;
-    return $user;
+  my $user = {%{DEFAULT_USER()}};
+  $user->{groups}       = [Bugzilla::Group->get_all];
+  $user->{bless_groups} = [Bugzilla::Group->get_all];
+  bless $user, $class;
+  return $user;
 }
 
 sub _update_groups {
-    my $self = shift;
-    my $group_changes = shift;
-    my $changes = shift;
-    my $dbh = Bugzilla->dbh;
-
-    # Update group settings.
-    my $sth_add_mapping = $dbh->prepare(
-        qq{INSERT INTO user_group_map (
+  my $self          = shift;
+  my $group_changes = shift;
+  my $changes       = shift;
+  my $dbh           = Bugzilla->dbh;
+
+  # Update group settings.
+  my $sth_add_mapping = $dbh->prepare(
+    qq{INSERT INTO user_group_map (
                   user_id, group_id, isbless, grant_type
                  ) VALUES (
                   ?, ?, ?, ?
                  )
-          });
-    my $sth_remove_mapping = $dbh->prepare(
-        qq{DELETE FROM user_group_map
+          }
+  );
+  my $sth_remove_mapping = $dbh->prepare(
+    qq{DELETE FROM user_group_map
             WHERE user_id = ?
               AND group_id = ?
               AND isbless = ?
               AND grant_type = ?
-          });
+          }
+  );
 
-    foreach my $is_bless (keys %$group_changes) {
-        my ($removed, $added) = @{$group_changes->{$is_bless}};
+  foreach my $is_bless (keys %$group_changes) {
+    my ($removed, $added) = @{$group_changes->{$is_bless}};
 
-        foreach my $group (@$removed) {
-            $sth_remove_mapping->execute(
-                $self->id, $group->id, $is_bless, GRANT_DIRECT
-             );
-        }
-        foreach my $group (@$added) {
-            $sth_add_mapping->execute(
-                $self->id, $group->id, $is_bless, GRANT_DIRECT
-             );
-        }
+    foreach my $group (@$removed) {
+      $sth_remove_mapping->execute($self->id, $group->id, $is_bless, GRANT_DIRECT);
+    }
+    foreach my $group (@$added) {
+      $sth_add_mapping->execute($self->id, $group->id, $is_bless, GRANT_DIRECT);
+    }
 
-        if (! $is_bless) {
-            my $query = qq{
+    if (!$is_bless) {
+      my $query = qq{
                 INSERT INTO profiles_activity
                     (userid, who, profiles_when, fieldid, oldvalue, newvalue)
                 VALUES ( ?, ?, now(), ?, ?, ?)
             };
 
-            $dbh->do(
-                $query, undef,
-                $self->id, Bugzilla->user->id,
-                get_field_id('bug_group'),
-                join(', ', map { $_->name } @$removed),
-                join(', ', map { $_->name } @$added)
-            );
-        }
-        else {
-            # XXX: should create profiles_activity entries for blesser changes.
-        }
+      $dbh->do(
+        $query, undef, $self->id, Bugzilla->user->id,
+        get_field_id('bug_group'),
+        join(', ', map { $_->name } @$removed),
+        join(', ', map { $_->name } @$added)
+      );
+    }
+    else {
+      # XXX: should create profiles_activity entries for blesser changes.
+    }
 
-        Bugzilla->memcached->clear_config({ key => 'user_groups.' . $self->id });
+    Bugzilla->memcached->clear_config({key => 'user_groups.' . $self->id});
 
-        my $type = $is_bless ? 'bless_groups' : 'groups';
-        $changes->{$type} = [
-            [ map { $_->name } @$removed ],
-            [ map { $_->name } @$added ],
-        ];
-    }
+    my $type = $is_bless ? 'bless_groups' : 'groups';
+    $changes->{$type} = [[map { $_->name } @$removed], [map { $_->name } @$added],];
+  }
 }
 
 sub update {
-    my $self = shift;
-    my $options = shift;
+  my $self    = shift;
+  my $options = shift;
 
-    my $group_changes = delete $self->{_group_changes};
+  my $group_changes = delete $self->{_group_changes};
 
-    my $changes = $self->SUPER::update(@_);
-    my $dbh = Bugzilla->dbh;
-    $self->_update_groups($group_changes, $changes);
+  my $changes = $self->SUPER::update(@_);
+  my $dbh     = Bugzilla->dbh;
+  $self->_update_groups($group_changes, $changes);
 
-    if (exists $changes->{login_name}) {
-        # Delete all the tokens related to the userid
-        $dbh->do('DELETE FROM tokens WHERE userid = ?', undef, $self->id)
-          unless $options->{keep_tokens};
-        # And rederive regex groups
-        $self->derive_regexp_groups();
-    }
+  if (exists $changes->{login_name}) {
+
+    # Delete all the tokens related to the userid
+    $dbh->do('DELETE FROM tokens WHERE userid = ?', undef, $self->id)
+      unless $options->{keep_tokens};
 
-    # Logout the user if necessary.
-    Bugzilla->logout_user($self) 
-        if (!$options->{keep_session}
-            && (exists $changes->{login_name}
-                || exists $changes->{disabledtext}
-                || exists $changes->{cryptpassword}));
+    # And rederive regex groups
+    $self->derive_regexp_groups();
+  }
+
+  # Logout the user if necessary.
+  Bugzilla->logout_user($self)
+    if (
+    !$options->{keep_session}
+    && ( exists $changes->{login_name}
+      || exists $changes->{disabledtext}
+      || exists $changes->{cryptpassword})
+    );
 
-    # XXX Can update profiles_activity here as soon as it understands
-    #     field names like login_name.
-    
-    return $changes;
+  # XXX Can update profiles_activity here as soon as it understands
+  #     field names like login_name.
+
+  return $changes;
 }
 
 ################################################################################
@@ -252,62 +249,63 @@ sub _check_disabledtext { return trim($_[1]) || ''; }
 
 # Check whether the extern_id is unique.
 sub _check_extern_id {
-    my ($invocant, $extern_id) = @_;
-    $extern_id = trim($extern_id);
-    return undef unless defined($extern_id) && $extern_id ne "";
-    if (!ref($invocant) || $invocant->extern_id ne $extern_id) {
-        my $existing_login = $invocant->new({ extern_id => $extern_id });
-        if ($existing_login) {
-            ThrowUserError( 'extern_id_exists',
-                            { extern_id => $extern_id,
-                              existing_login_name => $existing_login->login });
-        }
+  my ($invocant, $extern_id) = @_;
+  $extern_id = trim($extern_id);
+  return undef unless defined($extern_id) && $extern_id ne "";
+  if (!ref($invocant) || $invocant->extern_id ne $extern_id) {
+    my $existing_login = $invocant->new({extern_id => $extern_id});
+    if ($existing_login) {
+      ThrowUserError('extern_id_exists',
+        {extern_id => $extern_id, existing_login_name => $existing_login->login});
     }
-    return $extern_id;
+  }
+  return $extern_id;
 }
 
 # This is public since createaccount.cgi needs to use it before issuing
 # a token for account creation.
 sub check_login_name {
-    my ($invocant, $name) = @_;
-    $name = trim($name);
-    $name || ThrowUserError('user_login_required');
-    check_email_syntax($name);
-
-    # Check the name if it's a new user, or if we're changing the name.
-    if (!ref($invocant) || lc($invocant->login) ne lc($name)) {
-        my @params = ($name);
-        push(@params, $invocant->login) if ref($invocant);
-        is_available_username(@params)
-            || ThrowUserError('account_exists', { email => $name });
-    }
+  my ($invocant, $name) = @_;
+  $name = trim($name);
+  $name || ThrowUserError('user_login_required');
+  check_email_syntax($name);
 
-    return $name;
+  # Check the name if it's a new user, or if we're changing the name.
+  if (!ref($invocant) || lc($invocant->login) ne lc($name)) {
+    my @params = ($name);
+    push(@params, $invocant->login) if ref($invocant);
+    is_available_username(@params)
+      || ThrowUserError('account_exists', {email => $name});
+  }
+
+  return $name;
 }
 
 sub _check_password {
-    my ($self, $pass) = @_;
+  my ($self, $pass) = @_;
 
-    # If the password is '*', do not encrypt it or validate it further--we 
-    # are creating a user who should not be able to log in using DB 
-    # authentication.
-    return $pass if $pass eq '*';
+  # If the password is '*', do not encrypt it or validate it further--we
+  # are creating a user who should not be able to log in using DB
+  # authentication.
+  return $pass if $pass eq '*';
 
-    validate_password($pass);
-    my $cryptpassword = bz_crypt($pass);
-    return $cryptpassword;
+  validate_password($pass);
+  my $cryptpassword = bz_crypt($pass);
+  return $cryptpassword;
 }
 
 sub _check_realname { return trim($_[1]) || ''; }
 
 sub _check_is_enabled {
-    my ($invocant, $is_enabled, undef, $params) = @_;
-    # is_enabled is set automatically on creation depending on whether 
-    # disabledtext is empty (enabled) or not empty (disabled).
-    # When updating the user, is_enabled is set by calling set_disabledtext().
-    # Any value passed into this validator is ignored.
-    my $disabledtext = ref($invocant) ? $invocant->disabledtext : $params->{disabledtext};
-    return $disabledtext ? 0 : 1;
+  my ($invocant, $is_enabled, undef, $params) = @_;
+
+  # is_enabled is set automatically on creation depending on whether
+  # disabledtext is empty (enabled) or not empty (disabled).
+  # When updating the user, is_enabled is set by calling set_disabledtext().
+  # Any value passed into this validator is ignored.
+  my $disabledtext
+    = ref($invocant) ? $invocant->disabledtext : $params->{disabledtext};
+  return $disabledtext ? 0 : 1;
 }
 
 ################################################################################
@@ -316,150 +314,151 @@ sub _check_is_enabled {
 
 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_extern_id     { $_[0]->set('extern_id',    $_[1]); }
 
 sub set_login {
-    my ($self, $login) = @_;
-    $self->set('login_name', $login);
-    delete $self->{identity};
-    delete $self->{nick};
+  my ($self, $login) = @_;
+  $self->set('login_name', $login);
+  delete $self->{identity};
+  delete $self->{nick};
 }
 
 sub set_name {
-    my ($self, $name) = @_;
-    $self->set('realname', $name);
-    delete $self->{identity};
+  my ($self, $name) = @_;
+  $self->set('realname', $name);
+  delete $self->{identity};
 }
 
 sub set_password { $_[0]->set('cryptpassword', $_[1]); }
 
 sub set_disabledtext {
-    $_[0]->set('disabledtext', $_[1]);
-    $_[0]->set('is_enabled', $_[1] ? 0 : 1);
+  $_[0]->set('disabledtext', $_[1]);
+  $_[0]->set('is_enabled', $_[1] ? 0 : 1);
 }
 
 sub set_groups {
-    my $self = shift;
-    $self->_set_groups(GROUP_MEMBERSHIP, @_);
+  my $self = shift;
+  $self->_set_groups(GROUP_MEMBERSHIP, @_);
 }
 
 sub set_bless_groups {
-    my $self = shift;
+  my $self = shift;
 
-    # The person making the change needs to be in the editusers group
-    Bugzilla->user->in_group('editusers')
-        || ThrowUserError("auth_failure", {group  => "editusers",
-                                           reason => "cant_bless",
-                                           action => "edit",
-                                           object => "users"});
+  # The person making the change needs to be in the editusers group
+  Bugzilla->user->in_group('editusers') || ThrowUserError(
+    "auth_failure",
+    {
+      group  => "editusers",
+      reason => "cant_bless",
+      action => "edit",
+      object => "users"
+    }
+  );
 
-    $self->_set_groups(GROUP_BLESS, @_);
+  $self->_set_groups(GROUP_BLESS, @_);
 }
 
 sub _set_groups {
-    my $self     = shift;
-    my $is_bless = shift;
-    my $changes  = shift;
-    my $dbh = Bugzilla->dbh;
+  my $self     = shift;
+  my $is_bless = shift;
+  my $changes  = shift;
+  my $dbh      = Bugzilla->dbh;
 
-    # The person making the change is $user, $self is the person being changed
-    my $user = Bugzilla->user;
+  # The person making the change is $user, $self is the person being changed
+  my $user = Bugzilla->user;
 
-    # Input is a hash of arrays. Key is 'set', 'add' or 'remove'. The array
-    # is a list of group ids and/or names.
+  # Input is a hash of arrays. Key is 'set', 'add' or 'remove'. The array
+  # is a list of group ids and/or names.
 
-    # First turn the arrays into group objects.
-    $changes = $self->_set_groups_to_object($changes);
+  # First turn the arrays into group objects.
+  $changes = $self->_set_groups_to_object($changes);
 
-    # Get a list of the groups the user currently is a member of
-    my $ids = $dbh->selectcol_arrayref(
-        q{SELECT DISTINCT group_id
+  # Get a list of the groups the user currently is a member of
+  my $ids = $dbh->selectcol_arrayref(
+    q{SELECT DISTINCT group_id
             FROM user_group_map
-           WHERE user_id = ? AND isbless = ? AND grant_type = ?},
-        undef, $self->id, $is_bless, GRANT_DIRECT);
-
-    my $current_groups = Bugzilla::Group->new_from_list($ids);
-    my $new_groups = dclone($current_groups);
-
-    # Record the changes
-    if (exists $changes->{set}) {
-        $new_groups = $changes->{set};
-
-        # We need to check the user has bless rights on the existing groups
-        # If they don't, then we need to add them back to new_groups
-        foreach my $group (@$current_groups) {
-            if (! $user->can_bless($group->id)) {
-                push @$new_groups, $group
-                    unless grep { $_->id eq $group->id } @$new_groups;
-            }
-        }
+           WHERE user_id = ? AND isbless = ? AND grant_type = ?}, undef, $self->id,
+    $is_bless, GRANT_DIRECT
+  );
+
+  my $current_groups = Bugzilla::Group->new_from_list($ids);
+  my $new_groups     = dclone($current_groups);
+
+  # Record the changes
+  if (exists $changes->{set}) {
+    $new_groups = $changes->{set};
+
+    # We need to check the user has bless rights on the existing groups
+    # If they don't, then we need to add them back to new_groups
+    foreach my $group (@$current_groups) {
+      if (!$user->can_bless($group->id)) {
+        push @$new_groups, $group unless grep { $_->id eq $group->id } @$new_groups;
+      }
     }
-    else {
-        foreach my $group (@{$changes->{remove} // []}) {
-            @$new_groups = grep { $_->id ne $group->id } @$new_groups;
-        }
-        foreach my $group (@{$changes->{add} // []}) {
-            push @$new_groups, $group
-                unless grep { $_->id eq $group->id } @$new_groups;
-        }
+  }
+  else {
+    foreach my $group (@{$changes->{remove} // []}) {
+      @$new_groups = grep { $_->id ne $group->id } @$new_groups;
     }
-
-    # Stash the changes, so self->update can actually make them
-    my @diffs = diff_arrays($current_groups, $new_groups, 'id');
-    if (scalar(@{$diffs[0]}) || scalar(@{$diffs[1]})) {
-        $self->{_group_changes}{$is_bless} = \@diffs;
+    foreach my $group (@{$changes->{add} // []}) {
+      push @$new_groups, $group unless grep { $_->id eq $group->id } @$new_groups;
     }
+  }
+
+  # Stash the changes, so self->update can actually make them
+  my @diffs = diff_arrays($current_groups, $new_groups, 'id');
+  if (scalar(@{$diffs[0]}) || scalar(@{$diffs[1]})) {
+    $self->{_group_changes}{$is_bless} = \@diffs;
+  }
 }
 
 sub _set_groups_to_object {
-    my $self = shift;
-    my $changes = shift;
-    my $user = Bugzilla->user;
-
-    foreach my $key (keys %$changes) {
-        # Check we were given an array
-        unless (ref($changes->{$key}) eq 'ARRAY') {
-            ThrowCodeError(
-                'param_invalid',
-                { param => $changes->{$key}, function => $key }
-            );
-        }
+  my $self    = shift;
+  my $changes = shift;
+  my $user    = Bugzilla->user;
 
-        # Go through the array, and turn items into group objects
-        my @groups = ();
-        foreach my $value (@{$changes->{$key}}) {
-            my $type = $value =~ /^\d+$/ ? 'id' : 'name';
-            my $group = Bugzilla::Group->new({$type => $value});
-
-            if (! $group || ! $user->can_bless($group->id)) {
-                ThrowUserError('auth_failure',
-                    { group  => $value, reason => 'cant_bless',
-                      action => 'edit', object => 'users' });
-            }
-            push @groups, $group;
-        }
-        $changes->{$key} = \@groups;
+  foreach my $key (keys %$changes) {
+
+    # Check we were given an array
+    unless (ref($changes->{$key}) eq 'ARRAY') {
+      ThrowCodeError('param_invalid', {param => $changes->{$key}, function => $key});
+    }
+
+    # Go through the array, and turn items into group objects
+    my @groups = ();
+    foreach my $value (@{$changes->{$key}}) {
+      my $type = $value =~ /^\d+$/ ? 'id' : 'name';
+      my $group = Bugzilla::Group->new({$type => $value});
+
+      if (!$group || !$user->can_bless($group->id)) {
+        ThrowUserError('auth_failure',
+          {group => $value, reason => 'cant_bless', action => 'edit', object => 'users'});
+      }
+      push @groups, $group;
     }
+    $changes->{$key} = \@groups;
+  }
 
-    return $changes;
+  return $changes;
 }
 
 sub update_last_seen_date {
-    my $self = shift;
-    return unless $self->id;
-    my $dbh = Bugzilla->dbh;
-    my $date = $dbh->selectrow_array(
-        'SELECT ' . $dbh->sql_date_format('NOW()', '%Y-%m-%d'));
-
-    if (!$self->last_seen_date or $date ne $self->last_seen_date) {
-        $self->{last_seen_date} = $date;
-        # We don't use the normal update() routine here as we only
-        # want to update the last_seen_date column, not any other
-        # pending changes
-        $dbh->do("UPDATE profiles SET last_seen_date = ? WHERE userid = ?",
-                 undef, $date, $self->id);
-        Bugzilla->memcached->clear({ table => 'profiles', id => $self->id });
-    }
+  my $self = shift;
+  return unless $self->id;
+  my $dbh  = Bugzilla->dbh;
+  my $date = $dbh->selectrow_array(
+    'SELECT ' . $dbh->sql_date_format('NOW()', '%Y-%m-%d'));
+
+  if (!$self->last_seen_date or $date ne $self->last_seen_date) {
+    $self->{last_seen_date} = $date;
+
+    # We don't use the normal update() routine here as we only
+    # want to update the last_seen_date column, not any other
+    # pending changes
+    $dbh->do("UPDATE profiles SET last_seen_date = ? WHERE userid = ?",
+      undef, $date, $self->id);
+    Bugzilla->memcached->clear({table => 'profiles', id => $self->id});
+  }
 }
 
 ################################################################################
@@ -467,171 +466,181 @@ sub update_last_seen_date {
 ################################################################################
 
 # Accessors for user attributes
-sub name  { $_[0]->{realname};   }
-sub login { $_[0]->{login_name}; }
-sub extern_id { $_[0]->{extern_id}; }
-sub email { $_[0]->login . Bugzilla->params->{'emailsuffix'}; }
-sub disabledtext { $_[0]->{'disabledtext'}; }
-sub is_enabled { $_[0]->{'is_enabled'} ? 1 : 0; }
+sub name           { $_[0]->{realname}; }
+sub login          { $_[0]->{login_name}; }
+sub extern_id      { $_[0]->{extern_id}; }
+sub email          { $_[0]->login . Bugzilla->params->{'emailsuffix'}; }
+sub disabledtext   { $_[0]->{'disabledtext'}; }
+sub is_enabled     { $_[0]->{'is_enabled'} ? 1 : 0; }
 sub showmybugslink { $_[0]->{showmybugslink}; }
 sub email_disabled { $_[0]->{disable_mail}; }
-sub email_enabled { !($_[0]->{disable_mail}); }
+sub email_enabled  { !($_[0]->{disable_mail}); }
 sub last_seen_date { $_[0]->{last_seen_date}; }
+
 sub cryptpassword {
-    my $self = shift;
-    # We don't store it because we never want it in the object (we
-    # don't want to accidentally dump even the hash somewhere).
-    my ($pw) = Bugzilla->dbh->selectrow_array(
-        'SELECT cryptpassword FROM profiles WHERE userid = ?',
-        undef, $self->id);
-    return $pw;
+  my $self = shift;
+
+  # We don't store it because we never want it in the object (we
+  # don't want to accidentally dump even the hash somewhere).
+  my ($pw)
+    = Bugzilla->dbh->selectrow_array(
+    'SELECT cryptpassword FROM profiles WHERE userid = ?',
+    undef, $self->id);
+  return $pw;
 }
 
 sub set_authorizer {
-    my ($self, $authorizer) = @_;
-    $self->{authorizer} = $authorizer;
+  my ($self, $authorizer) = @_;
+  $self->{authorizer} = $authorizer;
 }
+
 sub authorizer {
-    my ($self) = @_;
-    if (!$self->{authorizer}) {
-        require Bugzilla::Auth;
-        $self->{authorizer} = new Bugzilla::Auth();
-    }
-    return $self->{authorizer};
+  my ($self) = @_;
+  if (!$self->{authorizer}) {
+    require Bugzilla::Auth;
+    $self->{authorizer} = new Bugzilla::Auth();
+  }
+  return $self->{authorizer};
 }
 
 # Generate a string to identify the user by name + login if the user
 # has a name or by login only if they don't.
 sub identity {
-    my $self = shift;
+  my $self = shift;
 
-    return "" unless $self->id;
+  return "" unless $self->id;
 
-    if (!defined $self->{identity}) {
-        $self->{identity} = 
-          $self->name ? $self->name . " <" . $self->login. ">" : $self->login;
-    }
+  if (!defined $self->{identity}) {
+    $self->{identity}
+      = $self->name ? $self->name . " <" . $self->login . ">" : $self->login;
+  }
 
-    return $self->{identity};
+  return $self->{identity};
 }
 
 sub nick {
-    my $self = shift;
+  my $self = shift;
 
-    return "" unless $self->id;
+  return "" unless $self->id;
 
-    if (!defined $self->{nick}) {
-        $self->{nick} = (split(/@/, $self->login, 2))[0];
-    }
+  if (!defined $self->{nick}) {
+    $self->{nick} = (split(/@/, $self->login, 2))[0];
+  }
 
-    return $self->{nick};
+  return $self->{nick};
 }
 
 sub queries {
-    my $self = shift;
-    return $self->{queries} if defined $self->{queries};
-    return [] unless $self->id;
+  my $self = shift;
+  return $self->{queries} if defined $self->{queries};
+  return [] unless $self->id;
 
-    my $dbh = Bugzilla->dbh;
-    my $query_ids = $dbh->selectcol_arrayref(
-        'SELECT id FROM namedqueries WHERE userid = ?', undef, $self->id);
-    require Bugzilla::Search::Saved;
-    $self->{queries} = Bugzilla::Search::Saved->new_from_list($query_ids);
+  my $dbh = Bugzilla->dbh;
+  my $query_ids
+    = $dbh->selectcol_arrayref('SELECT id FROM namedqueries WHERE userid = ?',
+    undef, $self->id);
+  require Bugzilla::Search::Saved;
+  $self->{queries} = Bugzilla::Search::Saved->new_from_list($query_ids);
 
-    # We preload link_in_footer from here as this information is always requested.
-    # This only works if the user object represents the current logged in user.
-    Bugzilla::Search::Saved::preload($self->{queries}) if $self->id == Bugzilla->user->id;
+  # We preload link_in_footer from here as this information is always requested.
+  # This only works if the user object represents the current logged in user.
+  Bugzilla::Search::Saved::preload($self->{queries})
+    if $self->id == Bugzilla->user->id;
 
-    return $self->{queries};
+  return $self->{queries};
 }
 
 sub queries_subscribed {
-    my $self = shift;
-    return $self->{queries_subscribed} if defined $self->{queries_subscribed};
-    return [] unless $self->id;
-
-    # Exclude the user's own queries.
-    my @my_query_ids = map($_->id, @{$self->queries});
-    my $query_id_string = join(',', @my_query_ids) || '-1';
-
-    # Only show subscriptions that we can still actually see. If a
-    # user changes the shared group of a query, our subscription
-    # will remain but we won't have access to the query anymore.
-    my $subscribed_query_ids = Bugzilla->dbh->selectcol_arrayref(
-        "SELECT lif.namedquery_id
+  my $self = shift;
+  return $self->{queries_subscribed} if defined $self->{queries_subscribed};
+  return [] unless $self->id;
+
+  # Exclude the user's own queries.
+  my @my_query_ids = map($_->id, @{$self->queries});
+  my $query_id_string = join(',', @my_query_ids) || '-1';
+
+  # Only show subscriptions that we can still actually see. If a
+  # user changes the shared group of a query, our subscription
+  # will remain but we won't have access to the query anymore.
+  my $subscribed_query_ids = Bugzilla->dbh->selectcol_arrayref(
+    "SELECT lif.namedquery_id
            FROM namedqueries_link_in_footer lif
                 INNER JOIN namedquery_group_map ngm
                 ON ngm.namedquery_id = lif.namedquery_id
           WHERE lif.user_id = ? 
                 AND lif.namedquery_id NOT IN ($query_id_string)
-                AND " . $self->groups_in_sql,
-          undef, $self->id);
-    require Bugzilla::Search::Saved;
-    $self->{queries_subscribed} =
-        Bugzilla::Search::Saved->new_from_list($subscribed_query_ids);
-    return $self->{queries_subscribed};
+                AND " . $self->groups_in_sql, undef, $self->id
+  );
+  require Bugzilla::Search::Saved;
+  $self->{queries_subscribed}
+    = Bugzilla::Search::Saved->new_from_list($subscribed_query_ids);
+  return $self->{queries_subscribed};
 }
 
 sub queries_available {
-    my $self = shift;
-    return $self->{queries_available} if defined $self->{queries_available};
-    return [] unless $self->id;
+  my $self = shift;
+  return $self->{queries_available} if defined $self->{queries_available};
+  return [] unless $self->id;
 
-    # Exclude the user's own queries.
-    my @my_query_ids = map($_->id, @{$self->queries});
-    my $query_id_string = join(',', @my_query_ids) || '-1';
+  # Exclude the user's own queries.
+  my @my_query_ids = map($_->id, @{$self->queries});
+  my $query_id_string = join(',', @my_query_ids) || '-1';
 
-    my $avail_query_ids = Bugzilla->dbh->selectcol_arrayref(
-        'SELECT namedquery_id FROM namedquery_group_map
-          WHERE '  . $self->groups_in_sql . "
-                AND namedquery_id NOT IN ($query_id_string)");
-    require Bugzilla::Search::Saved;
-    $self->{queries_available} =
-        Bugzilla::Search::Saved->new_from_list($avail_query_ids);
-    return $self->{queries_available};
+  my $avail_query_ids = Bugzilla->dbh->selectcol_arrayref(
+    'SELECT namedquery_id FROM namedquery_group_map
+          WHERE ' . $self->groups_in_sql . "
+                AND namedquery_id NOT IN ($query_id_string)"
+  );
+  require Bugzilla::Search::Saved;
+  $self->{queries_available}
+    = Bugzilla::Search::Saved->new_from_list($avail_query_ids);
+  return $self->{queries_available};
 }
 
 sub tags {
-    my $self = shift;
-    my $dbh = Bugzilla->dbh;
-
-    if (!defined $self->{tags}) {
-        # We must use LEFT JOIN instead of INNER JOIN as we may be
-        # in the process of inserting a new tag to some bugs,
-        # in which case there are no bugs with this tag yet.
-        $self->{tags} = $dbh->selectall_hashref(
-            'SELECT name, id, COUNT(bug_id) AS bug_count
+  my $self = shift;
+  my $dbh  = Bugzilla->dbh;
+
+  if (!defined $self->{tags}) {
+
+    # We must use LEFT JOIN instead of INNER JOIN as we may be
+    # in the process of inserting a new tag to some bugs,
+    # in which case there are no bugs with this tag yet.
+    $self->{tags} = $dbh->selectall_hashref(
+      'SELECT name, id, COUNT(bug_id) AS bug_count
                FROM tag
           LEFT JOIN bug_tag ON bug_tag.tag_id = tag.id
-              WHERE user_id = ? ' . $dbh->sql_group_by('id', 'name'),
-            'name', undef, $self->id);
-    }
-    return $self->{tags};
+              WHERE user_id = ? ' . $dbh->sql_group_by('id', 'name'), 'name', undef,
+      $self->id
+    );
+  }
+  return $self->{tags};
 }
 
 sub bugs_ignored {
-    my ($self) = @_;
-    my $dbh = Bugzilla->dbh;
-    if (!defined $self->{'bugs_ignored'}) {
-        $self->{'bugs_ignored'} = $dbh->selectall_arrayref(
-            'SELECT bugs.bug_id AS id,
+  my ($self) = @_;
+  my $dbh = Bugzilla->dbh;
+  if (!defined $self->{'bugs_ignored'}) {
+    $self->{'bugs_ignored'} = $dbh->selectall_arrayref(
+      'SELECT bugs.bug_id AS id,
                     bugs.bug_status AS status,
                     bugs.short_desc AS summary
                FROM bugs
                     INNER JOIN email_bug_ignore
                     ON bugs.bug_id = email_bug_ignore.bug_id
-              WHERE user_id = ?',
-            { Slice => {} }, $self->id);
-        # Go ahead and load these into the visible bugs cache
-        # to speed up can_see_bug checks later
-        $self->visible_bugs([ map { $_->{'id'} } @{ $self->{'bugs_ignored'} } ]);
-    }
-    return $self->{'bugs_ignored'};
+              WHERE user_id = ?', {Slice => {}}, $self->id
+    );
+
+    # Go ahead and load these into the visible bugs cache
+    # to speed up can_see_bug checks later
+    $self->visible_bugs([map { $_->{'id'} } @{$self->{'bugs_ignored'}}]);
+  }
+  return $self->{'bugs_ignored'};
 }
 
 sub is_bug_ignored {
-    my ($self, $bug_id) = @_;
-    return (grep {$_->{'id'} == $bug_id} @{$self->bugs_ignored}) ? 1 : 0;
+  my ($self, $bug_id) = @_;
+  return (grep { $_->{'id'} == $bug_id } @{$self->bugs_ignored}) ? 1 : 0;
 }
 
 ##########################
@@ -639,309 +648,316 @@ sub is_bug_ignored {
 ##########################
 
 sub recent_searches {
-    my $self = shift;
-    $self->{recent_searches} ||= 
-        Bugzilla::Search::Recent->match({ user_id => $self->id });
-    return $self->{recent_searches};
+  my $self = shift;
+  $self->{recent_searches}
+    ||= Bugzilla::Search::Recent->match({user_id => $self->id});
+  return $self->{recent_searches};
 }
 
 sub recent_search_containing {
-    my ($self, $bug_id) = @_;
-    my $searches = $self->recent_searches;
+  my ($self, $bug_id) = @_;
+  my $searches = $self->recent_searches;
 
-    foreach my $search (@$searches) {
-        return $search if grep($_ == $bug_id, @{ $search->bug_list });
-    }
+  foreach my $search (@$searches) {
+    return $search if grep($_ == $bug_id, @{$search->bug_list});
+  }
 
-    return undef;
+  return undef;
 }
 
 sub recent_search_for {
-    my ($self, $bug) = @_;
-    my $params = Bugzilla->input_params;
-    my $cgi = Bugzilla->cgi;
-
-    if ($self->id) {
-        # First see if there's a list_id parameter in the query string.
-        my $list_id = $params->{list_id};
-        if (!$list_id) {
-            # If not, check for "list_id" in the query string of the referer.
-            my $referer = $cgi->referer;
-            if ($referer) {
-                my $uri = URI->new($referer);
-                if ($uri->path =~ /buglist\.cgi$/) {
-                    $list_id = $uri->query_param('list_id')
-                               || $uri->query_param('regetlastlist');
-                }
-            }
+  my ($self, $bug) = @_;
+  my $params = Bugzilla->input_params;
+  my $cgi    = Bugzilla->cgi;
+
+  if ($self->id) {
+
+    # First see if there's a list_id parameter in the query string.
+    my $list_id = $params->{list_id};
+    if (!$list_id) {
+
+      # If not, check for "list_id" in the query string of the referer.
+      my $referer = $cgi->referer;
+      if ($referer) {
+        my $uri = URI->new($referer);
+        if ($uri->path =~ /buglist\.cgi$/) {
+          $list_id = $uri->query_param('list_id') || $uri->query_param('regetlastlist');
         }
+      }
+    }
 
-        if ($list_id && $list_id ne 'cookie') {
-            # If we got a bad list_id (either some other user's or an expired
-            # one) don't crash, just don't return that list.
-            my $search = Bugzilla::Search::Recent->check_quietly(
-                { id => $list_id });
-            return $search if $search;
-        }
+    if ($list_id && $list_id ne 'cookie') {
 
-        # If there's no list_id, see if the current bug's id is contained
-        # in any of the user's saved lists.
-        my $search = $self->recent_search_containing($bug->id);
-        return $search if $search;
+      # If we got a bad list_id (either some other user's or an expired
+      # one) don't crash, just don't return that list.
+      my $search = Bugzilla::Search::Recent->check_quietly({id => $list_id});
+      return $search if $search;
     }
 
-    # Finally (or always, if we're logged out), if there's a BUGLIST cookie
-    # and the selected bug is in the list, then return the cookie as a fake
-    # Search::Recent object.
-    if (my $list = $cgi->cookie('BUGLIST')) {
-        # Also split on colons, which was used as a separator in old cookies.
-        my @bug_ids = split(/[:-]/, $list);
-        if (grep { $_ == $bug->id } @bug_ids) {
-            my $search = Bugzilla::Search::Recent->new_from_cookie(\@bug_ids);
-            return $search;
-        }
+    # If there's no list_id, see if the current bug's id is contained
+    # in any of the user's saved lists.
+    my $search = $self->recent_search_containing($bug->id);
+    return $search if $search;
+  }
+
+  # Finally (or always, if we're logged out), if there's a BUGLIST cookie
+  # and the selected bug is in the list, then return the cookie as a fake
+  # Search::Recent object.
+  if (my $list = $cgi->cookie('BUGLIST')) {
+
+    # Also split on colons, which was used as a separator in old cookies.
+    my @bug_ids = split(/[:-]/, $list);
+    if (grep { $_ == $bug->id } @bug_ids) {
+      my $search = Bugzilla::Search::Recent->new_from_cookie(\@bug_ids);
+      return $search;
     }
+  }
 
-    return undef;
+  return undef;
 }
 
 sub save_last_search {
-    my ($self, $params) = @_;
-    my ($bug_ids, $order, $vars, $list_id) = 
-        @$params{qw(bugs order vars list_id)};
-
-    my $cgi = Bugzilla->cgi;
-    if ($order) {
-        $cgi->send_cookie(-name => 'LASTORDER',
-                          -value => $order,
-                          -expires => 'Fri, 01-Jan-2038 00:00:00 GMT');
-    }
+  my ($self, $params) = @_;
+  my ($bug_ids, $order, $vars, $list_id) = @$params{qw(bugs order vars list_id)};
+
+  my $cgi = Bugzilla->cgi;
+  if ($order) {
+    $cgi->send_cookie(
+      -name    => 'LASTORDER',
+      -value   => $order,
+      -expires => 'Fri, 01-Jan-2038 00:00:00 GMT'
+    );
+  }
 
-    return if !@$bug_ids;
-
-    my $search;
-    if ($self->id) {
-        on_main_db {
-            if ($list_id) {
-                $search = Bugzilla::Search::Recent->check_quietly({ id => $list_id });
-            }
-
-            if ($search) {
-                if (join(',', @{$search->bug_list}) ne join(',', @$bug_ids)) {
-                    $search->set_bug_list($bug_ids);
-                }
-                if (!$search->list_order || $order ne $search->list_order) {
-                    $search->set_list_order($order);
-                }
-                $search->update();
-            }
-            else {
-                # If we already have an existing search with a totally
-                # identical bug list, then don't create a new one. This
-                # prevents people from writing over their whole 
-                # recent-search list by just refreshing a saved search
-                # (which doesn't have list_id in the header) over and over.
-                my $list_string = join(',', @$bug_ids);
-                my $existing_search = Bugzilla::Search::Recent->match({
-                    user_id => $self->id, bug_list => $list_string });
-           
-                if (!scalar(@$existing_search)) {
-                    $search = Bugzilla::Search::Recent->create({
-                        user_id    => $self->id,
-                        bug_list   => $bug_ids,
-                        list_order => $order });
-                }
-                else {
-                    $search = $existing_search->[0];
-                }
-            }
-        };
-        delete $self->{recent_searches};
-    }
-    # Logged-out users use a cookie to store a single last search. We don't
-    # override that cookie with the logged-in user's latest search, because
-    # if they did one search while logged out and another while logged in,
-    # they may still want to navigate through the search they made while
-    # logged out.
-    else {
-        my $bug_list = join('-', @$bug_ids);
-        if (length($bug_list) < 4000) {
-            $cgi->send_cookie(-name => 'BUGLIST',
-                              -value => $bug_list,
-                              -expires => 'Fri, 01-Jan-2038 00:00:00 GMT');
+  return if !@$bug_ids;
+
+  my $search;
+  if ($self->id) {
+    on_main_db {
+      if ($list_id) {
+        $search = Bugzilla::Search::Recent->check_quietly({id => $list_id});
+      }
+
+      if ($search) {
+        if (join(',', @{$search->bug_list}) ne join(',', @$bug_ids)) {
+          $search->set_bug_list($bug_ids);
+        }
+        if (!$search->list_order || $order ne $search->list_order) {
+          $search->set_list_order($order);
+        }
+        $search->update();
+      }
+      else {
+        # If we already have an existing search with a totally
+        # identical bug list, then don't create a new one. This
+        # prevents people from writing over their whole
+        # recent-search list by just refreshing a saved search
+        # (which doesn't have list_id in the header) over and over.
+        my $list_string = join(',', @$bug_ids);
+        my $existing_search = Bugzilla::Search::Recent->match(
+          {user_id => $self->id, bug_list => $list_string});
+
+        if (!scalar(@$existing_search)) {
+          $search
+            = Bugzilla::Search::Recent->create({
+            user_id => $self->id, bug_list => $bug_ids, list_order => $order
+            });
         }
         else {
-            $cgi->remove_cookie('BUGLIST');
-            $vars->{'toolong'} = 1;
+          $search = $existing_search->[0];
         }
+      }
+    };
+    delete $self->{recent_searches};
+  }
+
+  # Logged-out users use a cookie to store a single last search. We don't
+  # override that cookie with the logged-in user's latest search, because
+  # if they did one search while logged out and another while logged in,
+  # they may still want to navigate through the search they made while
+  # logged out.
+  else {
+    my $bug_list = join('-', @$bug_ids);
+    if (length($bug_list) < 4000) {
+      $cgi->send_cookie(
+        -name    => 'BUGLIST',
+        -value   => $bug_list,
+        -expires => 'Fri, 01-Jan-2038 00:00:00 GMT'
+      );
     }
-    return $search;
+    else {
+      $cgi->remove_cookie('BUGLIST');
+      $vars->{'toolong'} = 1;
+    }
+  }
+  return $search;
 }
 
 sub reports {
-    my $self = shift;
-    return $self->{reports} if defined $self->{reports};
-    return [] unless $self->id;
+  my $self = shift;
+  return $self->{reports} if defined $self->{reports};
+  return [] unless $self->id;
 
-    my $dbh = Bugzilla->dbh;
-    my $report_ids = $dbh->selectcol_arrayref(
-        'SELECT id FROM reports WHERE user_id = ?', undef, $self->id);
-    require Bugzilla::Report;
-    $self->{reports} = Bugzilla::Report->new_from_list($report_ids);
-    return $self->{reports};
+  my $dbh = Bugzilla->dbh;
+  my $report_ids
+    = $dbh->selectcol_arrayref('SELECT id FROM reports WHERE user_id = ?',
+    undef, $self->id);
+  require Bugzilla::Report;
+  $self->{reports} = Bugzilla::Report->new_from_list($report_ids);
+  return $self->{reports};
 }
 
 sub flush_reports_cache {
-    my $self = shift;
+  my $self = shift;
 
-    delete $self->{reports};
+  delete $self->{reports};
 }
 
 sub settings {
-    my ($self) = @_;
+  my ($self) = @_;
 
-    return $self->{'settings'} if (defined $self->{'settings'});
+  return $self->{'settings'} if (defined $self->{'settings'});
 
-    # IF the user is logged in
-    # THEN get the user's settings
-    # ELSE get default settings
-    if ($self->id) {
-        $self->{'settings'} = get_all_settings($self->id);
-    } else {
-        $self->{'settings'} = get_defaults();
-    }
+  # IF the user is logged in
+  # THEN get the user's settings
+  # ELSE get default settings
+  if ($self->id) {
+    $self->{'settings'} = get_all_settings($self->id);
+  }
+  else {
+    $self->{'settings'} = get_defaults();
+  }
 
-    return $self->{'settings'};
+  return $self->{'settings'};
 }
 
 sub setting {
-    my ($self, $name) = @_;
-    return $self->settings->{$name}->{'value'};
+  my ($self, $name) = @_;
+  return $self->settings->{$name}->{'value'};
 }
 
 sub timezone {
-    my $self = shift;
+  my $self = shift;
 
-    if (!defined $self->{timezone}) {
-        my $tz = $self->setting('timezone');
-        if ($tz eq 'local') {
-            # The user wants the local timezone of the server.
-            $self->{timezone} = Bugzilla->local_timezone;
-        }
-        else {
-            $self->{timezone} = DateTime::TimeZone->new(name => $tz);
-        }
+  if (!defined $self->{timezone}) {
+    my $tz = $self->setting('timezone');
+    if ($tz eq 'local') {
+
+      # The user wants the local timezone of the server.
+      $self->{timezone} = Bugzilla->local_timezone;
     }
-    return $self->{timezone};
+    else {
+      $self->{timezone} = DateTime::TimeZone->new(name => $tz);
+    }
+  }
+  return $self->{timezone};
 }
 
 sub flush_queries_cache {
-    my $self = shift;
+  my $self = shift;
 
-    delete $self->{queries};
-    delete $self->{queries_subscribed};
-    delete $self->{queries_available};
+  delete $self->{queries};
+  delete $self->{queries_subscribed};
+  delete $self->{queries_available};
 }
 
 sub groups {
-    my $self = shift;
+  my $self = shift;
 
-    return $self->{groups} if defined $self->{groups};
-    return [] unless $self->id;
+  return $self->{groups} if defined $self->{groups};
+  return [] unless $self->id;
 
-    my $user_groups_key = "user_groups." . $self->id;
-    my $groups = Bugzilla->memcached->get_config({
-        key => $user_groups_key
-    });
+  my $user_groups_key = "user_groups." . $self->id;
+  my $groups = Bugzilla->memcached->get_config({key => $user_groups_key});
 
-    if (!$groups) {
-        my $dbh = Bugzilla->dbh;
-        my $groups_to_check = $dbh->selectcol_arrayref(
-            "SELECT DISTINCT group_id
+  if (!$groups) {
+    my $dbh             = Bugzilla->dbh;
+    my $groups_to_check = $dbh->selectcol_arrayref(
+      "SELECT DISTINCT group_id
                FROM user_group_map
-              WHERE user_id = ? AND isbless = 0", undef, $self->id);
-
-        my $grant_type_key = 'group_grant_type_' . GROUP_MEMBERSHIP;
-        my $membership_rows = Bugzilla->memcached->get_config({
-            key => $grant_type_key,
-        });
-        if (!$membership_rows) {
-            $membership_rows = $dbh->selectall_arrayref(
-                "SELECT DISTINCT grantor_id, member_id
+              WHERE user_id = ? AND isbless = 0", undef, $self->id
+    );
+
+    my $grant_type_key = 'group_grant_type_' . GROUP_MEMBERSHIP;
+    my $membership_rows
+      = Bugzilla->memcached->get_config({key => $grant_type_key,});
+    if (!$membership_rows) {
+      $membership_rows = $dbh->selectall_arrayref(
+        "SELECT DISTINCT grantor_id, member_id
                 FROM group_group_map
-                WHERE grant_type = " . GROUP_MEMBERSHIP);
-            Bugzilla->memcached->set_config({
-                key  => $grant_type_key,
-                data => $membership_rows,
-            });
-        }
+                WHERE grant_type = " . GROUP_MEMBERSHIP
+      );
+      Bugzilla->memcached->set_config({
+        key => $grant_type_key, data => $membership_rows,
+      });
+    }
 
-        my %group_membership;
-        foreach my $row (@$membership_rows) {
-            my ($grantor_id, $member_id) = @$row;
-            push (@{ $group_membership{$member_id} }, $grantor_id);
-        }
+    my %group_membership;
+    foreach my $row (@$membership_rows) {
+      my ($grantor_id, $member_id) = @$row;
+      push(@{$group_membership{$member_id}}, $grantor_id);
+    }
 
-        # Let's walk the groups hierarchy tree (using FIFO)
-        # On the first iteration it's pre-filled with direct groups
-        # membership. Later on, each group can add its own members into the
-        # FIFO. Circular dependencies are eliminated by checking
-        # $checked_groups{$member_id} hash values.
-        # As a result, %groups will have all the groups we are the member of.
-        my %checked_groups;
-        my %groups;
-        while (scalar(@$groups_to_check) > 0) {
-            # Pop the head group from FIFO
-            my $member_id = shift @$groups_to_check;
-
-            # Skip the group if we have already checked it
-            if (!$checked_groups{$member_id}) {
-                # Mark group as checked
-                $checked_groups{$member_id} = 1;
-
-                # Add all its members to the FIFO check list
-                # %group_membership contains arrays of group members
-                # for all groups. Accessible by group number.
-                my $members = $group_membership{$member_id};
-                my @new_to_check = grep(!$checked_groups{$_}, @$members);
-                push(@$groups_to_check, @new_to_check);
-
-                $groups{$member_id} = 1;
-            }
-        }
-        $groups = [ keys %groups ];
+    # Let's walk the groups hierarchy tree (using FIFO)
+    # On the first iteration it's pre-filled with direct groups
+    # membership. Later on, each group can add its own members into the
+    # FIFO. Circular dependencies are eliminated by checking
+    # $checked_groups{$member_id} hash values.
+    # As a result, %groups will have all the groups we are the member of.
+    my %checked_groups;
+    my %groups;
+    while (scalar(@$groups_to_check) > 0) {
+
+      # Pop the head group from FIFO
+      my $member_id = shift @$groups_to_check;
+
+      # Skip the group if we have already checked it
+      if (!$checked_groups{$member_id}) {
+
+        # Mark group as checked
+        $checked_groups{$member_id} = 1;
+
+        # Add all its members to the FIFO check list
+        # %group_membership contains arrays of group members
+        # for all groups. Accessible by group number.
+        my $members = $group_membership{$member_id};
+        my @new_to_check = grep(!$checked_groups{$_}, @$members);
+        push(@$groups_to_check, @new_to_check);
 
-        Bugzilla->memcached->set_config({
-            key  => $user_groups_key,
-            data => $groups,
-        });
+        $groups{$member_id} = 1;
+      }
     }
+    $groups = [keys %groups];
 
-    $self->{groups} = Bugzilla::Group->new_from_list($groups);
-    return $self->{groups};
+    Bugzilla->memcached->set_config({key => $user_groups_key, data => $groups,});
+  }
+
+  $self->{groups} = Bugzilla::Group->new_from_list($groups);
+  return $self->{groups};
 }
 
 sub last_visited {
-    my ($self, $ids) = @_;
+  my ($self, $ids) = @_;
 
-    return Bugzilla::BugUserLastVisit->match({ user_id => $self->id,
-                                               $ids ? ( bug_id => $ids ) : () });
+  return Bugzilla::BugUserLastVisit->match({
+    user_id => $self->id, $ids ? (bug_id => $ids) : ()
+  });
 }
 
 sub is_involved_in_bug {
-    my ($self, $bug) = @_;
-    my $user_id    = $self->id;
-    my $user_login = $self->login;
+  my ($self, $bug) = @_;
+  my $user_id    = $self->id;
+  my $user_login = $self->login;
 
-    return unless $user_id;
-    return 1 if $user_id == $bug->assigned_to->id;
-    return 1 if $user_id == $bug->reporter->id;
+  return unless $user_id;
+  return 1 if $user_id == $bug->assigned_to->id;
+  return 1 if $user_id == $bug->reporter->id;
 
-    if (Bugzilla->params->{'useqacontact'} and $bug->qa_contact) {
-        return 1 if $user_id == $bug->qa_contact->id;
-    }
+  if (Bugzilla->params->{'useqacontact'} and $bug->qa_contact) {
+    return 1 if $user_id == $bug->qa_contact->id;
+  }
 
-    return any { $user_login eq $_ } @{ $bug->cc };
+  return any { $user_login eq $_ } @{$bug->cc};
 }
 
 # It turns out that calling ->id on objects a few hundred thousand
@@ -949,296 +965,311 @@ sub is_involved_in_bug {
 # when profiling xt/search.t.) So we cache the group ids separately from
 # groups for functions that need the group ids.
 sub _group_ids {
-    my ($self) = @_;
-    $self->{group_ids} ||= [map { $_->id } @{ $self->groups }];
-    return $self->{group_ids};
+  my ($self) = @_;
+  $self->{group_ids} ||= [map { $_->id } @{$self->groups}];
+  return $self->{group_ids};
 }
 
 sub groups_as_string {
-    my $self = shift;
-    my $ids = $self->_group_ids;
-    return scalar(@$ids) ? join(',', @$ids) : '-1';
+  my $self = shift;
+  my $ids  = $self->_group_ids;
+  return scalar(@$ids) ? join(',', @$ids) : '-1';
 }
 
 sub groups_in_sql {
-    my ($self, $field) = @_;
-    $field ||= 'group_id';
-    my $ids = $self->_group_ids;
-    $ids = [-1] if !scalar @$ids;
-    return Bugzilla->dbh->sql_in($field, $ids);
+  my ($self, $field) = @_;
+  $field ||= 'group_id';
+  my $ids = $self->_group_ids;
+  $ids = [-1] if !scalar @$ids;
+  return Bugzilla->dbh->sql_in($field, $ids);
 }
 
 sub bless_groups {
-    my $self = shift;
+  my $self = shift;
 
-    return $self->{'bless_groups'} if defined $self->{'bless_groups'};
-    return [] unless $self->id;
+  return $self->{'bless_groups'} if defined $self->{'bless_groups'};
+  return [] unless $self->id;
 
-    if ($self->in_group('editusers')) {
-        # Users having editusers permissions may bless all groups.
-        $self->{'bless_groups'} = [Bugzilla::Group->get_all];
-        return $self->{'bless_groups'};
-    }
+  if ($self->in_group('editusers')) {
 
-    if (Bugzilla->params->{usevisibilitygroups}
-        && !@{ $self->visible_groups_inherited }) {
-        return [];
-    }
+    # Users having editusers permissions may bless all groups.
+    $self->{'bless_groups'} = [Bugzilla::Group->get_all];
+    return $self->{'bless_groups'};
+  }
+
+  if (Bugzilla->params->{usevisibilitygroups}
+    && !@{$self->visible_groups_inherited})
+  {
+    return [];
+  }
 
-    my $dbh = Bugzilla->dbh;
+  my $dbh = Bugzilla->dbh;
 
-    # Get all groups for the user where they have direct bless privileges.
-    my $query = "
+  # Get all groups for the user where they have direct bless privileges.
+  my $query = "
         SELECT DISTINCT group_id
           FROM user_group_map
          WHERE user_id = ?
                AND isbless = 1";
-    if (Bugzilla->params->{usevisibilitygroups}) {
-        $query .= " AND "
-            . $dbh->sql_in('group_id', $self->visible_groups_inherited);
-    }
-
-    # Get all groups for the user where they are a member of a group that
-    # inherits bless privs.
-    my @group_ids = map { $_->id } @{ $self->groups };
-    if (@group_ids) {
-        $query .= "
+  if (Bugzilla->params->{usevisibilitygroups}) {
+    $query .= " AND " . $dbh->sql_in('group_id', $self->visible_groups_inherited);
+  }
+
+  # Get all groups for the user where they are a member of a group that
+  # inherits bless privs.
+  my @group_ids = map { $_->id } @{$self->groups};
+  if (@group_ids) {
+    $query .= "
             UNION
             SELECT DISTINCT grantor_id
             FROM group_group_map
             WHERE grant_type = " . GROUP_BLESS . "
                 AND " . $dbh->sql_in('member_id', \@group_ids);
-        if (Bugzilla->params->{usevisibilitygroups}) {
-            $query .= " AND "
-                . $dbh->sql_in('grantor_id', $self->visible_groups_inherited);
-        }
+    if (Bugzilla->params->{usevisibilitygroups}) {
+      $query .= " AND " . $dbh->sql_in('grantor_id', $self->visible_groups_inherited);
     }
+  }
 
-    my $ids = $dbh->selectcol_arrayref($query, undef, $self->id);
-    return $self->{bless_groups} = Bugzilla::Group->new_from_list($ids);
+  my $ids = $dbh->selectcol_arrayref($query, undef, $self->id);
+  return $self->{bless_groups} = Bugzilla::Group->new_from_list($ids);
 }
 
 sub in_group {
-    my ($self, $group, $product_id) = @_;
-    $group = $group->name if blessed $group;
-    if (scalar grep($_->name eq $group, @{ $self->groups })) {
-        return 1;
-    }
-    elsif ($product_id && detaint_natural($product_id)) {
-        # Make sure $group exists on a per-product basis.
-        return 0 unless (grep {$_ eq $group} PER_PRODUCT_PRIVILEGES);
-
-        $self->{"product_$product_id"} = {} unless exists $self->{"product_$product_id"};
-        if (!defined $self->{"product_$product_id"}->{$group}) {
-            my $dbh = Bugzilla->dbh;
-            my $in_group = $dbh->selectrow_array(
-                           "SELECT 1
+  my ($self, $group, $product_id) = @_;
+  $group = $group->name if blessed $group;
+  if (scalar grep($_->name eq $group, @{$self->groups})) {
+    return 1;
+  }
+  elsif ($product_id && detaint_natural($product_id)) {
+
+    # Make sure $group exists on a per-product basis.
+    return 0 unless (grep { $_ eq $group } PER_PRODUCT_PRIVILEGES);
+
+    $self->{"product_$product_id"} = {}
+      unless exists $self->{"product_$product_id"};
+    if (!defined $self->{"product_$product_id"}->{$group}) {
+      my $dbh      = Bugzilla->dbh;
+      my $in_group = $dbh->selectrow_array(
+        "SELECT 1
                               FROM group_control_map
                              WHERE product_id = ?
                                    AND $group != 0
-                                   AND " . $self->groups_in_sql . ' ' .
-                              $dbh->sql_limit(1),
-                             undef, $product_id);
+                                   AND "
+          . $self->groups_in_sql . ' ' . $dbh->sql_limit(1), undef, $product_id
+      );
 
-            $self->{"product_$product_id"}->{$group} = $in_group ? 1 : 0;
-        }
-        return $self->{"product_$product_id"}->{$group};
+      $self->{"product_$product_id"}->{$group} = $in_group ? 1 : 0;
     }
-    # If we come here, then the user is not in the requested group.
-    return 0;
+    return $self->{"product_$product_id"}->{$group};
+  }
+
+  # If we come here, then the user is not in the requested group.
+  return 0;
 }
 
 sub in_group_id {
-    my ($self, $id) = @_;
-    return grep($_->id == $id, @{ $self->groups }) ? 1 : 0;
+  my ($self, $id) = @_;
+  return grep($_->id == $id, @{$self->groups}) ? 1 : 0;
 }
 
 # This is a helper to get all groups which have an icon to be displayed
 # besides the name of the commenter.
 sub groups_with_icon {
-    my $self = shift;
+  my $self = shift;
 
-    return $self->{groups_with_icon} //= [grep { $_->icon_url } @{ $self->groups }];
+  return $self->{groups_with_icon} //= [grep { $_->icon_url } @{$self->groups}];
 }
 
 sub get_products_by_permission {
-    my ($self, $group) = @_;
-    # Make sure $group exists on a per-product basis.
-    return [] unless (grep {$_ eq $group} PER_PRODUCT_PRIVILEGES);
+  my ($self, $group) = @_;
+
+  # Make sure $group exists on a per-product basis.
+  return [] unless (grep { $_ eq $group } PER_PRODUCT_PRIVILEGES);
 
-    my $product_ids = Bugzilla->dbh->selectcol_arrayref(
-                          "SELECT DISTINCT product_id
+  my $product_ids = Bugzilla->dbh->selectcol_arrayref(
+    "SELECT DISTINCT product_id
                              FROM group_control_map
                             WHERE $group != 0
-                              AND " . $self->groups_in_sql);
+                              AND " . $self->groups_in_sql
+  );
 
-    # No need to go further if the user has no "special" privs.
-    return [] unless scalar(@$product_ids);
-    my %product_map = map { $_ => 1 } @$product_ids;
+  # No need to go further if the user has no "special" privs.
+  return [] unless scalar(@$product_ids);
+  my %product_map = map { $_ => 1 } @$product_ids;
 
-    # We will restrict the list to products the user can see.
-    my $selectable_products = $self->get_selectable_products;
-    my @products = grep { $product_map{$_->id} } @$selectable_products;
-    return \@products;
+  # We will restrict the list to products the user can see.
+  my $selectable_products = $self->get_selectable_products;
+  my @products = grep { $product_map{$_->id} } @$selectable_products;
+  return \@products;
 }
 
 sub can_see_user {
-    my ($self, $otherUser) = @_;
-    my $query;
+  my ($self, $otherUser) = @_;
+  my $query;
 
-    if (Bugzilla->params->{'usevisibilitygroups'}) {
-        # If the user can see no groups, then no users are visible either.
-        my $visibleGroups = $self->visible_groups_as_string() || return 0;
-        $query = qq{SELECT COUNT(DISTINCT userid)
+  if (Bugzilla->params->{'usevisibilitygroups'}) {
+
+    # If the user can see no groups, then no users are visible either.
+    my $visibleGroups = $self->visible_groups_as_string() || return 0;
+    $query = qq{SELECT COUNT(DISTINCT userid)
                     FROM profiles, user_group_map
                     WHERE userid = ?
                     AND user_id = userid
                     AND isbless = 0
                     AND group_id IN ($visibleGroups)
                    };
-    } else {
-        $query = qq{SELECT COUNT(userid)
+  }
+  else {
+    $query = qq{SELECT COUNT(userid)
                     FROM profiles
                     WHERE userid = ?
                    };
-    }
-    return Bugzilla->dbh->selectrow_array($query, undef, $otherUser->id);
+  }
+  return Bugzilla->dbh->selectrow_array($query, undef, $otherUser->id);
 }
 
 sub can_edit_product {
-    my ($self, $prod_id) = @_;
-    my $dbh = Bugzilla->dbh;
+  my ($self, $prod_id) = @_;
+  my $dbh = Bugzilla->dbh;
 
-    if (Bugzilla->params->{'or_groups'}) {
-        my $groups = $self->groups_as_string;
-        # For or-groups, we check if there are any can_edit groups for the
-        # product, and if the user is in any of them. If there are none or
-        # the user is in at least one of them, they can edit the product
-        my ($cnt_can_edit, $cnt_group_member) = $dbh->selectrow_array(
-           "SELECT SUM(p.cnt_can_edit),
+  if (Bugzilla->params->{'or_groups'}) {
+    my $groups = $self->groups_as_string;
+
+    # For or-groups, we check if there are any can_edit groups for the
+    # product, and if the user is in any of them. If there are none or
+    # the user is in at least one of them, they can edit the product
+    my ($cnt_can_edit, $cnt_group_member) = $dbh->selectrow_array(
+      "SELECT SUM(p.cnt_can_edit),
                    SUM(p.cnt_group_member)
               FROM (SELECT CASE WHEN canedit = 1 THEN 1 ELSE 0 END AS cnt_can_edit,
                            CASE WHEN canedit = 1 AND group_id IN ($groups) THEN 1 ELSE 0 END AS cnt_group_member
                     FROM group_control_map
-                    WHERE product_id = $prod_id) AS p");
-        return (!$cnt_can_edit or $cnt_group_member);
-    }
-    else {
-        # For and-groups, a user needs to be in all canedit groups. Therefore
-        # if the user is not in a can_edit group for the product, they cannot
-        # edit the product.
-        my $has_external_groups =
-          $dbh->selectrow_array('SELECT 1
+                    WHERE product_id = $prod_id) AS p"
+    );
+    return (!$cnt_can_edit or $cnt_group_member);
+  }
+  else {
+    # For and-groups, a user needs to be in all canedit groups. Therefore
+    # if the user is not in a can_edit group for the product, they cannot
+    # edit the product.
+    my $has_external_groups = $dbh->selectrow_array(
+      'SELECT 1
                                    FROM group_control_map
                                   WHERE product_id = ?
                                     AND canedit != 0
-                                    AND group_id NOT IN(' . $self->groups_as_string . ')',
-                                 undef, $prod_id);
+                                    AND group_id NOT IN('
+        . $self->groups_as_string . ')', undef, $prod_id
+    );
 
-        return !$has_external_groups;
-    }
+    return !$has_external_groups;
+  }
 }
 
 sub can_see_bug {
-    my ($self, $bug_id) = @_;
-    return @{ $self->visible_bugs([$bug_id]) } ? 1 : 0;
+  my ($self, $bug_id) = @_;
+  return @{$self->visible_bugs([$bug_id])} ? 1 : 0;
 }
 
 sub visible_bugs {
-    my ($self, $bugs) = @_;
-    # Allow users to pass in Bug objects and bug ids both.
-    my @bug_ids = map { blessed $_ ? $_->id : $_ } @$bugs;
-
-    # We only check the visibility of bugs that we haven't
-    # checked yet.
-    # Bugzilla::Bug->update automatically removes updated bugs
-    # from the cache to force them to be checked again.
-    my $visible_cache = $self->{_visible_bugs_cache} ||= {};
-    my @check_ids = grep(!exists $visible_cache->{$_}, @bug_ids);
-
-    if (@check_ids) {
-        foreach my $id (@check_ids) {
-            my $orig_id = $id;
-            detaint_natural($id)
-              || ThrowCodeError('param_must_be_numeric', { param    => $orig_id,
-                                                           function => 'Bugzilla::User->visible_bugs'});
-        }
+  my ($self, $bugs) = @_;
+
+  # Allow users to pass in Bug objects and bug ids both.
+  my @bug_ids = map { blessed $_ ? $_->id : $_ } @$bugs;
+
+  # We only check the visibility of bugs that we haven't
+  # checked yet.
+  # Bugzilla::Bug->update automatically removes updated bugs
+  # from the cache to force them to be checked again.
+  my $visible_cache = $self->{_visible_bugs_cache} ||= {};
+  my @check_ids = grep(!exists $visible_cache->{$_}, @bug_ids);
 
-        Bugzilla->params->{'or_groups'}
-            ? $self->_visible_bugs_check_or(\@check_ids)
-            : $self->_visible_bugs_check_and(\@check_ids);
+  if (@check_ids) {
+    foreach my $id (@check_ids) {
+      my $orig_id = $id;
+      detaint_natural($id)
+        || ThrowCodeError('param_must_be_numeric',
+        {param => $orig_id, function => 'Bugzilla::User->visible_bugs'});
     }
 
-    return [grep { $visible_cache->{blessed $_ ? $_->id : $_} } @$bugs];
+    Bugzilla->params->{'or_groups'}
+      ? $self->_visible_bugs_check_or(\@check_ids)
+      : $self->_visible_bugs_check_and(\@check_ids);
+  }
+
+  return [grep { $visible_cache->{blessed $_ ? $_->id : $_} } @$bugs];
 }
 
 sub _visible_bugs_check_or {
-    my ($self, $check_ids) = @_;
-    my $visible_cache = $self->{_visible_bugs_cache};
-    my $dbh = Bugzilla->dbh;
-    my $user_id = $self->id;
-
-    my $sth;
-    # Speed up the can_see_bug case.
-    if (scalar(@$check_ids) == 1) {
-        $sth = $self->{_sth_one_visible_bug};
-    }
-    my $query = qq{
+  my ($self, $check_ids) = @_;
+  my $visible_cache = $self->{_visible_bugs_cache};
+  my $dbh           = Bugzilla->dbh;
+  my $user_id       = $self->id;
+
+  my $sth;
+
+  # Speed up the can_see_bug case.
+  if (scalar(@$check_ids) == 1) {
+    $sth = $self->{_sth_one_visible_bug};
+  }
+  my $query = qq{
         SELECT DISTINCT bugs.bug_id
         FROM bugs
             LEFT JOIN bug_group_map AS security_map ON bugs.bug_id = security_map.bug_id
             LEFT JOIN cc AS security_cc ON bugs.bug_id = security_cc.bug_id AND security_cc.who = $user_id
         WHERE bugs.bug_id IN (} . join(',', ('?') x @$check_ids) . qq{)
-          AND ((security_map.group_id IS NULL OR security_map.group_id IN (} . $self->groups_as_string . qq{))
+          AND ((security_map.group_id IS NULL OR security_map.group_id IN (}
+    . $self->groups_as_string . qq{))
             OR (bugs.reporter_accessible = 1 AND bugs.reporter = $user_id)
             OR (bugs.cclist_accessible = 1 AND security_cc.who IS NOT NULL)
             OR bugs.assigned_to = $user_id
     };
 
-    if (Bugzilla->params->{'useqacontact'}) {
-        $query .= " OR bugs.qa_contact = $user_id";
-    }
-    $query .= ')';
+  if (Bugzilla->params->{'useqacontact'}) {
+    $query .= " OR bugs.qa_contact = $user_id";
+  }
+  $query .= ')';
 
-    $sth ||= $dbh->prepare($query);
-    if (scalar(@$check_ids) == 1) {
-        $self->{_sth_one_visible_bug} = $sth;
-    }
+  $sth ||= $dbh->prepare($query);
+  if (scalar(@$check_ids) == 1) {
+    $self->{_sth_one_visible_bug} = $sth;
+  }
 
-    # Set all bugs as non visible
-    foreach my $bug_id (@$check_ids) {
-        $visible_cache->{$bug_id} = 0;
-    }
+  # Set all bugs as non visible
+  foreach my $bug_id (@$check_ids) {
+    $visible_cache->{$bug_id} = 0;
+  }
 
-    # Now get the bugs the user can see
-    my $visible_bug_ids = $dbh->selectcol_arrayref($sth, undef, @$check_ids);
-    foreach my $bug_id (@$visible_bug_ids) {
-        $visible_cache->{$bug_id} = 1;
-    }
+  # Now get the bugs the user can see
+  my $visible_bug_ids = $dbh->selectcol_arrayref($sth, undef, @$check_ids);
+  foreach my $bug_id (@$visible_bug_ids) {
+    $visible_cache->{$bug_id} = 1;
+  }
 }
 
 sub _visible_bugs_check_and {
-    my ($self, $check_ids) = @_;
-    my $visible_cache = $self->{_visible_bugs_cache};
-    my $dbh = Bugzilla->dbh;
-    my $user_id = $self->id;
-
-    my $sth;
-    # Speed up the can_see_bug case.
-    if (scalar(@$check_ids) == 1) {
-        $sth = $self->{_sth_one_visible_bug};
-    }
-    $sth ||= $dbh->prepare(
-        # This checks for groups that the bug is in that the user
-        # *isn't* in. Then, in the Perl code below, we check if
-        # the user can otherwise access the bug (for example, by being
-        # the assignee or QA Contact).
-        #
-        # The DISTINCT exists because the bug could be in *several*
-        # groups that the user isn't in, but they will all return the
-        # same result for bug_group_map.bug_id (so DISTINCT filters
-        # out duplicate rows).
-        "SELECT DISTINCT bugs.bug_id, reporter, assigned_to, qa_contact,
+  my ($self, $check_ids) = @_;
+  my $visible_cache = $self->{_visible_bugs_cache};
+  my $dbh           = Bugzilla->dbh;
+  my $user_id       = $self->id;
+
+  my $sth;
+
+  # Speed up the can_see_bug case.
+  if (scalar(@$check_ids) == 1) {
+    $sth = $self->{_sth_one_visible_bug};
+  }
+  $sth ||= $dbh->prepare(
+
+    # This checks for groups that the bug is in that the user
+    # *isn't* in. Then, in the Perl code below, we check if
+    # the user can otherwise access the bug (for example, by being
+    # the assignee or QA Contact).
+    #
+    # The DISTINCT exists because the bug could be in *several*
+    # groups that the user isn't in, but they will all return the
+    # same result for bug_group_map.bug_id (so DISTINCT filters
+    # out duplicate rows).
+    "SELECT DISTINCT bugs.bug_id, reporter, assigned_to, qa_contact,
                 reporter_accessible, cclist_accessible, cc.who,
                 bug_group_map.bug_id
            FROM bugs
@@ -1248,1056 +1279,1124 @@ sub _visible_bugs_check_and {
                 LEFT JOIN bug_group_map 
                           ON bugs.bug_id = bug_group_map.bug_id
                              AND bug_group_map.group_id NOT IN ("
-                                 . $self->groups_as_string . ')
+      . $self->groups_as_string . ')
           WHERE bugs.bug_id IN (' . join(',', ('?') x @$check_ids) . ')
-                AND creation_ts IS NOT NULL ');
-    if (scalar(@$check_ids) == 1) {
-        $self->{_sth_one_visible_bug} = $sth;
-    }
-
-    $sth->execute(@$check_ids);
-    my $use_qa_contact = Bugzilla->params->{'useqacontact'};
-    while (my $row = $sth->fetchrow_arrayref) {
-        my ($bug_id, $reporter, $owner, $qacontact, $reporter_access, 
-            $cclist_access, $isoncclist, $missinggroup) = @$row;
-        $visible_cache->{$bug_id} ||= 
-            ((($reporter == $user_id) && $reporter_access)
-             || ($use_qa_contact
-                 && $qacontact && ($qacontact == $user_id))
-             || ($owner == $user_id)
-             || ($isoncclist && $cclist_access)
-             || !$missinggroup) ? 1 : 0;
-    }
+                AND creation_ts IS NOT NULL '
+  );
+  if (scalar(@$check_ids) == 1) {
+    $self->{_sth_one_visible_bug} = $sth;
+  }
+
+  $sth->execute(@$check_ids);
+  my $use_qa_contact = Bugzilla->params->{'useqacontact'};
+  while (my $row = $sth->fetchrow_arrayref) {
+    my ($bug_id, $reporter, $owner, $qacontact, $reporter_access, $cclist_access,
+      $isoncclist, $missinggroup)
+      = @$row;
+    $visible_cache->{$bug_id}
+      ||= ((($reporter == $user_id) && $reporter_access)
+        || ($use_qa_contact && $qacontact && ($qacontact == $user_id))
+        || ($owner == $user_id)
+        || ($isoncclist && $cclist_access)
+        || !$missinggroup) ? 1 : 0;
+  }
 
 }
 
 sub clear_product_cache {
-    my $self = shift;
-    delete $self->{enterable_products};
-    delete $self->{selectable_products};
-    delete $self->{selectable_classifications};
+  my $self = shift;
+  delete $self->{enterable_products};
+  delete $self->{selectable_products};
+  delete $self->{selectable_classifications};
 }
 
 sub can_see_product {
-    my ($self, $product_name) = @_;
+  my ($self, $product_name) = @_;
 
-    return scalar(grep {$_->name eq $product_name} @{$self->get_selectable_products});
+  return
+    scalar(grep { $_->name eq $product_name } @{$self->get_selectable_products});
 }
 
 sub get_selectable_products {
-    my $self = shift;
-    my $class_id = shift;
-    my $class_restricted = Bugzilla->params->{'useclassification'} && $class_id;
+  my $self             = shift;
+  my $class_id         = shift;
+  my $class_restricted = Bugzilla->params->{'useclassification'} && $class_id;
 
-    if (!defined $self->{selectable_products}) {
-        my $query = "SELECT id
+  if (!defined $self->{selectable_products}) {
+    my $query = "SELECT id
                      FROM products
                          LEFT JOIN group_control_map
                              ON group_control_map.product_id = products.id
-                             AND group_control_map.membercontrol = " . CONTROLMAPMANDATORY;
-                             
-        if (Bugzilla->params->{'or_groups'}) {
-            # Either the user is in at least one of the MANDATORY groups, or
-            # there are no such groups for the product.
-            $query .= " WHERE group_id IN (" . $self->groups_as_string . ")
+                             AND group_control_map.membercontrol = "
+      . CONTROLMAPMANDATORY;
+
+    if (Bugzilla->params->{'or_groups'}) {
+
+      # Either the user is in at least one of the MANDATORY groups, or
+      # there are no such groups for the product.
+      $query .= " WHERE group_id IN (" . $self->groups_as_string . ")
                         OR group_id IS NULL";
-        }
-        else {
-            # There must be no MANDATORY groups that the user is not in.
-            $query .= " AND group_id NOT IN (" . $self->groups_as_string . ")
+    }
+    else {
+      # There must be no MANDATORY groups that the user is not in.
+      $query .= " AND group_id NOT IN (" . $self->groups_as_string . ")
                         WHERE group_id IS NULL";
-        }
-        
-        my $prod_ids = Bugzilla->dbh->selectcol_arrayref($query);
-        $self->{selectable_products} = Bugzilla::Product->new_from_list($prod_ids);
     }
 
-    # Restrict the list of products to those being in the classification, if any.
-    if ($class_restricted) {
-        return [grep {$_->classification_id == $class_id} @{$self->{selectable_products}}];
-    }
-    # If we come here, then we want all selectable products.
-    return $self->{selectable_products};
+    my $prod_ids = Bugzilla->dbh->selectcol_arrayref($query);
+    $self->{selectable_products} = Bugzilla::Product->new_from_list($prod_ids);
+  }
+
+  # Restrict the list of products to those being in the classification, if any.
+  if ($class_restricted) {
+    return [grep { $_->classification_id == $class_id }
+        @{$self->{selectable_products}}];
+  }
+
+  # If we come here, then we want all selectable products.
+  return $self->{selectable_products};
 }
 
 sub get_selectable_classifications {
-    my ($self) = @_;
+  my ($self) = @_;
 
-    if (!defined $self->{selectable_classifications}) {
-        my $products = $self->get_selectable_products;
-        my %class_ids = map { $_->classification_id => 1 } @$products;
+  if (!defined $self->{selectable_classifications}) {
+    my $products = $self->get_selectable_products;
+    my %class_ids = map { $_->classification_id => 1 } @$products;
 
-        $self->{selectable_classifications} = Bugzilla::Classification->new_from_list([keys %class_ids]);
-    }
-    return $self->{selectable_classifications};
+    $self->{selectable_classifications}
+      = Bugzilla::Classification->new_from_list([keys %class_ids]);
+  }
+  return $self->{selectable_classifications};
 }
 
 sub can_enter_product {
-    my ($self, $input, $warn) = @_;
-    my $dbh = Bugzilla->dbh;
-    $warn ||= 0;
-
-    $input = trim($input) if !ref $input;
-    if (!defined $input or $input eq '') {
-        return unless $warn == THROW_ERROR;
-        ThrowUserError('object_not_specified',
-                       { class => 'Bugzilla::Product' });
-    }
+  my ($self, $input, $warn) = @_;
+  my $dbh = Bugzilla->dbh;
+  $warn ||= 0;
 
-    if (!scalar @{ $self->get_enterable_products }) {
-        return unless $warn == THROW_ERROR;
-        ThrowUserError('no_products');
-    }
+  $input = trim($input) if !ref $input;
+  if (!defined $input or $input eq '') {
+    return unless $warn == THROW_ERROR;
+    ThrowUserError('object_not_specified', {class => 'Bugzilla::Product'});
+  }
 
-    my $product = blessed($input) ? $input 
-                                  : new Bugzilla::Product({ name => $input });
-    my $can_enter =
-      $product && grep($_->name eq $product->name,
-                       @{ $self->get_enterable_products });
+  if (!scalar @{$self->get_enterable_products}) {
+    return unless $warn == THROW_ERROR;
+    ThrowUserError('no_products');
+  }
 
-    return $product if $can_enter;
+  my $product
+    = blessed($input) ? $input : new Bugzilla::Product({name => $input});
+  my $can_enter = $product
+    && grep($_->name eq $product->name, @{$self->get_enterable_products});
 
-    return 0 unless $warn == THROW_ERROR;
+  return $product if $can_enter;
 
-    # Check why access was denied. These checks are slow,
-    # but that's fine, because they only happen if we fail.
+  return 0 unless $warn == THROW_ERROR;
 
-    # We don't just use $product->name for error messages, because if it
-    # changes case from $input, then that's a clue that the product does
-    # exist but is hidden.
-    my $name = blessed($input) ? $input->name : $input;
+  # Check why access was denied. These checks are slow,
+  # but that's fine, because they only happen if we fail.
 
-    # The product could not exist or you could be denied...
-    if (!$product || !$product->user_has_access($self)) {
-        ThrowUserError('entry_access_denied', { product => $name });
-    }
-    # It could be closed for bug entry...
-    elsif (!$product->is_active) {
-        ThrowUserError('product_disabled', { product => $product });
-    }
-    # It could have no components...
-    elsif (!@{$product->components}
-           || !grep { $_->is_active } @{$product->components})
-    {
-        ThrowUserError('missing_component', { product => $product });
-    }
-    # It could have no versions...
-    elsif (!@{$product->versions}
-           || !grep { $_->is_active } @{$product->versions})
-    {
-        ThrowUserError ('missing_version', { product => $product });
-    }
+  # We don't just use $product->name for error messages, because if it
+  # changes case from $input, then that's a clue that the product does
+  # exist but is hidden.
+  my $name = blessed($input) ? $input->name : $input;
+
+  # The product could not exist or you could be denied...
+  if (!$product || !$product->user_has_access($self)) {
+    ThrowUserError('entry_access_denied', {product => $name});
+  }
+
+  # It could be closed for bug entry...
+  elsif (!$product->is_active) {
+    ThrowUserError('product_disabled', {product => $product});
+  }
 
-    die "can_enter_product reached an unreachable location.";
+  # It could have no components...
+  elsif (!@{$product->components}
+    || !grep { $_->is_active } @{$product->components})
+  {
+    ThrowUserError('missing_component', {product => $product});
+  }
+
+  # It could have no versions...
+  elsif (!@{$product->versions} || !grep { $_->is_active } @{$product->versions})
+  {
+    ThrowUserError('missing_version', {product => $product});
+  }
+
+  die "can_enter_product reached an unreachable location.";
 }
 
 sub get_enterable_products {
-    my $self = shift;
-    my $dbh = Bugzilla->dbh;
+  my $self = shift;
+  my $dbh  = Bugzilla->dbh;
 
-    if (defined $self->{enterable_products}) {
-        return $self->{enterable_products};
-    }
+  if (defined $self->{enterable_products}) {
+    return $self->{enterable_products};
+  }
 
-     # All products which the user has "Entry" access to.
-     my $query =
-           'SELECT products.id FROM products
+  # All products which the user has "Entry" access to.
+  my $query = 'SELECT products.id FROM products
             LEFT JOIN group_control_map
                 ON group_control_map.product_id = products.id
                     AND group_control_map.entry != 0';
 
-    if (Bugzilla->params->{'or_groups'}) {
-        $query .= " WHERE (group_id IN (" . $self->groups_as_string . ")" .
-                  "    OR group_id IS NULL)";
-    } else {
-        $query .= " AND group_id NOT IN (" . $self->groups_as_string . ")" .
-                  " WHERE group_id IS NULL"
-    }
-    $query .= " AND products.isactive = 1";
-    my $enterable_ids = $dbh->selectcol_arrayref($query);
-
-    if (scalar @$enterable_ids) {
-        # And all of these products must have at least one component
-        # and one version.
-        $enterable_ids = $dbh->selectcol_arrayref(
-            'SELECT DISTINCT products.id FROM products
-              WHERE ' . $dbh->sql_in('products.id', $enterable_ids) .
-              ' AND products.id IN (SELECT DISTINCT components.product_id
+  if (Bugzilla->params->{'or_groups'}) {
+    $query
+      .= " WHERE (group_id IN ("
+      . $self->groups_as_string . ")"
+      . "    OR group_id IS NULL)";
+  }
+  else {
+    $query
+      .= " AND group_id NOT IN ("
+      . $self->groups_as_string . ")"
+      . " WHERE group_id IS NULL";
+  }
+  $query .= " AND products.isactive = 1";
+  my $enterable_ids = $dbh->selectcol_arrayref($query);
+
+  if (scalar @$enterable_ids) {
+
+    # And all of these products must have at least one component
+    # and one version.
+    $enterable_ids = $dbh->selectcol_arrayref(
+      'SELECT DISTINCT products.id FROM products
+              WHERE '
+        . $dbh->sql_in('products.id', $enterable_ids)
+        . ' AND products.id IN (SELECT DISTINCT components.product_id
                                       FROM components
                                      WHERE components.isactive = 1)
                 AND products.id IN (SELECT DISTINCT versions.product_id
                                       FROM versions
-                                     WHERE versions.isactive = 1)');
-    }
+                                     WHERE versions.isactive = 1)'
+    );
+  }
 
-    $self->{enterable_products} =
-         Bugzilla::Product->new_from_list($enterable_ids);
-    return $self->{enterable_products};
+  $self->{enterable_products} = Bugzilla::Product->new_from_list($enterable_ids);
+  return $self->{enterable_products};
 }
 
 sub can_access_product {
-    my ($self, $product) = @_;
-    my $product_name = blessed($product) ? $product->name : $product;
-    return scalar(grep {$_->name eq $product_name} @{$self->get_accessible_products});
+  my ($self, $product) = @_;
+  my $product_name = blessed($product) ? $product->name : $product;
+  return
+    scalar(grep { $_->name eq $product_name } @{$self->get_accessible_products});
 }
 
 sub get_accessible_products {
-    my $self = shift;
-    
-    # Map the objects into a hash using the ids as keys
-    my %products = map { $_->id => $_ }
-                       @{$self->get_selectable_products},
-                       @{$self->get_enterable_products};
-    
-    return [ sort { $a->name cmp $b->name } values %products ];
+  my $self = shift;
+
+  # Map the objects into a hash using the ids as keys
+  my %products = map { $_->id => $_ } @{$self->get_selectable_products},
+    @{$self->get_enterable_products};
+
+  return [sort { $a->name cmp $b->name } values %products];
 }
 
 sub can_administer {
-    my $self = shift;
-
-    if (not defined $self->{can_administer}) {
-        my $can_administer = 0;
-
-        $can_administer = 1 if $self->in_group('admin')
-            || $self->in_group('tweakparams')
-            || $self->in_group('editusers')
-            || $self->can_bless
-            || (Bugzilla->params->{'useclassification'} && $self->in_group('editclassifications'))
-            || $self->in_group('editcomponents')
-            || scalar(@{$self->get_products_by_permission('editcomponents')})
-            || $self->in_group('creategroups')
-            || $self->in_group('editkeywords')
-            || $self->in_group('bz_canusewhines');
-
-        Bugzilla::Hook::process('user_can_administer', { can_administer => \$can_administer });
-        $self->{can_administer} = $can_administer;
-    }
+  my $self = shift;
+
+  if (not defined $self->{can_administer}) {
+    my $can_administer = 0;
 
-    return $self->{can_administer};
+    $can_administer = 1
+      if $self->in_group('admin')
+      || $self->in_group('tweakparams')
+      || $self->in_group('editusers')
+      || $self->can_bless
+      || (Bugzilla->params->{'useclassification'}
+      && $self->in_group('editclassifications'))
+      || $self->in_group('editcomponents')
+      || scalar(@{$self->get_products_by_permission('editcomponents')})
+      || $self->in_group('creategroups')
+      || $self->in_group('editkeywords')
+      || $self->in_group('bz_canusewhines');
+
+    Bugzilla::Hook::process('user_can_administer',
+      {can_administer => \$can_administer});
+    $self->{can_administer} = $can_administer;
+  }
+
+  return $self->{can_administer};
 }
 
 sub check_can_admin_product {
-    my ($self, $product_name) = @_;
+  my ($self, $product_name) = @_;
 
-    # First make sure the product name is valid.
-    my $product = Bugzilla::Product->check($product_name);
+  # First make sure the product name is valid.
+  my $product = Bugzilla::Product->check($product_name);
 
-    ($self->in_group('editcomponents', $product->id)
-       && $self->can_see_product($product->name))
-         || ThrowUserError('product_admin_denied', {product => $product->name});
+  (      $self->in_group('editcomponents', $product->id)
+      && $self->can_see_product($product->name))
+    || ThrowUserError('product_admin_denied', {product => $product->name});
 
-    # Return the validated product object.
-    return $product;
+  # Return the validated product object.
+  return $product;
 }
 
 sub check_can_admin_flagtype {
-    my ($self, $flagtype_id) = @_;
-
-    my $flagtype = Bugzilla::FlagType->check({ id => $flagtype_id });
-    my $can_fully_edit = 1;
-
-    if (!$self->in_group('editcomponents')) {
-        my $products = $self->get_products_by_permission('editcomponents');
-        # You need editcomponents privs for at least one product to have
-        # a chance to edit the flagtype.
-        scalar(@$products)
-          || ThrowUserError('auth_failure', {group  => 'editcomponents',
-                                             action => 'edit',
-                                             object => 'flagtypes'});
-        my $can_admin = 0;
-        my $i = $flagtype->inclusions_as_hash;
-        my $e = $flagtype->exclusions_as_hash;
-
-        # If there is at least one product for which the user doesn't have
-        # editcomponents privs, then don't allow them to do everything with
-        # this flagtype, independently of whether this product is in the
-        # exclusion list or not.
-        my %product_ids;
-        map { $product_ids{$_->id} = 1 } @$products;
-        $can_fully_edit = 0 if grep { !$product_ids{$_} } keys %$i;
-
-        unless ($e->{0}->{0}) {
-            foreach my $product (@$products) {
-                my $id = $product->id;
-                next if $e->{$id}->{0};
-                # If we are here, the product has not been explicitly excluded.
-                # Check whether it's explicitly included, or at least one of
-                # its components.
-                $can_admin = ($i->{0}->{0} || $i->{$id}->{0}
-                              || scalar(grep { !$e->{$id}->{$_} } keys %{$i->{$id}}));
-                last if $can_admin;
-            }
-        }
-        $can_admin || ThrowUserError('flag_type_not_editable', { flagtype => $flagtype });
-    }
-    return wantarray ? ($flagtype, $can_fully_edit) : $flagtype;
+  my ($self, $flagtype_id) = @_;
+
+  my $flagtype = Bugzilla::FlagType->check({id => $flagtype_id});
+  my $can_fully_edit = 1;
+
+  if (!$self->in_group('editcomponents')) {
+    my $products = $self->get_products_by_permission('editcomponents');
+
+    # You need editcomponents privs for at least one product to have
+    # a chance to edit the flagtype.
+    scalar(@$products)
+      || ThrowUserError('auth_failure',
+      {group => 'editcomponents', action => 'edit', object => 'flagtypes'});
+    my $can_admin = 0;
+    my $i         = $flagtype->inclusions_as_hash;
+    my $e         = $flagtype->exclusions_as_hash;
+
+    # If there is at least one product for which the user doesn't have
+    # editcomponents privs, then don't allow them to do everything with
+    # this flagtype, independently of whether this product is in the
+    # exclusion list or not.
+    my %product_ids;
+    map { $product_ids{$_->id} = 1 } @$products;
+    $can_fully_edit = 0 if grep { !$product_ids{$_} } keys %$i;
+
+    unless ($e->{0}->{0}) {
+      foreach my $product (@$products) {
+        my $id = $product->id;
+        next if $e->{$id}->{0};
+
+        # If we are here, the product has not been explicitly excluded.
+        # Check whether it's explicitly included, or at least one of
+        # its components.
+        $can_admin
+          = (  $i->{0}->{0}
+            || $i->{$id}->{0}
+            || scalar(grep { !$e->{$id}->{$_} } keys %{$i->{$id}}));
+        last if $can_admin;
+      }
+    }
+    $can_admin || ThrowUserError('flag_type_not_editable', {flagtype => $flagtype});
+  }
+  return wantarray ? ($flagtype, $can_fully_edit) : $flagtype;
 }
 
 sub can_request_flag {
-    my ($self, $flag_type) = @_;
+  my ($self, $flag_type) = @_;
 
-    return ($self->can_set_flag($flag_type)
-            || !$flag_type->request_group_id
-            || $self->in_group_id($flag_type->request_group_id)) ? 1 : 0;
+  return ($self->can_set_flag($flag_type)
+      || !$flag_type->request_group_id
+      || $self->in_group_id($flag_type->request_group_id)) ? 1 : 0;
 }
 
 sub can_set_flag {
-    my ($self, $flag_type) = @_;
+  my ($self, $flag_type) = @_;
 
-    return (!$flag_type->grant_group_id
-            || $self->in_group_id($flag_type->grant_group_id)) ? 1 : 0;
+  return (!$flag_type->grant_group_id
+      || $self->in_group_id($flag_type->grant_group_id)) ? 1 : 0;
 }
 
 # visible_groups_inherited returns a reference to a list of all the groups
 # whose members are visible to this user.
 sub visible_groups_inherited {
-    my $self = shift;
-    return $self->{visible_groups_inherited} if defined $self->{visible_groups_inherited};
-    return [] unless $self->id;
-    my @visgroups = @{$self->visible_groups_direct};
-    @visgroups = @{Bugzilla::Group->flatten_group_membership(@visgroups)};
-    $self->{visible_groups_inherited} = \@visgroups;
-    return $self->{visible_groups_inherited};
+  my $self = shift;
+  return $self->{visible_groups_inherited}
+    if defined $self->{visible_groups_inherited};
+  return [] unless $self->id;
+  my @visgroups = @{$self->visible_groups_direct};
+  @visgroups = @{Bugzilla::Group->flatten_group_membership(@visgroups)};
+  $self->{visible_groups_inherited} = \@visgroups;
+  return $self->{visible_groups_inherited};
 }
 
 # visible_groups_direct returns a reference to a list of all the groups that
 # are visible to this user.
 sub visible_groups_direct {
-    my $self = shift;
-    my @visgroups = ();
-    return $self->{visible_groups_direct} if defined $self->{visible_groups_direct};
-    return [] unless $self->id;
-
-    my $dbh = Bugzilla->dbh;
-    my $sth;
-   
-    if (Bugzilla->params->{'usevisibilitygroups'}) {
-        $sth = $dbh->prepare("SELECT DISTINCT grantor_id
+  my $self      = shift;
+  my @visgroups = ();
+  return $self->{visible_groups_direct} if defined $self->{visible_groups_direct};
+  return [] unless $self->id;
+
+  my $dbh = Bugzilla->dbh;
+  my $sth;
+
+  if (Bugzilla->params->{'usevisibilitygroups'}) {
+    $sth = $dbh->prepare(
+      "SELECT DISTINCT grantor_id
                                  FROM group_group_map
                                 WHERE " . $self->groups_in_sql('member_id') . "
-                                  AND grant_type=" . GROUP_VISIBLE);
-    }
-    else {
-        # All groups are visible if usevisibilitygroups is off.
-        $sth = $dbh->prepare('SELECT id FROM groups');
-    }
-    $sth->execute();
+                                  AND grant_type=" . GROUP_VISIBLE
+    );
+  }
+  else {
+    # All groups are visible if usevisibilitygroups is off.
+    $sth = $dbh->prepare('SELECT id FROM groups');
+  }
+  $sth->execute();
 
-    while (my ($row) = $sth->fetchrow_array) {
-        push @visgroups,$row;
-    }
-    $self->{visible_groups_direct} = \@visgroups;
+  while (my ($row) = $sth->fetchrow_array) {
+    push @visgroups, $row;
+  }
+  $self->{visible_groups_direct} = \@visgroups;
 
-    return $self->{visible_groups_direct};
+  return $self->{visible_groups_direct};
 }
 
 sub visible_groups_as_string {
-    my $self = shift;
-    return join(', ', @{$self->visible_groups_inherited()});
+  my $self = shift;
+  return join(', ', @{$self->visible_groups_inherited()});
 }
 
 # This function defines the groups a user may share a query with.
 # More restrictive sites may want to build this reference to a list of group IDs
 # from bless_groups instead of mirroring visible_groups_inherited, perhaps.
 sub queryshare_groups {
-    my $self = shift;
-    my @queryshare_groups;
-
-    return $self->{queryshare_groups} if defined $self->{queryshare_groups};
-
-    if ($self->in_group(Bugzilla->params->{'querysharegroup'})) {
-        # We want to be allowed to share with groups we're in only.
-        # If usevisibilitygroups is on, then we need to restrict this to groups
-        # we may see.
-        if (Bugzilla->params->{'usevisibilitygroups'}) {
-            foreach(@{$self->visible_groups_inherited()}) {
-                next unless $self->in_group_id($_);
-                push(@queryshare_groups, $_);
-            }
-        }
-        else {
-            @queryshare_groups = @{ $self->_group_ids };
-        }
+  my $self = shift;
+  my @queryshare_groups;
+
+  return $self->{queryshare_groups} if defined $self->{queryshare_groups};
+
+  if ($self->in_group(Bugzilla->params->{'querysharegroup'})) {
+
+    # We want to be allowed to share with groups we're in only.
+    # If usevisibilitygroups is on, then we need to restrict this to groups
+    # we may see.
+    if (Bugzilla->params->{'usevisibilitygroups'}) {
+      foreach (@{$self->visible_groups_inherited()}) {
+        next unless $self->in_group_id($_);
+        push(@queryshare_groups, $_);
+      }
     }
+    else {
+      @queryshare_groups = @{$self->_group_ids};
+    }
+  }
 
-    return $self->{queryshare_groups} = \@queryshare_groups;
+  return $self->{queryshare_groups} = \@queryshare_groups;
 }
 
 sub queryshare_groups_as_string {
-    my $self = shift;
-    return join(', ', @{$self->queryshare_groups()});
+  my $self = shift;
+  return join(', ', @{$self->queryshare_groups()});
 }
 
 sub derive_regexp_groups {
-    my ($self) = @_;
+  my ($self) = @_;
 
-    my $id = $self->id;
-    return unless $id;
+  my $id = $self->id;
+  return unless $id;
 
-    my $dbh = Bugzilla->dbh;
+  my $dbh = Bugzilla->dbh;
 
-    my $sth;
+  my $sth;
 
-    # add derived records for any matching regexps
+  # add derived records for any matching regexps
 
-    $sth = $dbh->prepare("SELECT id, userregexp, user_group_map.group_id
+  $sth = $dbh->prepare(
+    "SELECT id, userregexp, user_group_map.group_id
                             FROM groups
                        LEFT JOIN user_group_map
                               ON groups.id = user_group_map.group_id
                              AND user_group_map.user_id = ?
-                             AND user_group_map.grant_type = ?");
-    $sth->execute($id, GRANT_REGEXP);
+                             AND user_group_map.grant_type = ?"
+  );
+  $sth->execute($id, GRANT_REGEXP);
 
-    my $group_insert = $dbh->prepare(q{INSERT INTO user_group_map
+  my $group_insert = $dbh->prepare(
+    q{INSERT INTO user_group_map
                                        (user_id, group_id, isbless, grant_type)
-                                       VALUES (?, ?, 0, ?)});
-    my $group_delete = $dbh->prepare(q{DELETE FROM user_group_map
+                                       VALUES (?, ?, 0, ?)}
+  );
+  my $group_delete = $dbh->prepare(
+    q{DELETE FROM user_group_map
                                        WHERE user_id = ?
                                          AND group_id = ?
                                          AND isbless = 0
-                                         AND grant_type = ?});
-    while (my ($group, $regexp, $present) = $sth->fetchrow_array()) {
-        if (($regexp ne '') && ($self->login =~ m/$regexp/i)) {
-            $group_insert->execute($id, $group, GRANT_REGEXP) unless $present;
-        } else {
-            $group_delete->execute($id, $group, GRANT_REGEXP) if $present;
-        }
+                                         AND grant_type = ?}
+  );
+  while (my ($group, $regexp, $present) = $sth->fetchrow_array()) {
+    if (($regexp ne '') && ($self->login =~ m/$regexp/i)) {
+      $group_insert->execute($id, $group, GRANT_REGEXP) unless $present;
     }
+    else {
+      $group_delete->execute($id, $group, GRANT_REGEXP) if $present;
+    }
+  }
 
-    Bugzilla->memcached->clear_config({ key => "user_groups.$id" });
+  Bugzilla->memcached->clear_config({key => "user_groups.$id"});
 }
 
 sub product_responsibilities {
-    my $self = shift;
-    my $dbh = Bugzilla->dbh;
+  my $self = shift;
+  my $dbh  = Bugzilla->dbh;
 
-    return $self->{'product_resp'} if defined $self->{'product_resp'};
-    return [] unless $self->id;
+  return $self->{'product_resp'} if defined $self->{'product_resp'};
+  return [] unless $self->id;
 
-    my $list = $dbh->selectall_arrayref('SELECT components.product_id, components.id
+  my $list = $dbh->selectall_arrayref(
+    'SELECT components.product_id, components.id
                                            FROM components
                                            LEFT JOIN component_cc
                                            ON components.id = component_cc.component_id
                                           WHERE components.initialowner = ?
                                              OR components.initialqacontact = ?
                                              OR component_cc.user_id = ?',
-                                  {Slice => {}}, ($self->id, $self->id, $self->id));
+    {Slice => {}}, ($self->id, $self->id, $self->id)
+  );
 
-    unless ($list) {
-        $self->{'product_resp'} = [];
-        return $self->{'product_resp'};
-    }
-
-    my @prod_ids = map {$_->{'product_id'}} @$list;
-    my $products = Bugzilla::Product->new_from_list(\@prod_ids);
-    # We cannot |use| it, because Component.pm already |use|s User.pm.
-    require Bugzilla::Component;
-    my @comp_ids = map {$_->{'id'}} @$list;
-    my $components = Bugzilla::Component->new_from_list(\@comp_ids);
-
-    my @prod_list;
-    # @$products is already sorted alphabetically.
-    foreach my $prod (@$products) {
-        # We use @components instead of $prod->components because we only want
-        # components where the user is either the default assignee or QA contact.
-        push(@prod_list, {product    => $prod,
-                          components => [grep {$_->product_id == $prod->id} @$components]});
-    }
-    $self->{'product_resp'} = \@prod_list;
+  unless ($list) {
+    $self->{'product_resp'} = [];
     return $self->{'product_resp'};
+  }
+
+  my @prod_ids = map { $_->{'product_id'} } @$list;
+  my $products = Bugzilla::Product->new_from_list(\@prod_ids);
+
+  # We cannot |use| it, because Component.pm already |use|s User.pm.
+  require Bugzilla::Component;
+  my @comp_ids = map { $_->{'id'} } @$list;
+  my $components = Bugzilla::Component->new_from_list(\@comp_ids);
+
+  my @prod_list;
+
+  # @$products is already sorted alphabetically.
+  foreach my $prod (@$products) {
+
+    # We use @components instead of $prod->components because we only want
+    # components where the user is either the default assignee or QA contact.
+    push(
+      @prod_list,
+      {
+        product    => $prod,
+        components => [grep { $_->product_id == $prod->id } @$components]
+      }
+    );
+  }
+  $self->{'product_resp'} = \@prod_list;
+  return $self->{'product_resp'};
 }
 
 sub can_bless {
-    my $self = shift;
+  my $self = shift;
 
-    if (!scalar(@_)) {
-        # If we're called without an argument, just return 
-        # whether or not we can bless at all.
-        return scalar(@{ $self->bless_groups }) ? 1 : 0;
-    }
+  if (!scalar(@_)) {
+
+    # If we're called without an argument, just return
+    # whether or not we can bless at all.
+    return scalar(@{$self->bless_groups}) ? 1 : 0;
+  }
 
-    # Otherwise, we're checking a specific group
-    my $group_id = shift;
-    return grep($_->id == $group_id, @{ $self->bless_groups }) ? 1 : 0;
+  # Otherwise, we're checking a specific group
+  my $group_id = shift;
+  return grep($_->id == $group_id, @{$self->bless_groups}) ? 1 : 0;
 }
 
 sub match {
-    # Generates a list of users whose login name (email address) or real name
-    # matches a substring or wildcard.
-    # This is also called if matches are disabled (for error checking), but
-    # in this case only the exact match code will end up running.
-
-    # $str contains the string to match, while $limit contains the
-    # maximum number of records to retrieve.
-    my ($str, $limit, $exclude_disabled) = @_;
-    my $user = Bugzilla->user;
-    my $dbh = Bugzilla->dbh;
-
-    $str = trim($str);
-
-    my @users = ();
-    return \@users if $str =~ /^\s*$/;
-
-    # The search order is wildcards, then exact match, then substring search.
-    # Wildcard matching is skipped if there is no '*', and exact matches will
-    # not (?) have a '*' in them.  If any search comes up with something, the
-    # ones following it will not execute.
-
-    # first try wildcards
-    my $wildstr = $str;
-
-    # Do not do wildcards if there is no '*' in the string.
-    if ($wildstr =~ s/\*/\%/g && $user->id) {
-        # Build the query.
-        trick_taint($wildstr);
-        my $query  = "SELECT DISTINCT userid FROM profiles ";
-        if (Bugzilla->params->{'usevisibilitygroups'}) {
-            $query .= "INNER JOIN user_group_map
-                               ON user_group_map.user_id = profiles.userid ";
-        }
-        $query .= "WHERE ("
-            . $dbh->sql_istrcmp('login_name', '?', "LIKE") . " OR " .
-              $dbh->sql_istrcmp('realname', '?', "LIKE") . ") ";
-        if (Bugzilla->params->{'usevisibilitygroups'}) {
-            $query .= "AND isbless = 0 " .
-                      "AND group_id IN(" .
-                      join(', ', (-1, @{$user->visible_groups_inherited})) . ") ";
-        }
-        $query    .= " AND is_enabled = 1 " if $exclude_disabled;
-        $query    .= $dbh->sql_limit($limit) if $limit;
 
-        # Execute the query, retrieve the results, and make them into
-        # User objects.
-        my $user_ids = $dbh->selectcol_arrayref($query, undef, ($wildstr, $wildstr));
-        @users = @{Bugzilla::User->new_from_list($user_ids)};
-    }
-    else {    # try an exact match
-        # Exact matches don't care if a user is disabled.
-        trick_taint($str);
-        my $user_id = $dbh->selectrow_array('SELECT userid FROM profiles
-                                             WHERE ' . $dbh->sql_istrcmp('login_name', '?'),
-                                             undef, $str);
-
-        push(@users, new Bugzilla::User($user_id)) if $user_id;
+  # Generates a list of users whose login name (email address) or real name
+  # matches a substring or wildcard.
+  # This is also called if matches are disabled (for error checking), but
+  # in this case only the exact match code will end up running.
+
+  # $str contains the string to match, while $limit contains the
+  # maximum number of records to retrieve.
+  my ($str, $limit, $exclude_disabled) = @_;
+  my $user = Bugzilla->user;
+  my $dbh  = Bugzilla->dbh;
+
+  $str = trim($str);
+
+  my @users = ();
+  return \@users if $str =~ /^\s*$/;
+
+  # The search order is wildcards, then exact match, then substring search.
+  # Wildcard matching is skipped if there is no '*', and exact matches will
+  # not (?) have a '*' in them.  If any search comes up with something, the
+  # ones following it will not execute.
+
+  # first try wildcards
+  my $wildstr = $str;
+
+  # Do not do wildcards if there is no '*' in the string.
+  if ($wildstr =~ s/\*/\%/g && $user->id) {
+
+    # Build the query.
+    trick_taint($wildstr);
+    my $query = "SELECT DISTINCT userid FROM profiles ";
+    if (Bugzilla->params->{'usevisibilitygroups'}) {
+      $query .= "INNER JOIN user_group_map
+                               ON user_group_map.user_id = profiles.userid ";
     }
+    $query
+      .= "WHERE ("
+      . $dbh->sql_istrcmp('login_name', '?', "LIKE") . " OR "
+      . $dbh->sql_istrcmp('realname',   '?', "LIKE") . ") ";
+    if (Bugzilla->params->{'usevisibilitygroups'}) {
+      $query
+        .= "AND isbless = 0 "
+        . "AND group_id IN("
+        . join(', ', (-1, @{$user->visible_groups_inherited})) . ") ";
+    }
+    $query .= " AND is_enabled = 1 "  if $exclude_disabled;
+    $query .= $dbh->sql_limit($limit) if $limit;
+
+    # Execute the query, retrieve the results, and make them into
+    # User objects.
+    my $user_ids = $dbh->selectcol_arrayref($query, undef, ($wildstr, $wildstr));
+    @users = @{Bugzilla::User->new_from_list($user_ids)};
+  }
+  else {    # try an exact match
+            # Exact matches don't care if a user is disabled.
+    trick_taint($str);
+    my $user_id = $dbh->selectrow_array(
+      'SELECT userid FROM profiles
+                                             WHERE '
+        . $dbh->sql_istrcmp('login_name', '?'), undef, $str
+    );
+
+    push(@users, new Bugzilla::User($user_id)) if $user_id;
+  }
 
-    # then try substring search
-    if (!scalar(@users) && length($str) >= 3 && $user->id) {
-        trick_taint($str);
+  # then try substring search
+  if (!scalar(@users) && length($str) >= 3 && $user->id) {
+    trick_taint($str);
 
-        my $query   = "SELECT DISTINCT userid FROM profiles ";
-        if (Bugzilla->params->{'usevisibilitygroups'}) {
-            $query .= "INNER JOIN user_group_map
+    my $query = "SELECT DISTINCT userid FROM profiles ";
+    if (Bugzilla->params->{'usevisibilitygroups'}) {
+      $query .= "INNER JOIN user_group_map
                                ON user_group_map.user_id = profiles.userid ";
-        }
-        $query     .= " WHERE (" .
-                $dbh->sql_iposition('?', 'login_name') . " > 0" . " OR " .
-                $dbh->sql_iposition('?', 'realname') . " > 0) ";
-        if (Bugzilla->params->{'usevisibilitygroups'}) {
-            $query .= " AND isbless = 0" .
-                      " AND group_id IN(" .
-                join(', ', (-1, @{$user->visible_groups_inherited})) . ") ";
-        }
-        $query     .= " AND is_enabled = 1 " if $exclude_disabled;
-        $query     .= $dbh->sql_limit($limit) if $limit;
-        my $user_ids = $dbh->selectcol_arrayref($query, undef, ($str, $str));
-        @users = @{Bugzilla::User->new_from_list($user_ids)};
     }
-    return \@users;
+    $query
+      .= " WHERE ("
+      . $dbh->sql_iposition('?', 'login_name') . " > 0" . " OR "
+      . $dbh->sql_iposition('?', 'realname')
+      . " > 0) ";
+    if (Bugzilla->params->{'usevisibilitygroups'}) {
+      $query
+        .= " AND isbless = 0"
+        . " AND group_id IN("
+        . join(', ', (-1, @{$user->visible_groups_inherited})) . ") ";
+    }
+    $query .= " AND is_enabled = 1 "  if $exclude_disabled;
+    $query .= $dbh->sql_limit($limit) if $limit;
+    my $user_ids = $dbh->selectcol_arrayref($query, undef, ($str, $str));
+    @users = @{Bugzilla::User->new_from_list($user_ids)};
+  }
+  return \@users;
 }
 
 sub match_field {
-    my $fields       = shift;   # arguments as a hash
-    my $data         = shift || Bugzilla->input_params; # hash to look up fields in
-    my $behavior     = shift || 0; # A constant that tells us how to act
-    my $matches      = {};      # the values sent to the template
-    my $matchsuccess = 1;       # did the match fail?
-    my $need_confirm = 0;       # whether to display confirmation screen
-    my $match_multiple = 0;     # whether we ever matched more than one user
-    my @non_conclusive_fields;  # fields which don't have a unique user.
-
-    my $params = Bugzilla->params;
-
-    # prepare default form values
-
-    # Fields can be regular expressions matching multiple form fields
-    # (f.e. "requestee-(\d+)"), so expand each non-literal field
-    # into the list of form fields it matches.
-    my $expanded_fields = {};
-    foreach my $field_pattern (keys %{$fields}) {
-        # Check if the field has any non-word characters.  Only those fields
-        # can be regular expressions, so don't expand the field if it doesn't
-        # have any of those characters.
-        if ($field_pattern =~ /^\w+$/) {
-            $expanded_fields->{$field_pattern} = $fields->{$field_pattern};
-        }
-        else {
-            my @field_names = grep(/$field_pattern/, keys %$data);
-
-            foreach my $field_name (@field_names) {
-                $expanded_fields->{$field_name} = 
-                  { type => $fields->{$field_pattern}->{'type'} };
-                
-                # The field is a requestee field; in order for its name 
-                # to show up correctly on the confirmation page, we need 
-                # to find out the name of its flag type.
-                if ($field_name =~ /^requestee(_type)?-(\d+)$/) {
-                    my $flag_type;
-                    if ($1) {
-                        require Bugzilla::FlagType;
-                        $flag_type = new Bugzilla::FlagType($2);
-                    }
-                    else {
-                        require Bugzilla::Flag;
-                        my $flag = new Bugzilla::Flag($2);
-                        $flag_type = $flag->type if $flag;
-                    }
-                    if ($flag_type) {
-                        $expanded_fields->{$field_name}->{'flag_type'} = $flag_type;
-                    }
-                    else {
-                        # No need to look for a valid requestee if the flag(type)
-                        # has been deleted (may occur in race conditions).
-                        delete $expanded_fields->{$field_name};
-                        delete $data->{$field_name};
-                    }
-                }
-            }
+  my $fields = shift;                            # arguments as a hash
+  my $data = shift || Bugzilla->input_params;    # hash to look up fields in
+  my $behavior       = shift || 0;    # A constant that tells us how to act
+  my $matches        = {};            # the values sent to the template
+  my $matchsuccess   = 1;             # did the match fail?
+  my $need_confirm   = 0;             # whether to display confirmation screen
+  my $match_multiple = 0;             # whether we ever matched more than one user
+  my @non_conclusive_fields;          # fields which don't have a unique user.
+
+  my $params = Bugzilla->params;
+
+  # prepare default form values
+
+  # Fields can be regular expressions matching multiple form fields
+  # (f.e. "requestee-(\d+)"), so expand each non-literal field
+  # into the list of form fields it matches.
+  my $expanded_fields = {};
+  foreach my $field_pattern (keys %{$fields}) {
+
+    # Check if the field has any non-word characters.  Only those fields
+    # can be regular expressions, so don't expand the field if it doesn't
+    # have any of those characters.
+    if ($field_pattern =~ /^\w+$/) {
+      $expanded_fields->{$field_pattern} = $fields->{$field_pattern};
+    }
+    else {
+      my @field_names = grep(/$field_pattern/, keys %$data);
+
+      foreach my $field_name (@field_names) {
+        $expanded_fields->{$field_name} = {type => $fields->{$field_pattern}->{'type'}};
+
+        # The field is a requestee field; in order for its name
+        # to show up correctly on the confirmation page, we need
+        # to find out the name of its flag type.
+        if ($field_name =~ /^requestee(_type)?-(\d+)$/) {
+          my $flag_type;
+          if ($1) {
+            require Bugzilla::FlagType;
+            $flag_type = new Bugzilla::FlagType($2);
+          }
+          else {
+            require Bugzilla::Flag;
+            my $flag = new Bugzilla::Flag($2);
+            $flag_type = $flag->type if $flag;
+          }
+          if ($flag_type) {
+            $expanded_fields->{$field_name}->{'flag_type'} = $flag_type;
+          }
+          else {
+            # No need to look for a valid requestee if the flag(type)
+            # has been deleted (may occur in race conditions).
+            delete $expanded_fields->{$field_name};
+            delete $data->{$field_name};
+          }
         }
+      }
     }
-    $fields = $expanded_fields;
+  }
+  $fields = $expanded_fields;
 
-    foreach my $field (keys %{$fields}) {
-        next unless defined $data->{$field};
+  foreach my $field (keys %{$fields}) {
+    next unless defined $data->{$field};
 
-        #Concatenate login names, so that we have a common way to handle them.
-        my $raw_field;
-        if (ref $data->{$field}) {
-            $raw_field = join(",", @{$data->{$field}});
-        }
-        else {
-            $raw_field = $data->{$field};
-        }
-        $raw_field = clean_text($raw_field || '');
-
-        # Now we either split $raw_field by spaces/commas and put the list
-        # into @queries, or in the case of fields which only accept single
-        # entries, we simply use the verbatim text.
-        my @queries;
-        if ($fields->{$field}->{'type'} eq 'single') {
-            @queries = ($raw_field);
-            # We will repopulate it later if a match is found, else it must
-            # be set to an empty string so that the field remains defined.
-            $data->{$field} = '';
-        }
-        elsif ($fields->{$field}->{'type'} eq 'multi') {
-            @queries =  split(/[,;]+/, $raw_field);
-            # We will repopulate it later if a match is found, else it must
-            # be undefined.
-            delete $data->{$field};
-        }
-        else {
-            # bad argument
-            ThrowCodeError('bad_arg',
-                           { argument => $fields->{$field}->{'type'},
-                             function =>  'Bugzilla::User::match_field',
-                           });
-        }
+    #Concatenate login names, so that we have a common way to handle them.
+    my $raw_field;
+    if (ref $data->{$field}) {
+      $raw_field = join(",", @{$data->{$field}});
+    }
+    else {
+      $raw_field = $data->{$field};
+    }
+    $raw_field = clean_text($raw_field || '');
 
-        # Tolerate fields that do not exist (in case you specify
-        # e.g. the QA contact, and it's currently not in use).
-        next unless (defined $raw_field && $raw_field ne '');
+    # Now we either split $raw_field by spaces/commas and put the list
+    # into @queries, or in the case of fields which only accept single
+    # entries, we simply use the verbatim text.
+    my @queries;
+    if ($fields->{$field}->{'type'} eq 'single') {
+      @queries = ($raw_field);
 
-        my $limit = 0;
-        if ($params->{'maxusermatches'}) {
-            $limit = $params->{'maxusermatches'} + 1;
-        }
+      # We will repopulate it later if a match is found, else it must
+      # be set to an empty string so that the field remains defined.
+      $data->{$field} = '';
+    }
+    elsif ($fields->{$field}->{'type'} eq 'multi') {
+      @queries = split(/[,;]+/, $raw_field);
 
-        my @logins;
-        for my $query (@queries) {
-            $query = trim($query);
-            next if $query eq '';
-
-            my $users = match(
-                $query,   # match string
-                $limit,   # match limit
-                1         # exclude_disabled
-            );
-
-            # here is where it checks for multiple matches
-            if (scalar(@{$users}) == 1) { # exactly one match
-                push(@logins, @{$users}[0]->login);
-
-                # skip confirmation for exact matches
-                next if (lc(@{$users}[0]->login) eq lc($query));
-
-                $matches->{$field}->{$query}->{'status'} = 'success';
-                $need_confirm = 1 if $params->{'confirmuniqueusermatch'};
-
-            }
-            elsif ((scalar(@{$users}) > 1)
-                    && ($params->{'maxusermatches'} != 1)) {
-                $need_confirm = 1;
-                $match_multiple = 1;
-                push(@non_conclusive_fields, $field);
-
-                if (($params->{'maxusermatches'})
-                   && (scalar(@{$users}) > $params->{'maxusermatches'}))
-                {
-                    $matches->{$field}->{$query}->{'status'} = 'trunc';
-                    pop @{$users};  # take the last one out
-                }
-                else {
-                    $matches->{$field}->{$query}->{'status'} = 'success';
-                }
-
-            }
-            else {
-                # everything else fails
-                $matchsuccess = 0; # fail
-                push(@non_conclusive_fields, $field);
-                $matches->{$field}->{$query}->{'status'} = 'fail';
-                $need_confirm = 1;  # confirmation screen shows failures
-            }
-
-            $matches->{$field}->{$query}->{'users'}  = $users;
+      # We will repopulate it later if a match is found, else it must
+      # be undefined.
+      delete $data->{$field};
+    }
+    else {
+      # bad argument
+      ThrowCodeError(
+        'bad_arg',
+        {
+          argument => $fields->{$field}->{'type'},
+          function => 'Bugzilla::User::match_field',
         }
+      );
+    }
+
+    # Tolerate fields that do not exist (in case you specify
+    # e.g. the QA contact, and it's currently not in use).
+    next unless (defined $raw_field && $raw_field ne '');
+
+    my $limit = 0;
+    if ($params->{'maxusermatches'}) {
+      $limit = $params->{'maxusermatches'} + 1;
+    }
+
+    my @logins;
+    for my $query (@queries) {
+      $query = trim($query);
+      next if $query eq '';
+
+      my $users = match(
+        $query,    # match string
+        $limit,    # match limit
+        1          # exclude_disabled
+      );
+
+      # here is where it checks for multiple matches
+      if (scalar(@{$users}) == 1) {    # exactly one match
+        push(@logins, @{$users}[0]->login);
+
+        # skip confirmation for exact matches
+        next if (lc(@{$users}[0]->login) eq lc($query));
 
-        # If no match or more than one match has been found for a field
-        # expecting only one match (type eq "single"), we set it back to ''
-        # so that the caller of this function can still check whether this
-        # field was defined or not (and it was if we came here).
-        if ($fields->{$field}->{'type'} eq 'single') {
-            $data->{$field} = $logins[0] || '';
+        $matches->{$field}->{$query}->{'status'} = 'success';
+        $need_confirm = 1 if $params->{'confirmuniqueusermatch'};
+
+      }
+      elsif ((scalar(@{$users}) > 1) && ($params->{'maxusermatches'} != 1)) {
+        $need_confirm   = 1;
+        $match_multiple = 1;
+        push(@non_conclusive_fields, $field);
+
+        if ( ($params->{'maxusermatches'})
+          && (scalar(@{$users}) > $params->{'maxusermatches'}))
+        {
+          $matches->{$field}->{$query}->{'status'} = 'trunc';
+          pop @{$users};    # take the last one out
         }
-        elsif (scalar @logins) {
-            $data->{$field} = \@logins;
+        else {
+          $matches->{$field}->{$query}->{'status'} = 'success';
         }
-    }
 
-    my $retval;
-    if (!$matchsuccess) {
-        $retval = USER_MATCH_FAILED;
+      }
+      else {
+        # everything else fails
+        $matchsuccess = 0;    # fail
+        push(@non_conclusive_fields, $field);
+        $matches->{$field}->{$query}->{'status'} = 'fail';
+        $need_confirm = 1;    # confirmation screen shows failures
+      }
+
+      $matches->{$field}->{$query}->{'users'} = $users;
     }
-    elsif ($match_multiple) {
-        $retval = USER_MATCH_MULTIPLE;
+
+    # If no match or more than one match has been found for a field
+    # expecting only one match (type eq "single"), we set it back to ''
+    # so that the caller of this function can still check whether this
+    # field was defined or not (and it was if we came here).
+    if ($fields->{$field}->{'type'} eq 'single') {
+      $data->{$field} = $logins[0] || '';
     }
-    else {
-        $retval = USER_MATCH_SUCCESS;
+    elsif (scalar @logins) {
+      $data->{$field} = \@logins;
     }
+  }
 
-    # Skip confirmation if we were told to, or if we don't need to confirm.
-    if ($behavior == MATCH_SKIP_CONFIRM || !$need_confirm) {
-        return wantarray ? ($retval, \@non_conclusive_fields) : $retval;
-    }
+  my $retval;
+  if (!$matchsuccess) {
+    $retval = USER_MATCH_FAILED;
+  }
+  elsif ($match_multiple) {
+    $retval = USER_MATCH_MULTIPLE;
+  }
+  else {
+    $retval = USER_MATCH_SUCCESS;
+  }
 
-    my $template = Bugzilla->template;
-    my $cgi = Bugzilla->cgi;
-    my $vars = {};
+  # Skip confirmation if we were told to, or if we don't need to confirm.
+  if ($behavior == MATCH_SKIP_CONFIRM || !$need_confirm) {
+    return wantarray ? ($retval, \@non_conclusive_fields) : $retval;
+  }
 
-    $vars->{'script'}        = $cgi->url(-relative => 1); # for self-referencing URLs
-    $vars->{'fields'}        = $fields; # fields being matched
-    $vars->{'matches'}       = $matches; # matches that were made
-    $vars->{'matchsuccess'}  = $matchsuccess; # continue or fail
-    $vars->{'matchmultiple'} = $match_multiple;
+  my $template = Bugzilla->template;
+  my $cgi      = Bugzilla->cgi;
+  my $vars     = {};
 
-    print $cgi->header();
+  $vars->{'script'}       = $cgi->url(-relative => 1); # for self-referencing URLs
+  $vars->{'fields'}       = $fields;                   # fields being matched
+  $vars->{'matches'}      = $matches;                  # matches that were made
+  $vars->{'matchsuccess'} = $matchsuccess;             # continue or fail
+  $vars->{'matchmultiple'} = $match_multiple;
 
-    $template->process("global/confirm-user-match.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
-    exit;
+  print $cgi->header();
+
+  $template->process("global/confirm-user-match.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+  exit;
 
 }
 
 # Changes in some fields automatically trigger events. The field names are
 # from the fielddefs table.
 our %names_to_events = (
-    'resolution'              => EVT_OPENED_CLOSED,
-    'keywords'                => EVT_KEYWORD,
-    'cc'                      => EVT_CC,
-    'bug_severity'            => EVT_PROJ_MANAGEMENT,
-    'priority'                => EVT_PROJ_MANAGEMENT,
-    'bug_status'              => EVT_PROJ_MANAGEMENT,
-    'target_milestone'        => EVT_PROJ_MANAGEMENT,
-    'attachments.description' => EVT_ATTACHMENT_DATA,
-    'attachments.mimetype'    => EVT_ATTACHMENT_DATA,
-    'attachments.ispatch'     => EVT_ATTACHMENT_DATA,
-    'dependson'               => EVT_DEPEND_BLOCK,
-    'blocked'                 => EVT_DEPEND_BLOCK,
-    'product'                 => EVT_COMPONENT,
-    'component'               => EVT_COMPONENT);
+  'resolution'              => EVT_OPENED_CLOSED,
+  'keywords'                => EVT_KEYWORD,
+  'cc'                      => EVT_CC,
+  'bug_severity'            => EVT_PROJ_MANAGEMENT,
+  'priority'                => EVT_PROJ_MANAGEMENT,
+  'bug_status'              => EVT_PROJ_MANAGEMENT,
+  'target_milestone'        => EVT_PROJ_MANAGEMENT,
+  'attachments.description' => EVT_ATTACHMENT_DATA,
+  'attachments.mimetype'    => EVT_ATTACHMENT_DATA,
+  'attachments.ispatch'     => EVT_ATTACHMENT_DATA,
+  'dependson'               => EVT_DEPEND_BLOCK,
+  'blocked'                 => EVT_DEPEND_BLOCK,
+  'product'                 => EVT_COMPONENT,
+  'component'               => EVT_COMPONENT
+);
 
 # Returns true if the user wants mail for a given bug change.
 # Note: the "+" signs before the constants suppress bareword quoting.
 sub wants_bug_mail {
-    my $self = shift;
-    my ($bug, $relationship, $fieldDiffs, $comments, $dep_mail, $changer) = @_;
-
-    # Make a list of the events which have happened during this bug change,
-    # from the point of view of this user.    
-    my %events;    
-    foreach my $change (@$fieldDiffs) {
-        my $fieldName = $change->{field_name};
-        # A change to any of the above fields sets the corresponding event
-        if (defined($names_to_events{$fieldName})) {
-            $events{$names_to_events{$fieldName}} = 1;
-        }
-        else {
-            # Catch-all for any change not caught by a more specific event
-            $events{+EVT_OTHER} = 1;
-        }
+  my $self = shift;
+  my ($bug, $relationship, $fieldDiffs, $comments, $dep_mail, $changer) = @_;
 
-        # If the user is in a particular role and the value of that role
-        # changed, we need the ADDED_REMOVED event.
-        if (($fieldName eq "assigned_to" && $relationship == REL_ASSIGNEE) ||
-            ($fieldName eq "qa_contact" && $relationship == REL_QA))
-        {
-            $events{+EVT_ADDED_REMOVED} = 1;
-        }
-        
-        if ($fieldName eq "cc") {
-            my $login = $self->login;
-            my $inold = ($change->{old} =~ /^(.*,\s*)?\Q$login\E(,.*)?$/);
-            my $innew = ($change->{new} =~ /^(.*,\s*)?\Q$login\E(,.*)?$/);
-            if ($inold != $innew)
-            {
-                $events{+EVT_ADDED_REMOVED} = 1;
-            }
-        }
-    }
-
-    if (!$bug->lastdiffed) {
-        # Notify about new bugs.
-        $events{+EVT_BUG_CREATED} = 1;
+  # Make a list of the events which have happened during this bug change,
+  # from the point of view of this user.
+  my %events;
+  foreach my $change (@$fieldDiffs) {
+    my $fieldName = $change->{field_name};
 
-        # You role is new if the bug itself is.
-        # Only makes sense for the assignee, QA contact and the CC list.
-        if ($relationship == REL_ASSIGNEE
-            || $relationship == REL_QA
-            || $relationship == REL_CC)
-        {
-            $events{+EVT_ADDED_REMOVED} = 1;
-        }
+    # A change to any of the above fields sets the corresponding event
+    if (defined($names_to_events{$fieldName})) {
+      $events{$names_to_events{$fieldName}} = 1;
     }
-
-    if (grep { $_->type == CMT_ATTACHMENT_CREATED } @$comments) {
-        $events{+EVT_ATTACHMENT} = 1;
+    else {
+      # Catch-all for any change not caught by a more specific event
+      $events{+EVT_OTHER} = 1;
     }
-    elsif (defined($$comments[0])) {
-        $events{+EVT_COMMENT} = 1;
+
+    # If the user is in a particular role and the value of that role
+    # changed, we need the ADDED_REMOVED event.
+    if ( ($fieldName eq "assigned_to" && $relationship == REL_ASSIGNEE)
+      || ($fieldName eq "qa_contact" && $relationship == REL_QA))
+    {
+      $events{+EVT_ADDED_REMOVED} = 1;
     }
-    
-    # Dependent changed bugmails must have an event to ensure the bugmail is
-    # emailed.
-    if ($dep_mail) {
-        $events{+EVT_DEPEND_BLOCK} = 1;
+
+    if ($fieldName eq "cc") {
+      my $login = $self->login;
+      my $inold = ($change->{old} =~ /^(.*,\s*)?\Q$login\E(,.*)?$/);
+      my $innew = ($change->{new} =~ /^(.*,\s*)?\Q$login\E(,.*)?$/);
+      if ($inold != $innew) {
+        $events{+EVT_ADDED_REMOVED} = 1;
+      }
     }
+  }
+
+  if (!$bug->lastdiffed) {
+
+    # Notify about new bugs.
+    $events{+EVT_BUG_CREATED} = 1;
 
-    my @event_list = keys %events;
-    
-    my $wants_mail = $self->wants_mail(\@event_list, $relationship);
-
-    # The negative events are handled separately - they can't be incorporated
-    # into the first wants_mail call, because they are of the opposite sense.
-    # 
-    # We do them separately because if _any_ of them are set, we don't want
-    # the mail.
-    if ($wants_mail && $changer && ($self->id == $changer->id)) {
-        $wants_mail &= $self->wants_mail([EVT_CHANGED_BY_ME], $relationship);
-    }    
-    
-    if ($wants_mail && $bug->bug_status eq 'UNCONFIRMED') {
-        $wants_mail &= $self->wants_mail([EVT_UNCONFIRMED], $relationship);
+    # You role is new if the bug itself is.
+    # Only makes sense for the assignee, QA contact and the CC list.
+    if ( $relationship == REL_ASSIGNEE
+      || $relationship == REL_QA
+      || $relationship == REL_CC)
+    {
+      $events{+EVT_ADDED_REMOVED} = 1;
     }
-    
-    return $wants_mail;
+  }
+
+  if (grep { $_->type == CMT_ATTACHMENT_CREATED } @$comments) {
+    $events{+EVT_ATTACHMENT} = 1;
+  }
+  elsif (defined($$comments[0])) {
+    $events{+EVT_COMMENT} = 1;
+  }
+
+  # Dependent changed bugmails must have an event to ensure the bugmail is
+  # emailed.
+  if ($dep_mail) {
+    $events{+EVT_DEPEND_BLOCK} = 1;
+  }
+
+  my @event_list = keys %events;
+
+  my $wants_mail = $self->wants_mail(\@event_list, $relationship);
+
+  # The negative events are handled separately - they can't be incorporated
+  # into the first wants_mail call, because they are of the opposite sense.
+  #
+  # We do them separately because if _any_ of them are set, we don't want
+  # the mail.
+  if ($wants_mail && $changer && ($self->id == $changer->id)) {
+    $wants_mail &= $self->wants_mail([EVT_CHANGED_BY_ME], $relationship);
+  }
+
+  if ($wants_mail && $bug->bug_status eq 'UNCONFIRMED') {
+    $wants_mail &= $self->wants_mail([EVT_UNCONFIRMED], $relationship);
+  }
+
+  return $wants_mail;
 }
 
 # Returns true if the user wants mail for a given set of events.
 sub wants_mail {
-    my $self = shift;
-    my ($events, $relationship) = @_;
-    
-    # Don't send any mail, ever, if account is disabled 
-    # XXX Temporary Compatibility Change 1 of 2:
-    # This code is disabled for the moment to make the behaviour like the old
-    # system, which sent bugmail to disabled accounts.
-    # return 0 if $self->{'disabledtext'};
-    
-    # No mail if there are no events
-    return 0 if !scalar(@$events);
-
-    # If a relationship isn't given, default to REL_ANY.
-    if (!defined($relationship)) {
-        $relationship = REL_ANY;
-    }
+  my $self = shift;
+  my ($events, $relationship) = @_;
+
+  # Don't send any mail, ever, if account is disabled
+  # XXX Temporary Compatibility Change 1 of 2:
+  # This code is disabled for the moment to make the behaviour like the old
+  # system, which sent bugmail to disabled accounts.
+  # return 0 if $self->{'disabledtext'};
 
-    # Skip DB query if relationship is explicit
-    return 1 if $relationship == REL_GLOBAL_WATCHER;
+  # No mail if there are no events
+  return 0 if !scalar(@$events);
 
-    my $wants_mail = grep { $self->mail_settings->{$relationship}{$_} } @$events;
-    return $wants_mail ? 1 : 0;
+  # If a relationship isn't given, default to REL_ANY.
+  if (!defined($relationship)) {
+    $relationship = REL_ANY;
+  }
+
+  # Skip DB query if relationship is explicit
+  return 1 if $relationship == REL_GLOBAL_WATCHER;
+
+  my $wants_mail = grep { $self->mail_settings->{$relationship}{$_} } @$events;
+  return $wants_mail ? 1 : 0;
 }
 
 sub mail_settings {
-    my $self = shift;
-    my $dbh = Bugzilla->dbh;
-
-    if (!defined $self->{'mail_settings'}) {
-        my $data =
-          $dbh->selectall_arrayref('SELECT relationship, event FROM email_setting
-                                    WHERE user_id = ?', undef, $self->id);
-        my %mail;
-        # The hash is of the form $mail{$relationship}{$event} = 1.
-        $mail{$_->[0]}{$_->[1]} = 1 foreach @$data;
-
-        $self->{'mail_settings'} = \%mail;
-    }
-    return $self->{'mail_settings'};
+  my $self = shift;
+  my $dbh  = Bugzilla->dbh;
+
+  if (!defined $self->{'mail_settings'}) {
+    my $data = $dbh->selectall_arrayref(
+      'SELECT relationship, event FROM email_setting
+                                    WHERE user_id = ?', undef, $self->id
+    );
+    my %mail;
+
+    # The hash is of the form $mail{$relationship}{$event} = 1.
+    $mail{$_->[0]}{$_->[1]} = 1 foreach @$data;
+
+    $self->{'mail_settings'} = \%mail;
+  }
+  return $self->{'mail_settings'};
 }
 
 sub has_audit_entries {
-    my $self = shift;
-    my $dbh = Bugzilla->dbh;
+  my $self = shift;
+  my $dbh  = Bugzilla->dbh;
 
-    if (!exists $self->{'has_audit_entries'}) {
-        $self->{'has_audit_entries'} =
-            $dbh->selectrow_array('SELECT 1 FROM audit_log WHERE user_id = ? ' .
-                                   $dbh->sql_limit(1), undef, $self->id);
-    }
-    return $self->{'has_audit_entries'};
+  if (!exists $self->{'has_audit_entries'}) {
+    $self->{'has_audit_entries'}
+      = $dbh->selectrow_array(
+      'SELECT 1 FROM audit_log WHERE user_id = ? ' . $dbh->sql_limit(1),
+      undef, $self->id);
+  }
+  return $self->{'has_audit_entries'};
 }
 
 sub is_insider {
-    my $self = shift;
+  my $self = shift;
 
-    if (!defined $self->{'is_insider'}) {
-        my $insider_group = Bugzilla->params->{'insidergroup'};
-        $self->{'is_insider'} =
-            ($insider_group && $self->in_group($insider_group)) ? 1 : 0;
-    }
-    return $self->{'is_insider'};
+  if (!defined $self->{'is_insider'}) {
+    my $insider_group = Bugzilla->params->{'insidergroup'};
+    $self->{'is_insider'}
+      = ($insider_group && $self->in_group($insider_group)) ? 1 : 0;
+  }
+  return $self->{'is_insider'};
 }
 
 sub is_global_watcher {
-    my $self = shift;
+  my $self = shift;
 
-    if (!defined $self->{'is_global_watcher'}) {
-        my @watchers = split(/[,;]+/, Bugzilla->params->{'globalwatchers'});
-        $self->{'is_global_watcher'} = scalar(grep { $_ eq $self->login } @watchers) ? 1 : 0;
-    }
-    return  $self->{'is_global_watcher'};
+  if (!defined $self->{'is_global_watcher'}) {
+    my @watchers = split(/[,;]+/, Bugzilla->params->{'globalwatchers'});
+    $self->{'is_global_watcher'}
+      = scalar(grep { $_ eq $self->login } @watchers) ? 1 : 0;
+  }
+  return $self->{'is_global_watcher'};
 }
 
 sub is_timetracker {
-    my $self = shift;
+  my $self = shift;
 
-    if (!defined $self->{'is_timetracker'}) {
-        my $tt_group = Bugzilla->params->{'timetrackinggroup'};
-        $self->{'is_timetracker'} =
-            ($tt_group && $self->in_group($tt_group)) ? 1 : 0;
-    }
-    return $self->{'is_timetracker'};
+  if (!defined $self->{'is_timetracker'}) {
+    my $tt_group = Bugzilla->params->{'timetrackinggroup'};
+    $self->{'is_timetracker'} = ($tt_group && $self->in_group($tt_group)) ? 1 : 0;
+  }
+  return $self->{'is_timetracker'};
 }
 
 sub can_tag_comments {
-    my $self = shift;
+  my $self = shift;
 
-    if (!defined $self->{'can_tag_comments'}) {
-        my $group = Bugzilla->params->{'comment_taggers_group'};
-        $self->{'can_tag_comments'} =
-            ($group && $self->in_group($group)) ? 1 : 0;
-    }
-    return $self->{'can_tag_comments'};
+  if (!defined $self->{'can_tag_comments'}) {
+    my $group = Bugzilla->params->{'comment_taggers_group'};
+    $self->{'can_tag_comments'} = ($group && $self->in_group($group)) ? 1 : 0;
+  }
+  return $self->{'can_tag_comments'};
 }
 
 sub get_userlist {
-    my $self = shift;
-
-    return $self->{'userlist'} if defined $self->{'userlist'};
-
-    my $dbh = Bugzilla->dbh;
-    my $query  = "SELECT DISTINCT login_name, realname,";
-    if (Bugzilla->params->{'usevisibilitygroups'}) {
-        $query .= " COUNT(group_id) ";
-    } else {
-        $query .= " 1 ";
-    }
-    $query     .= "FROM profiles ";
-    if (Bugzilla->params->{'usevisibilitygroups'}) {
-        $query .= "LEFT JOIN user_group_map " .
-                  "ON user_group_map.user_id = userid AND isbless = 0 " .
-                  "AND group_id IN(" .
-                  join(', ', (-1, @{$self->visible_groups_inherited})) . ")";
-    }
-    $query    .= " WHERE is_enabled = 1 ";
-    $query    .= $dbh->sql_group_by('userid', 'login_name, realname');
-
-    my $sth = $dbh->prepare($query);
-    $sth->execute;
-
-    my @userlist;
-    while (my($login, $name, $visible) = $sth->fetchrow_array) {
-        push @userlist, {
-            login => $login,
-            identity => $name ? "$name <$login>" : $login,
-            visible => $visible,
-        };
-    }
-    @userlist = sort { lc $$a{'identity'} cmp lc $$b{'identity'} } @userlist;
-
-    $self->{'userlist'} = \@userlist;
-    return $self->{'userlist'};
+  my $self = shift;
+
+  return $self->{'userlist'} if defined $self->{'userlist'};
+
+  my $dbh   = Bugzilla->dbh;
+  my $query = "SELECT DISTINCT login_name, realname,";
+  if (Bugzilla->params->{'usevisibilitygroups'}) {
+    $query .= " COUNT(group_id) ";
+  }
+  else {
+    $query .= " 1 ";
+  }
+  $query .= "FROM profiles ";
+  if (Bugzilla->params->{'usevisibilitygroups'}) {
+    $query
+      .= "LEFT JOIN user_group_map "
+      . "ON user_group_map.user_id = userid AND isbless = 0 "
+      . "AND group_id IN("
+      . join(', ', (-1, @{$self->visible_groups_inherited})) . ")";
+  }
+  $query .= " WHERE is_enabled = 1 ";
+  $query .= $dbh->sql_group_by('userid', 'login_name, realname');
+
+  my $sth = $dbh->prepare($query);
+  $sth->execute;
+
+  my @userlist;
+  while (my ($login, $name, $visible) = $sth->fetchrow_array) {
+    push @userlist,
+      {
+      login    => $login,
+      identity => $name ? "$name <$login>" : $login,
+      visible  => $visible,
+      };
+  }
+  @userlist = sort { lc $$a{'identity'} cmp lc $$b{'identity'} } @userlist;
+
+  $self->{'userlist'} = \@userlist;
+  return $self->{'userlist'};
 }
 
 sub create {
-    my $invocant = shift;
-    my $class = ref($invocant) || $invocant;
-    my $dbh = Bugzilla->dbh;
-
-    $dbh->bz_start_transaction();
-
-    my $user = $class->SUPER::create(@_);
-
-    # Turn on all email for the new user
-    require Bugzilla::BugMail;
-    my %relationships = Bugzilla::BugMail::relationships();
-    foreach my $rel (keys %relationships) {
-        foreach my $event (POS_EVENTS, NEG_EVENTS) {
-            # These "exceptions" define the default email preferences.
-            # 
-            # We enable mail unless the change was made by the user, or it's
-            # just a CC list addition and the user is not the reporter.
-            next if ($event == EVT_CHANGED_BY_ME);
-            next if (($event == EVT_CC) && ($rel != REL_REPORTER));
-
-            $dbh->do('INSERT INTO email_setting (user_id, relationship, event)
-                      VALUES (?, ?, ?)', undef, ($user->id, $rel, $event));
-        }
-    }
-
-    foreach my $event (GLOBAL_EVENTS) {
-        $dbh->do('INSERT INTO email_setting (user_id, relationship, event)
-                  VALUES (?, ?, ?)', undef, ($user->id, REL_ANY, $event));
-    }
+  my $invocant = shift;
+  my $class    = ref($invocant) || $invocant;
+  my $dbh      = Bugzilla->dbh;
+
+  $dbh->bz_start_transaction();
+
+  my $user = $class->SUPER::create(@_);
+
+  # Turn on all email for the new user
+  require Bugzilla::BugMail;
+  my %relationships = Bugzilla::BugMail::relationships();
+  foreach my $rel (keys %relationships) {
+    foreach my $event (POS_EVENTS, NEG_EVENTS) {
+
+      # These "exceptions" define the default email preferences.
+      #
+      # We enable mail unless the change was made by the user, or it's
+      # just a CC list addition and the user is not the reporter.
+      next if ($event == EVT_CHANGED_BY_ME);
+      next if (($event == EVT_CC) && ($rel != REL_REPORTER));
+
+      $dbh->do(
+        'INSERT INTO email_setting (user_id, relationship, event)
+                      VALUES (?, ?, ?)', undef, ($user->id, $rel, $event)
+      );
+    }
+  }
+
+  foreach my $event (GLOBAL_EVENTS) {
+    $dbh->do(
+      'INSERT INTO email_setting (user_id, relationship, event)
+                  VALUES (?, ?, ?)', undef, ($user->id, REL_ANY, $event)
+    );
+  }
 
-    $user->derive_regexp_groups();
+  $user->derive_regexp_groups();
 
-    # Add the creation date to the profiles_activity table.
-    # $who is the user who created the new user account, i.e. either an
-    # admin or the new user himself.
-    my $who = Bugzilla->user->id || $user->id;
-    my $creation_date_fieldid = get_field_id('creation_ts');
+  # Add the creation date to the profiles_activity table.
+  # $who is the user who created the new user account, i.e. either an
+  # admin or the new user himself.
+  my $who = Bugzilla->user->id || $user->id;
+  my $creation_date_fieldid = get_field_id('creation_ts');
 
-    $dbh->do('INSERT INTO profiles_activity
+  $dbh->do(
+    'INSERT INTO profiles_activity
                           (userid, who, profiles_when, fieldid, newvalue)
-                   VALUES (?, ?, NOW(), ?, NOW())',
-                   undef, ($user->id, $who, $creation_date_fieldid));
+                   VALUES (?, ?, NOW(), ?, NOW())', undef,
+    ($user->id, $who, $creation_date_fieldid)
+  );
 
-    $dbh->bz_commit_transaction();
+  $dbh->bz_commit_transaction();
 
-    # Return the newly created user account.
-    return $user;
+  # Return the newly created user account.
+  return $user;
 }
 
 ###########################
@@ -2305,44 +2404,45 @@ sub create {
 ###########################
 
 sub account_is_locked_out {
-    my $self = shift;
-    my $login_failures = scalar @{ $self->account_ip_login_failures };
-    return $login_failures >= MAX_LOGIN_ATTEMPTS ? 1 : 0;
+  my $self           = shift;
+  my $login_failures = scalar @{$self->account_ip_login_failures};
+  return $login_failures >= MAX_LOGIN_ATTEMPTS ? 1 : 0;
 }
 
 sub note_login_failure {
-    my $self = shift;
-    my $ip_addr = remote_ip();
-    trick_taint($ip_addr);
-    Bugzilla->dbh->do("INSERT INTO login_failure (user_id, ip_addr, login_time)
-                       VALUES (?, ?, LOCALTIMESTAMP(0))",
-                      undef, $self->id, $ip_addr);
-    delete $self->{account_ip_login_failures};
+  my $self    = shift;
+  my $ip_addr = remote_ip();
+  trick_taint($ip_addr);
+  Bugzilla->dbh->do(
+    "INSERT INTO login_failure (user_id, ip_addr, login_time)
+                       VALUES (?, ?, LOCALTIMESTAMP(0))", undef, $self->id, $ip_addr
+  );
+  delete $self->{account_ip_login_failures};
 }
 
 sub clear_login_failures {
-    my $self = shift;
-    my $ip_addr = remote_ip();
-    trick_taint($ip_addr);
-    Bugzilla->dbh->do(
-        'DELETE FROM login_failure WHERE user_id = ? AND ip_addr = ?',
-        undef, $self->id, $ip_addr);
-    delete $self->{account_ip_login_failures};
+  my $self    = shift;
+  my $ip_addr = remote_ip();
+  trick_taint($ip_addr);
+  Bugzilla->dbh->do('DELETE FROM login_failure WHERE user_id = ? AND ip_addr = ?',
+    undef, $self->id, $ip_addr);
+  delete $self->{account_ip_login_failures};
 }
 
 sub account_ip_login_failures {
-    my $self = shift;
-    my $dbh = Bugzilla->dbh;
-    my $time = $dbh->sql_date_math('LOCALTIMESTAMP(0)', '-', 
-                                   LOGIN_LOCKOUT_INTERVAL, 'MINUTE');
-    my $ip_addr = remote_ip();
-    trick_taint($ip_addr);
-    $self->{account_ip_login_failures} ||= Bugzilla->dbh->selectall_arrayref(
-        "SELECT login_time, ip_addr, user_id FROM login_failure
+  my $self = shift;
+  my $dbh  = Bugzilla->dbh;
+  my $time = $dbh->sql_date_math('LOCALTIMESTAMP(0)', '-', LOGIN_LOCKOUT_INTERVAL,
+    'MINUTE');
+  my $ip_addr = remote_ip();
+  trick_taint($ip_addr);
+  $self->{account_ip_login_failures} ||= Bugzilla->dbh->selectall_arrayref(
+    "SELECT login_time, ip_addr, user_id FROM login_failure
           WHERE user_id = ? AND login_time > $time
                 AND ip_addr = ?
-       ORDER BY login_time", {Slice => {}}, $self->id, $ip_addr);
-    return $self->{account_ip_login_failures};
+       ORDER BY login_time", {Slice => {}}, $self->id, $ip_addr
+  );
+  return $self->{account_ip_login_failures};
 }
 
 ###############
@@ -2350,145 +2450,162 @@ sub account_ip_login_failures {
 ###############
 
 sub is_available_username {
-    my ($username, $old_username) = @_;
-
-    if(login_to_id($username) != 0) {
-        return 0;
-    }
+  my ($username, $old_username) = @_;
 
-    my $dbh = Bugzilla->dbh;
-    # $username is safe because it is only used in SELECT placeholders.
-    trick_taint($username);
-    # Reject if the new login is part of an email change which is
-    # still in progress
-    #
-    # substring/locate stuff: bug 165221; this used to use regexes, but that
-    # was unsafe and required weird escaping; using substring to pull out
-    # the new/old email addresses and sql_position() to find the delimiter (':')
-    # is cleaner/safer
-    my ($tokentype, $eventdata) = $dbh->selectrow_array(
-        "SELECT tokentype, eventdata
+  if (login_to_id($username) != 0) {
+    return 0;
+  }
+
+  my $dbh = Bugzilla->dbh;
+
+  # $username is safe because it is only used in SELECT placeholders.
+  trick_taint($username);
+
+  # Reject if the new login is part of an email change which is
+  # still in progress
+  #
+  # substring/locate stuff: bug 165221; this used to use regexes, but that
+  # was unsafe and required weird escaping; using substring to pull out
+  # the new/old email addresses and sql_position() to find the delimiter (':')
+  # is cleaner/safer
+  my ($tokentype, $eventdata) = $dbh->selectrow_array(
+    "SELECT tokentype, eventdata
            FROM tokens
           WHERE (tokentype = 'emailold'
-                AND SUBSTRING(eventdata, 1, (" .
-                    $dbh->sql_position(q{':'}, 'eventdata') . "-  1)) = ?)
+                AND SUBSTRING(eventdata, 1, ("
+      . $dbh->sql_position(q{':'}, 'eventdata') . "-  1)) = ?)
              OR (tokentype = 'emailnew'
-                AND SUBSTRING(eventdata, (" .
-                    $dbh->sql_position(q{':'}, 'eventdata') . "+ 1), LENGTH(eventdata)) = ?)",
-         undef, ($username, $username));
-
-    if ($eventdata) {
-        # Allow thru owner of token
-        if ($old_username
-            && (($tokentype eq 'emailnew' && $eventdata eq "$old_username:$username")
-                || ($tokentype eq 'emailold' && $eventdata eq "$username:$old_username")))
-        {
-            return 1;
-        }
-        return 0;
+                AND SUBSTRING(eventdata, ("
+      . $dbh->sql_position(q{':'}, 'eventdata')
+      . "+ 1), LENGTH(eventdata)) = ?)",
+    undef, ($username, $username)
+  );
+
+  if ($eventdata) {
+
+    # Allow thru owner of token
+    if (
+      $old_username
+      && ( ($tokentype eq 'emailnew' && $eventdata eq "$old_username:$username")
+        || ($tokentype eq 'emailold' && $eventdata eq "$username:$old_username"))
+      )
+    {
+      return 1;
     }
+    return 0;
+  }
 
-    return 1;
+  return 1;
 }
 
 sub check_account_creation_enabled {
-    my $self = shift;
+  my $self = shift;
 
-    # If we're using e.g. LDAP for login, then we can't create a new account.
-    $self->authorizer->user_can_create_account
-      || ThrowUserError('auth_cant_create_account');
+  # If we're using e.g. LDAP for login, then we can't create a new account.
+  $self->authorizer->user_can_create_account
+    || ThrowUserError('auth_cant_create_account');
 
-    Bugzilla->params->{'createemailregexp'}
-      || ThrowUserError('account_creation_disabled');
+  Bugzilla->params->{'createemailregexp'}
+    || ThrowUserError('account_creation_disabled');
 }
 
 sub check_and_send_account_creation_confirmation {
-    my ($self, $login) = @_;
-    my $dbh = Bugzilla->dbh;
+  my ($self, $login) = @_;
+  my $dbh = Bugzilla->dbh;
 
-    $dbh->bz_start_transaction;
+  $dbh->bz_start_transaction;
 
-    $login = $self->check_login_name($login);
-    my $creation_regexp = Bugzilla->params->{'createemailregexp'};
+  $login = $self->check_login_name($login);
+  my $creation_regexp = Bugzilla->params->{'createemailregexp'};
 
-    if ($login !~ /$creation_regexp/i) {
-        ThrowUserError('account_creation_restricted');
-    }
+  if ($login !~ /$creation_regexp/i) {
+    ThrowUserError('account_creation_restricted');
+  }
 
-    # Allow extensions to do extra checks.
-    Bugzilla::Hook::process('user_check_account_creation', { login => $login });
+  # Allow extensions to do extra checks.
+  Bugzilla::Hook::process('user_check_account_creation', {login => $login});
 
-    # Create and send a token for this new account.
-    require Bugzilla::Token;
-    Bugzilla::Token::issue_new_user_account_token($login);
+  # Create and send a token for this new account.
+  require Bugzilla::Token;
+  Bugzilla::Token::issue_new_user_account_token($login);
 
-    $dbh->bz_commit_transaction;
+  $dbh->bz_commit_transaction;
 }
 
 # This is used in a few performance-critical areas where we don't want to
 # do check() and pull all the user data from the database.
 sub login_to_id {
-    my ($login, $throw_error) = @_;
-    my $dbh = Bugzilla->dbh;
-    my $cache = Bugzilla->request_cache->{user_login_to_id} ||= {};
-
-    # We cache lookups because this function showed up as taking up a 
-    # significant amount of time in profiles of xt/search.t. However,
-    # for users that don't exist, we re-do the check every time, because
-    # otherwise we break is_available_username.
-    my $user_id;
-    if (defined $cache->{$login}) {
-        $user_id = $cache->{$login};
-    }
-    else {
-        # No need to validate $login -- it will be used by the following SELECT
-        # statement only, so it's safe to simply trick_taint.
-        trick_taint($login);
-        $user_id = $dbh->selectrow_array(
-            "SELECT userid FROM profiles 
-              WHERE " . $dbh->sql_istrcmp('login_name', '?'), undef, $login);
-        $cache->{$login} = $user_id;
-    }
-
-    if ($user_id) {
-        return $user_id;
-    } elsif ($throw_error) {
-        ThrowUserError('invalid_username', { name => $login });
-    } else {
-        return 0;
-    }
+  my ($login, $throw_error) = @_;
+  my $dbh = Bugzilla->dbh;
+  my $cache = Bugzilla->request_cache->{user_login_to_id} ||= {};
+
+  # We cache lookups because this function showed up as taking up a
+  # significant amount of time in profiles of xt/search.t. However,
+  # for users that don't exist, we re-do the check every time, because
+  # otherwise we break is_available_username.
+  my $user_id;
+  if (defined $cache->{$login}) {
+    $user_id = $cache->{$login};
+  }
+  else {
+    # No need to validate $login -- it will be used by the following SELECT
+    # statement only, so it's safe to simply trick_taint.
+    trick_taint($login);
+    $user_id = $dbh->selectrow_array(
+      "SELECT userid FROM profiles 
+              WHERE " . $dbh->sql_istrcmp('login_name', '?'), undef, $login
+    );
+    $cache->{$login} = $user_id;
+  }
+
+  if ($user_id) {
+    return $user_id;
+  }
+  elsif ($throw_error) {
+    ThrowUserError('invalid_username', {name => $login});
+  }
+  else {
+    return 0;
+  }
 }
 
 sub validate_password {
-    my $check = validate_password_check(@_);
-    ThrowUserError($check) if $check;
-    return 1;
+  my $check = validate_password_check(@_);
+  ThrowUserError($check) if $check;
+  return 1;
 }
 
 sub validate_password_check {
-    my ($password, $matchpassword) = @_;
-
-    if (length($password) < USER_PASSWORD_MIN_LENGTH) {
-        return 'password_too_short';
-    } elsif ((defined $matchpassword) && ($password ne $matchpassword)) {
-        return 'passwords_dont_match';
-    }
-
-    my $complexity_level = Bugzilla->params->{password_complexity};
-    if ($complexity_level eq 'letters_numbers_specialchars') {
-        return 'password_not_complex'
-          if ($password !~ /[[:alpha:]]/ || $password !~ /\d/ || $password !~ /[[:punct:]]/);
-    } elsif ($complexity_level eq 'letters_numbers') {
-        return 'password_not_complex'
-          if ($password !~ /[[:lower:]]/ || $password !~ /[[:upper:]]/ || $password !~ /\d/);
-    } elsif ($complexity_level eq 'mixed_letters') {
-        return 'password_not_complex'
-          if ($password !~ /[[:lower:]]/ || $password !~ /[[:upper:]]/);
-    }
-
-    # Having done these checks makes us consider the password untainted.
-    trick_taint($_[0]);
-    return;
+  my ($password, $matchpassword) = @_;
+
+  if (length($password) < USER_PASSWORD_MIN_LENGTH) {
+    return 'password_too_short';
+  }
+  elsif ((defined $matchpassword) && ($password ne $matchpassword)) {
+    return 'passwords_dont_match';
+  }
+
+  my $complexity_level = Bugzilla->params->{password_complexity};
+  if ($complexity_level eq 'letters_numbers_specialchars') {
+    return 'password_not_complex'
+      if ($password !~ /[[:alpha:]]/
+      || $password !~ /\d/
+      || $password !~ /[[:punct:]]/);
+  }
+  elsif ($complexity_level eq 'letters_numbers') {
+    return 'password_not_complex'
+      if ($password !~ /[[:lower:]]/
+      || $password !~ /[[:upper:]]/
+      || $password !~ /\d/);
+  }
+  elsif ($complexity_level eq 'mixed_letters') {
+    return 'password_not_complex'
+      if ($password !~ /[[:lower:]]/ || $password !~ /[[:upper:]]/);
+  }
+
+  # Having done these checks makes us consider the password untainted.
+  trick_taint($_[0]);
+  return;
 }
 
 
diff --git a/Bugzilla/User/APIKey.pm b/Bugzilla/User/APIKey.pm
index d268a0a93..d2e337c5e 100644
--- a/Bugzilla/User/APIKey.pm
+++ b/Bugzilla/User/APIKey.pm
@@ -20,52 +20,54 @@ use Bugzilla::Util qw(generate_random_password trim);
 # Overriden Constants that are used as methods
 #####################################################################
 
-use constant DB_TABLE       => 'user_api_keys';
-use constant DB_COLUMNS     => qw(
-    id
-    user_id
-    api_key
-    description
-    revoked
-    last_used
+use constant DB_TABLE   => 'user_api_keys';
+use constant DB_COLUMNS => qw(
+  id
+  user_id
+  api_key
+  description
+  revoked
+  last_used
 );
 
 use constant UPDATE_COLUMNS => qw(description revoked last_used);
 use constant VALIDATORS     => {
-    api_key     => \&_check_api_key,
-    description => \&_check_description,
-    revoked     => \&Bugzilla::Object::check_boolean,
+  api_key     => \&_check_api_key,
+  description => \&_check_description,
+  revoked     => \&Bugzilla::Object::check_boolean,
 };
-use constant LIST_ORDER     => 'id';
-use constant NAME_FIELD     => 'api_key';
+use constant LIST_ORDER => 'id';
+use constant NAME_FIELD => 'api_key';
 
 # turn off auditing and exclude these objects from memcached
-use constant { AUDIT_CREATES => 0,
-               AUDIT_UPDATES => 0,
-               AUDIT_REMOVES => 0,
-               USE_MEMCACHED => 0 };
+use constant {
+  AUDIT_CREATES => 0,
+  AUDIT_UPDATES => 0,
+  AUDIT_REMOVES => 0,
+  USE_MEMCACHED => 0
+};
 
 # Accessors
-sub id            { return $_[0]->{id}          }
-sub user_id       { return $_[0]->{user_id}     }
-sub api_key       { return $_[0]->{api_key}     }
-sub description   { return $_[0]->{description} }
-sub revoked       { return $_[0]->{revoked}     }
-sub last_used     { return $_[0]->{last_used}   }
+sub id          { return $_[0]->{id} }
+sub user_id     { return $_[0]->{user_id} }
+sub api_key     { return $_[0]->{api_key} }
+sub description { return $_[0]->{description} }
+sub revoked     { return $_[0]->{revoked} }
+sub last_used   { return $_[0]->{last_used} }
 
 # Helpers
 sub user {
-    my $self = shift;
-    $self->{user} //= Bugzilla::User->new({name => $self->user_id, cache => 1});
-    return $self->{user};
+  my $self = shift;
+  $self->{user} //= Bugzilla::User->new({name => $self->user_id, cache => 1});
+  return $self->{user};
 }
 
 sub update_last_used {
-    my $self = shift;
-    my $timestamp = shift
-        || Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
-    $self->set('last_used', $timestamp);
-    $self->update;
+  my $self = shift;
+  my $timestamp
+    = shift || Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
+  $self->set('last_used', $timestamp);
+  $self->update;
 }
 
 # Setters
@@ -73,8 +75,8 @@ sub set_description { $_[0]->set('description', $_[1]); }
 sub set_revoked     { $_[0]->set('revoked',     $_[1]); }
 
 # Validators
-sub _check_api_key     { return generate_random_password(40); }
-sub _check_description { return trim($_[1]) || '';   }
+sub _check_api_key { return generate_random_password(40); }
+sub _check_description { return trim($_[1]) || ''; }
 1;
 
 __END__
diff --git a/Bugzilla/User/Setting.pm b/Bugzilla/User/Setting.pm
index aece3b7de..94171a5d9 100644
--- a/Bugzilla/User/Setting.pm
+++ b/Bugzilla/User/Setting.pm
@@ -17,10 +17,10 @@ use parent qw(Exporter);
 
 # Module stuff
 @Bugzilla::User::Setting::EXPORT = qw(
-    get_all_settings
-    get_defaults
-    add_setting
-    clear_settings_cache
+  get_all_settings
+  get_defaults
+  add_setting
+  clear_settings_cache
 );
 
 use Bugzilla::Error;
@@ -31,88 +31,84 @@ use Bugzilla::Util qw(trick_taint get_text);
 ###############################
 
 sub new {
-    my $invocant = shift;
-    my $setting_name = shift;
-    my $user_id = shift;
-
-    my $class = ref($invocant) || $invocant;
-    my $subclass = '';
-
-    # Create a ref to an empty hash and bless it
-    my $self = {};
-
-    my $dbh = Bugzilla->dbh;
-
-    # Confirm that the $setting_name is properly formed;
-    # if not, throw a code error. 
-    # 
-    # NOTE: due to the way that setting names are used in templates,
-    # they must conform to to the limitations set for HTML NAMEs and IDs.
-    #
-    if ( !($setting_name =~ /^[a-zA-Z][-.:\w]*$/) ) {
-      ThrowCodeError("setting_name_invalid", { name => $setting_name });
-    }
-
-    # If there were only two parameters passed in, then we need
-    # to retrieve the information for this setting ourselves.
-    if (scalar @_ == 0) {
-
-        my ($default, $is_enabled, $value);
-        ($default, $is_enabled, $value, $subclass) = 
-          $dbh->selectrow_array(
-             q{SELECT default_value, is_enabled, setting_value, subclass
+  my $invocant     = shift;
+  my $setting_name = shift;
+  my $user_id      = shift;
+
+  my $class = ref($invocant) || $invocant;
+  my $subclass = '';
+
+  # Create a ref to an empty hash and bless it
+  my $self = {};
+
+  my $dbh = Bugzilla->dbh;
+
+  # Confirm that the $setting_name is properly formed;
+  # if not, throw a code error.
+  #
+  # NOTE: due to the way that setting names are used in templates,
+  # they must conform to to the limitations set for HTML NAMEs and IDs.
+  #
+  if (!($setting_name =~ /^[a-zA-Z][-.:\w]*$/)) {
+    ThrowCodeError("setting_name_invalid", {name => $setting_name});
+  }
+
+  # If there were only two parameters passed in, then we need
+  # to retrieve the information for this setting ourselves.
+  if (scalar @_ == 0) {
+
+    my ($default, $is_enabled, $value);
+    ($default, $is_enabled, $value, $subclass) = $dbh->selectrow_array(
+      q{SELECT default_value, is_enabled, setting_value, subclass
                  FROM setting
             LEFT JOIN profile_setting
                    ON setting.name = profile_setting.setting_name
                 WHERE name = ?
-                  AND profile_setting.user_id = ?},
-             undef, 
-             $setting_name, $user_id);
-
-        # if not defined, then grab the default value
-        if (! defined $value) {
-            ($default, $is_enabled, $subclass) =
-              $dbh->selectrow_array(
-                 q{SELECT default_value, is_enabled, subclass
+                  AND profile_setting.user_id = ?}, undef, $setting_name, $user_id
+    );
+
+    # if not defined, then grab the default value
+    if (!defined $value) {
+      ($default, $is_enabled, $subclass) = $dbh->selectrow_array(
+        q{SELECT default_value, is_enabled, subclass
                    FROM setting
-                   WHERE name = ?},
-              undef,
-              $setting_name);
-        }
-
-        $self->{'is_enabled'} = $is_enabled;
-        $self->{'default_value'} = $default;
-
-        # IF the setting is enabled, AND the user has chosen a setting
-        # THEN return that value
-        # ELSE return the site default, and note that it is the default.
-        if ( ($is_enabled) && (defined $value) ) {
-            $self->{'value'} = $value;
-        } else {
-            $self->{'value'} = $default;
-            $self->{'isdefault'} = 1;
-        }
-    }
-    else {
-        # If the values were passed in, simply assign them and return.
-        $self->{'is_enabled'}    = shift;
-        $self->{'default_value'} = shift;
-        $self->{'value'}         = shift;
-        $self->{'is_default'}    = shift;
-        $subclass                = shift;
-    }
-    if ($subclass) {
-        eval('require ' . $class . '::' . $subclass);
-        $@ && ThrowCodeError('setting_subclass_invalid',
-                             {'subclass' => $subclass});
-        $class = $class . '::' . $subclass;
+                   WHERE name = ?}, undef, $setting_name
+      );
     }
-    bless($self, $class);
 
-    $self->{'_setting_name'} = $setting_name;
-    $self->{'_user_id'}      = $user_id;
+    $self->{'is_enabled'}    = $is_enabled;
+    $self->{'default_value'} = $default;
 
-    return $self;
+    # IF the setting is enabled, AND the user has chosen a setting
+    # THEN return that value
+    # ELSE return the site default, and note that it is the default.
+    if (($is_enabled) && (defined $value)) {
+      $self->{'value'} = $value;
+    }
+    else {
+      $self->{'value'}     = $default;
+      $self->{'isdefault'} = 1;
+    }
+  }
+  else {
+    # If the values were passed in, simply assign them and return.
+    $self->{'is_enabled'}    = shift;
+    $self->{'default_value'} = shift;
+    $self->{'value'}         = shift;
+    $self->{'is_default'}    = shift;
+    $subclass                = shift;
+  }
+  if ($subclass) {
+    eval('require ' . $class . '::' . $subclass);
+    $@ && ThrowCodeError('setting_subclass_invalid', {'subclass' => $subclass});
+    $class = $class . '::' . $subclass;
+  }
+  bless($self, $class);
+
+  $self->{'_setting_name'} = $setting_name;
+  $self->{'_user_id'}      = $user_id;
+
+  return $self;
 }
 
 ###############################
@@ -120,191 +116,205 @@ sub new {
 ###############################
 
 sub add_setting {
-    my ($name, $values, $default_value, $subclass, $force_check,
-        $silently) = @_;
-    my $dbh = Bugzilla->dbh;
-
-    my $exists = _setting_exists($name);
-    return if ($exists && !$force_check);
-
-    ($name && length( $default_value // '' ))
-      ||  ThrowCodeError("setting_info_invalid");
-
-    if ($exists) {
-        # If this setting exists, we delete it and regenerate it.
-        $dbh->do('DELETE FROM setting_value WHERE name = ?', undef, $name);
-        $dbh->do('DELETE FROM setting WHERE name = ?', undef, $name);
-        # Remove obsolete user preferences for this setting.
-        if (defined $values && scalar(@$values)) {
-            my $list = join(', ', map {$dbh->quote($_)} @$values);
-            $dbh->do("DELETE FROM profile_setting
-                      WHERE setting_name = ? AND setting_value NOT IN ($list)",
-                      undef, $name);
-        }
-    }
-    elsif (!$silently) {
-        print get_text('install_setting_new', { name => $name }) . "\n";
-    }
-    $dbh->do(q{INSERT INTO setting (name, default_value, is_enabled, subclass)
-                    VALUES (?, ?, 1, ?)},
-             undef, ($name, $default_value, $subclass));
+  my ($name, $values, $default_value, $subclass, $force_check, $silently) = @_;
+  my $dbh = Bugzilla->dbh;
+
+  my $exists = _setting_exists($name);
+  return if ($exists && !$force_check);
+
+  ($name && length($default_value // ''))
+    || ThrowCodeError("setting_info_invalid");
 
-    my $sth = $dbh->prepare(q{INSERT INTO setting_value (name, value, sortindex)
-                                    VALUES (?, ?, ?)});
+  if ($exists) {
 
-    my $sortindex = 5;
-    foreach my $key (@$values){
-        $sth->execute($name, $key, $sortindex);
-        $sortindex += 5;
+    # If this setting exists, we delete it and regenerate it.
+    $dbh->do('DELETE FROM setting_value WHERE name = ?', undef, $name);
+    $dbh->do('DELETE FROM setting WHERE name = ?',       undef, $name);
+
+    # Remove obsolete user preferences for this setting.
+    if (defined $values && scalar(@$values)) {
+      my $list = join(', ', map { $dbh->quote($_) } @$values);
+      $dbh->do(
+        "DELETE FROM profile_setting
+                      WHERE setting_name = ? AND setting_value NOT IN ($list)", undef,
+        $name
+      );
     }
+  }
+  elsif (!$silently) {
+    print get_text('install_setting_new', {name => $name}) . "\n";
+  }
+  $dbh->do(
+    q{INSERT INTO setting (name, default_value, is_enabled, subclass)
+                    VALUES (?, ?, 1, ?)}, undef, ($name, $default_value, $subclass)
+  );
+
+  my $sth = $dbh->prepare(
+    q{INSERT INTO setting_value (name, value, sortindex)
+                                    VALUES (?, ?, ?)}
+  );
+
+  my $sortindex = 5;
+  foreach my $key (@$values) {
+    $sth->execute($name, $key, $sortindex);
+    $sortindex += 5;
+  }
 }
 
 sub get_all_settings {
-    my ($user_id) = @_;
-    my $settings = {};
-    my $dbh = Bugzilla->dbh;
-
-    my $cache_key = "user_settings.$user_id";
-    my $rows = Bugzilla->memcached->get_config({ key => $cache_key });
-    if (!$rows) {
-        $rows = $dbh->selectall_arrayref(
-            q{SELECT name, default_value, is_enabled, setting_value, subclass
+  my ($user_id) = @_;
+  my $settings  = {};
+  my $dbh       = Bugzilla->dbh;
+
+  my $cache_key = "user_settings.$user_id";
+  my $rows = Bugzilla->memcached->get_config({key => $cache_key});
+  if (!$rows) {
+    $rows = $dbh->selectall_arrayref(
+      q{SELECT name, default_value, is_enabled, setting_value, subclass
                 FROM setting
            LEFT JOIN profile_setting
                      ON setting.name = profile_setting.setting_name
-                     AND profile_setting.user_id = ?}, undef, ($user_id));
-        Bugzilla->memcached->set_config({ key => $cache_key, data => $rows });
-    }
+                     AND profile_setting.user_id = ?}, undef, ($user_id)
+    );
+    Bugzilla->memcached->set_config({key => $cache_key, data => $rows});
+  }
 
-    foreach my $row (@$rows) {
-        my ($name, $default_value, $is_enabled, $value, $subclass) = @$row;
+  foreach my $row (@$rows) {
+    my ($name, $default_value, $is_enabled, $value, $subclass) = @$row;
 
-        my $is_default;
+    my $is_default;
 
-        if ( ($is_enabled) && (defined $value) ) {
-            $is_default = 0;
-        } else {
-            $value = $default_value;
-            $is_default = 1;
-        }
-
-        $settings->{$name} = new Bugzilla::User::Setting(
-           $name, $user_id, $is_enabled,
-           $default_value, $value, $is_default, $subclass);
+    if (($is_enabled) && (defined $value)) {
+      $is_default = 0;
     }
+    else {
+      $value      = $default_value;
+      $is_default = 1;
+    }
+
+    $settings->{$name}
+      = new Bugzilla::User::Setting($name, $user_id, $is_enabled, $default_value,
+      $value, $is_default, $subclass);
+  }
 
-    return $settings;
+  return $settings;
 }
 
 sub clear_settings_cache {
-    my ($user_id) = @_;
-    Bugzilla->memcached->clear_config({ key => "user_settings.$user_id" });
+  my ($user_id) = @_;
+  Bugzilla->memcached->clear_config({key => "user_settings.$user_id"});
 }
 
 sub get_defaults {
-    my ($user_id) = @_;
-    my $dbh = Bugzilla->dbh;
-    my $default_settings = {};
+  my ($user_id)        = @_;
+  my $dbh              = Bugzilla->dbh;
+  my $default_settings = {};
 
-    $user_id ||= 0;
+  $user_id ||= 0;
 
-    my $rows = $dbh->selectall_arrayref(q{SELECT name, default_value, is_enabled, subclass
-                                            FROM setting});
+  my $rows = $dbh->selectall_arrayref(
+    q{SELECT name, default_value, is_enabled, subclass
+                                            FROM setting}
+  );
 
-    foreach my $row (@$rows) {
-        my ($name, $default_value, $is_enabled, $subclass) = @$row;
+  foreach my $row (@$rows) {
+    my ($name, $default_value, $is_enabled, $subclass) = @$row;
 
-        $default_settings->{$name} = new Bugzilla::User::Setting(
-            $name, $user_id, $is_enabled, $default_value, $default_value, 1,
-            $subclass);
-    }
+    $default_settings->{$name}
+      = new Bugzilla::User::Setting($name, $user_id, $is_enabled, $default_value,
+      $default_value, 1, $subclass);
+  }
 
-    return $default_settings;
+  return $default_settings;
 }
 
 sub set_default {
-    my ($setting_name, $default_value, $is_enabled) = @_;
-    my $dbh = Bugzilla->dbh;
+  my ($setting_name, $default_value, $is_enabled) = @_;
+  my $dbh = Bugzilla->dbh;
 
-    my $sth = $dbh->prepare(q{UPDATE setting
+  my $sth = $dbh->prepare(
+    q{UPDATE setting
                                  SET default_value = ?, is_enabled = ?
-                               WHERE name = ?});
-    $sth->execute($default_value, $is_enabled, $setting_name);
+                               WHERE name = ?}
+  );
+  $sth->execute($default_value, $is_enabled, $setting_name);
 }
 
 sub _setting_exists {
-    my ($setting_name) = @_;
-    my $dbh = Bugzilla->dbh;
-    return $dbh->selectrow_arrayref(
-        "SELECT 1 FROM setting WHERE name = ?", undef, $setting_name) || 0;
+  my ($setting_name) = @_;
+  my $dbh = Bugzilla->dbh;
+  return $dbh->selectrow_arrayref("SELECT 1 FROM setting WHERE name = ?",
+    undef, $setting_name)
+    || 0;
 }
 
 
 sub legal_values {
-    my ($self) = @_;
+  my ($self) = @_;
 
-    return $self->{'legal_values'} if defined $self->{'legal_values'};
+  return $self->{'legal_values'} if defined $self->{'legal_values'};
 
-    my $dbh = Bugzilla->dbh;
-    $self->{'legal_values'} = $dbh->selectcol_arrayref(
-              q{SELECT value
+  my $dbh = Bugzilla->dbh;
+  $self->{'legal_values'} = $dbh->selectcol_arrayref(
+    q{SELECT value
                   FROM setting_value
                  WHERE name = ?
-              ORDER BY sortindex},
-        undef, $self->{'_setting_name'});
+              ORDER BY sortindex}, undef, $self->{'_setting_name'}
+  );
 
-    return $self->{'legal_values'};
+  return $self->{'legal_values'};
 }
 
 sub validate_value {
-    my $self = shift;
-
-    if (grep(/^$_[0]$/, @{$self->legal_values()})) {
-        trick_taint($_[0]);
-    }
-    else {
-        ThrowCodeError('setting_value_invalid',
-                       {'name'  => $self->{'_setting_name'},
-                        'value' => $_[0]});
-    }
+  my $self = shift;
+
+  if (grep(/^$_[0]$/, @{$self->legal_values()})) {
+    trick_taint($_[0]);
+  }
+  else {
+    ThrowCodeError('setting_value_invalid',
+      {'name' => $self->{'_setting_name'}, 'value' => $_[0]});
+  }
 }
 
 sub reset_to_default {
-    my ($self) = @_;
+  my ($self) = @_;
 
-    my $dbh = Bugzilla->dbh;
-    my $sth = $dbh->do(q{ DELETE
+  my $dbh = Bugzilla->dbh;
+  my $sth = $dbh->do(
+    q{ DELETE
                             FROM profile_setting
                            WHERE setting_name = ?
-                             AND user_id = ?},
-                       undef, $self->{'_setting_name'}, $self->{'_user_id'});
-      $self->{'value'}       = $self->{'default_value'};
-      $self->{'is_default'}  = 1;
+                             AND user_id = ?}, undef, $self->{'_setting_name'},
+    $self->{'_user_id'}
+  );
+  $self->{'value'}      = $self->{'default_value'};
+  $self->{'is_default'} = 1;
 }
 
 sub set {
-    my ($self, $value) = @_;
-    my $dbh = Bugzilla->dbh;
-    my $query;
+  my ($self, $value) = @_;
+  my $dbh = Bugzilla->dbh;
+  my $query;
 
-    if ($self->{'is_default'}) {
-        $query = q{INSERT INTO profile_setting
+  if ($self->{'is_default'}) {
+    $query = q{INSERT INTO profile_setting
                    (setting_value, setting_name, user_id)
                    VALUES (?,?,?)};
-    } else {
-        $query = q{UPDATE profile_setting
+  }
+  else {
+    $query = q{UPDATE profile_setting
                       SET setting_value = ?
                     WHERE setting_name = ?
                       AND user_id = ?};
-    }
-    $dbh->do($query, undef, $value, $self->{'_setting_name'}, $self->{'_user_id'});
+  }
+  $dbh->do($query, undef, $value, $self->{'_setting_name'}, $self->{'_user_id'});
 
-    $self->{'value'}       = $value;
-    $self->{'is_default'}  = 0;
+  $self->{'value'}      = $value;
+  $self->{'is_default'} = 0;
 }
 
 
-
 1;
 
 __END__
diff --git a/Bugzilla/User/Setting/Lang.pm b/Bugzilla/User/Setting/Lang.pm
index d980b7a92..d1aeb3421 100644
--- a/Bugzilla/User/Setting/Lang.pm
+++ b/Bugzilla/User/Setting/Lang.pm
@@ -16,11 +16,11 @@ use parent qw(Bugzilla::User::Setting);
 use Bugzilla::Constants;
 
 sub legal_values {
-    my ($self) = @_;
+  my ($self) = @_;
 
-    return $self->{'legal_values'} if defined $self->{'legal_values'};
+  return $self->{'legal_values'} if defined $self->{'legal_values'};
 
-    return $self->{'legal_values'} = Bugzilla->languages;
+  return $self->{'legal_values'} = Bugzilla->languages;
 }
 
 1;
diff --git a/Bugzilla/User/Setting/Skin.pm b/Bugzilla/User/Setting/Skin.pm
index 7b0688c0c..0447b02ab 100644
--- a/Bugzilla/User/Setting/Skin.pm
+++ b/Bugzilla/User/Setting/Skin.pm
@@ -21,24 +21,26 @@ use File::Basename;
 use constant BUILTIN_SKIN_NAMES => ['standard'];
 
 sub legal_values {
-    my ($self) = @_;
+  my ($self) = @_;
 
-    return $self->{'legal_values'} if defined $self->{'legal_values'};
+  return $self->{'legal_values'} if defined $self->{'legal_values'};
 
-    my $dirbase = bz_locations()->{'skinsdir'} . '/contrib';
-    # Avoid modification of the list BUILTIN_SKIN_NAMES points to by copying the
-    # list over instead of simply writing $legal_values = BUILTIN_SKIN_NAMES.
-    my @legal_values = @{(BUILTIN_SKIN_NAMES)};
+  my $dirbase = bz_locations()->{'skinsdir'} . '/contrib';
 
-    foreach my $direntry (glob(catdir($dirbase, '*'))) {
-        if (-d $direntry) {
-            next if basename($direntry) =~ /^cvs$/i;
-            # Stylesheet set found
-            push(@legal_values, basename($direntry));
-        }
+  # Avoid modification of the list BUILTIN_SKIN_NAMES points to by copying the
+  # list over instead of simply writing $legal_values = BUILTIN_SKIN_NAMES.
+  my @legal_values = @{(BUILTIN_SKIN_NAMES)};
+
+  foreach my $direntry (glob(catdir($dirbase, '*'))) {
+    if (-d $direntry) {
+      next if basename($direntry) =~ /^cvs$/i;
+
+      # Stylesheet set found
+      push(@legal_values, basename($direntry));
     }
+  }
 
-    return $self->{'legal_values'} = \@legal_values;
+  return $self->{'legal_values'} = \@legal_values;
 }
 
 1;
diff --git a/Bugzilla/User/Setting/Timezone.pm b/Bugzilla/User/Setting/Timezone.pm
index 8959d1dda..b6b2503b5 100644
--- a/Bugzilla/User/Setting/Timezone.pm
+++ b/Bugzilla/User/Setting/Timezone.pm
@@ -18,19 +18,21 @@ use parent qw(Bugzilla::User::Setting);
 use Bugzilla::Constants;
 
 sub legal_values {
-    my ($self) = @_;
+  my ($self) = @_;
 
-    return $self->{'legal_values'} if defined $self->{'legal_values'};
+  return $self->{'legal_values'} if defined $self->{'legal_values'};
 
-    my @timezones = DateTime::TimeZone->all_names;
-    # Remove old formats, such as CST6CDT, EST, EST5EDT.
-    @timezones = grep { $_ =~ m#.+/.+#} @timezones;
-    # Append 'local' to the list, which will use the timezone
-    # given by the server.
-    push(@timezones, 'local');
-    push(@timezones, 'UTC');
+  my @timezones = DateTime::TimeZone->all_names;
 
-    return $self->{'legal_values'} = \@timezones;
+  # Remove old formats, such as CST6CDT, EST, EST5EDT.
+  @timezones = grep { $_ =~ m#.+/.+# } @timezones;
+
+  # Append 'local' to the list, which will use the timezone
+  # given by the server.
+  push(@timezones, 'local');
+  push(@timezones, 'UTC');
+
+  return $self->{'legal_values'} = \@timezones;
 }
 
 1;
diff --git a/Bugzilla/UserAgent.pm b/Bugzilla/UserAgent.pm
index 14637038c..1995cc82f 100644
--- a/Bugzilla/UserAgent.pm
+++ b/Bugzilla/UserAgent.pm
@@ -20,176 +20,200 @@ use List::MoreUtils qw(natatime);
 use constant DEFAULT_VALUE => 'Other';
 
 use constant PLATFORMS_MAP => (
-    # PowerPC
-    qr/\(.*PowerPC.*\)/i => ["PowerPC", "Macintosh"],
-    # AMD64, Intel x86_64
-    qr/\(.*[ix0-9]86 (?:on |\()x86_64.*\)/ => ["IA32", "x86", "PC"],
-    qr/\(.*amd64.*\)/ => ["AMD64", "x86_64", "PC"],
-    qr/\(.*x86_64.*\)/ => ["AMD64", "x86_64", "PC"],
-    # Intel IA64
-    qr/\(.*IA64.*\)/ => ["IA64", "PC"],
-    # Intel x86
-    qr/\(.*Intel.*\)/ => ["IA32", "x86", "PC"],
-    qr/\(.*[ix0-9]86.*\)/ => ["IA32", "x86", "PC"],
-    # Versions of Windows that only run on Intel x86
-    qr/\(.*Win(?:dows |)[39M].*\)/ => ["IA32", "x86", "PC"],
-    qr/\(.*Win(?:dows |)16.*\)/ => ["IA32", "x86", "PC"],
-    # Sparc
-    qr/\(.*sparc.*\)/ => ["Sparc", "Sun"],
-    qr/\(.*sun4.*\)/ => ["Sparc", "Sun"],
-    # Alpha
-    qr/\(.*AXP.*\)/i => ["Alpha", "DEC"],
-    qr/\(.*[ _]Alpha.\D/i => ["Alpha", "DEC"],
-    qr/\(.*[ _]Alpha\)/i => ["Alpha", "DEC"],
-    # MIPS
-    qr/\(.*IRIX.*\)/i => ["MIPS", "SGI"],
-    qr/\(.*MIPS.*\)/i => ["MIPS", "SGI"],
-    # 68k
-    qr/\(.*68K.*\)/ => ["68k", "Macintosh"],
-    qr/\(.*680[x0]0.*\)/ => ["68k", "Macintosh"],
-    # 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"],
-    # PowerPC
-    qr/\(.*PPC.*\)/ => ["PowerPC", "Macintosh"],
-    qr/\(.*AIX.*\)/ => ["PowerPC", "Macintosh"],
-    # Stereotypical and broken
-    qr/\(.*Windows CE.*\)/ => ["ARM", "PocketPC"],
-    qr/\(.*Macintosh.*\)/ => ["68k", "Macintosh"],
-    qr/\(.*Mac OS [89].*\)/ => ["68k", "Macintosh"],
-    qr/\(.*WOW64.*\)/ => ["x86_64"],
-    qr/\(.*Win64.*\)/ => ["IA64"],
-    qr/\(Win.*\)/ => ["IA32", "x86", "PC"],
-    qr/\(.*Win(?:dows[ -])NT.*\)/ => ["IA32", "x86", "PC"],
-    qr/\(.*OSF.*\)/ => ["Alpha", "DEC"],
-    qr/\(.*HP-?UX.*\)/i => ["PA-RISC", "HP"],
-    qr/\(.*IRIX.*\)/i => ["MIPS", "SGI"],
-    qr/\(.*(SunOS|Solaris).*\)/ => ["Sparc", "Sun"],
-    # Braindead old browsers who didn't follow convention:
-    qr/Amiga/ => ["68k", "Macintosh"],
-    qr/WinMosaic/ => ["IA32", "x86", "PC"],
+
+  # PowerPC
+  qr/\(.*PowerPC.*\)/i => ["PowerPC", "Macintosh"],
+
+  # AMD64, Intel x86_64
+  qr/\(.*[ix0-9]86 (?:on |\()x86_64.*\)/ => ["IA32",  "x86",    "PC"],
+  qr/\(.*amd64.*\)/                      => ["AMD64", "x86_64", "PC"],
+  qr/\(.*x86_64.*\)/                     => ["AMD64", "x86_64", "PC"],
+
+  # Intel IA64
+  qr/\(.*IA64.*\)/ => ["IA64", "PC"],
+
+  # Intel x86
+  qr/\(.*Intel.*\)/     => ["IA32", "x86", "PC"],
+  qr/\(.*[ix0-9]86.*\)/ => ["IA32", "x86", "PC"],
+
+  # Versions of Windows that only run on Intel x86
+  qr/\(.*Win(?:dows |)[39M].*\)/ => ["IA32", "x86", "PC"],
+  qr/\(.*Win(?:dows |)16.*\)/    => ["IA32", "x86", "PC"],
+
+  # Sparc
+  qr/\(.*sparc.*\)/ => ["Sparc", "Sun"],
+  qr/\(.*sun4.*\)/  => ["Sparc", "Sun"],
+
+  # Alpha
+  qr/\(.*AXP.*\)/i      => ["Alpha", "DEC"],
+  qr/\(.*[ _]Alpha.\D/i => ["Alpha", "DEC"],
+  qr/\(.*[ _]Alpha\)/i  => ["Alpha", "DEC"],
+
+  # MIPS
+  qr/\(.*IRIX.*\)/i => ["MIPS", "SGI"],
+  qr/\(.*MIPS.*\)/i => ["MIPS", "SGI"],
+
+  # 68k
+  qr/\(.*68K.*\)/      => ["68k", "Macintosh"],
+  qr/\(.*680[x0]0.*\)/ => ["68k", "Macintosh"],
+
+  # 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"],
+
+  # PowerPC
+  qr/\(.*PPC.*\)/ => ["PowerPC", "Macintosh"],
+  qr/\(.*AIX.*\)/ => ["PowerPC", "Macintosh"],
+
+  # Stereotypical and broken
+  qr/\(.*Windows CE.*\)/        => ["ARM",     "PocketPC"],
+  qr/\(.*Macintosh.*\)/         => ["68k",     "Macintosh"],
+  qr/\(.*Mac OS [89].*\)/       => ["68k",     "Macintosh"],
+  qr/\(.*WOW64.*\)/             => ["x86_64"],
+  qr/\(.*Win64.*\)/             => ["IA64"],
+  qr/\(Win.*\)/                 => ["IA32",    "x86", "PC"],
+  qr/\(.*Win(?:dows[ -])NT.*\)/ => ["IA32",    "x86", "PC"],
+  qr/\(.*OSF.*\)/               => ["Alpha",   "DEC"],
+  qr/\(.*HP-?UX.*\)/i           => ["PA-RISC", "HP"],
+  qr/\(.*IRIX.*\)/i             => ["MIPS",    "SGI"],
+  qr/\(.*(SunOS|Solaris).*\)/   => ["Sparc",   "Sun"],
+
+  # Braindead old browsers who didn't follow convention:
+  qr/Amiga/     => ["68k",  "Macintosh"],
+  qr/WinMosaic/ => ["IA32", "x86", "PC"],
 );
 
 use constant OS_MAP => (
-    # Sun
-    qr/\(.*Solaris.*\)/ => ["Solaris"],
-    qr/\(.*SunOS 5.11.*\)/ => [("OpenSolaris", "Opensolaris", "Solaris 11")],
-    qr/\(.*SunOS 5.10.*\)/ => ["Solaris 10"],
-    qr/\(.*SunOS 5.9.*\)/ => ["Solaris 9"],
-    qr/\(.*SunOS 5.8.*\)/ => ["Solaris 8"],
-    qr/\(.*SunOS 5.7.*\)/ => ["Solaris 7"],
-    qr/\(.*SunOS 5.6.*\)/ => ["Solaris 6"],
-    qr/\(.*SunOS 5.5.*\)/ => ["Solaris 5"],
-    qr/\(.*SunOS 5.*\)/ => ["Solaris"],
-    qr/\(.*SunOS.*sun4u.*\)/ => ["Solaris"],
-    qr/\(.*SunOS.*i86pc.*\)/ => ["Solaris"],
-    qr/\(.*SunOS.*\)/ => ["SunOS"],
-    # BSD
-    qr/\(.*BSD\/(?:OS|386).*\)/ => ["BSDI"],
-    qr/\(.*FreeBSD.*\)/ => ["FreeBSD"],
-    qr/\(.*OpenBSD.*\)/ => ["OpenBSD"],
-    qr/\(.*NetBSD.*\)/ => ["NetBSD"],
-    # Misc POSIX
-    qr/\(.*IRIX.*\)/ => ["IRIX"],
-    qr/\(.*OSF.*\)/ => ["OSF/1"],
-    qr/\(.*Linux.*\)/ => ["Linux"],
-    qr/\(.*BeOS.*\)/ => ["BeOS"],
-    qr/\(.*AIX.*\)/ => ["AIX"],
-    qr/\(.*OS\/2.*\)/ => ["OS/2"],
-    qr/\(.*QNX.*\)/ => ["Neutrino"],
-    qr/\(.*VMS.*\)/ => ["OpenVMS"],
-    qr/\(.*HP-?UX.*\)/ => ["HP-UX"],
-    qr/\(.*Android.*\)/ => ["Android"],
-    # Windows
-    qr/\(.*Windows XP.*\)/ => ["Windows XP"],
-    qr/\(.*Windows NT 10\.0.*\)/ => ["Windows 10"],
-    qr/\(.*Windows NT 6\.4.*\)/ => ["Windows 10"],
-    qr/\(.*Windows NT 6\.3.*\)/ => ["Windows 8.1"],
-    qr/\(.*Windows NT 6\.2.*\)/ => ["Windows 8"],
-    qr/\(.*Windows NT 6\.1.*\)/ => ["Windows 7"],
-    qr/\(.*Windows NT 6\.0.*\)/ => ["Windows Vista"],
-    qr/\(.*Windows NT 5\.2.*\)/ => ["Windows Server 2003"],
-    qr/\(.*Windows NT 5\.1.*\)/ => ["Windows XP"],
-    qr/\(.*Windows 2000.*\)/ => ["Windows 2000"],
-    qr/\(.*Windows NT 5.*\)/ => ["Windows 2000"],
-    qr/\(.*Win.*9[8x].*4\.9.*\)/ => ["Windows ME"],
-    qr/\(.*Win(?:dows |)M[Ee].*\)/ => ["Windows ME"],
-    qr/\(.*Win(?:dows |)98.*\)/ => ["Windows 98"],
-    qr/\(.*Win(?:dows |)95.*\)/ => ["Windows 95"],
-    qr/\(.*Win(?:dows |)16.*\)/ => ["Windows 3.1"],
-    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"],
-    qr/\(.*Mac OS X (?:|Mach-O |\()10.5.*\)/ => ["Mac OS X 10.5"],
-    qr/\(.*Mac OS X (?:|Mach-O |\()10.4.*\)/ => ["Mac OS X 10.4"],
-    qr/\(.*Mac OS X (?:|Mach-O |\()10.3.*\)/ => ["Mac OS X 10.3"],
-    qr/\(.*Mac OS X (?:|Mach-O |\()10.2.*\)/ => ["Mac OS X 10.2"],
-    qr/\(.*Mac OS X (?:|Mach-O |\()10.1.*\)/ => ["Mac OS X 10.1"],
-    # Unfortunately, OS X 10.4 was the first to support Intel. This is fallback
-    # support because some browsers refused to include the OS Version.
-    qr/\(.*Intel.*Mac OS X.*\)/ => ["Mac OS X 10.4"],
-    # OS X 10.3 is the most likely default version of PowerPC Macs
-    # OS X 10.0 is more for configurations which didn't setup 10.x versions
-    qr/\(.*Mac OS X.*\)/ => [("Mac OS X 10.3", "Mac OS X 10.0", "Mac OS X")],
-    qr/\(.*Mac OS 9.*\)/ => [("Mac System 9.x", "Mac System 9.0")],
-    qr/\(.*Mac OS 8\.6.*\)/ => [("Mac System 8.6", "Mac System 8.5")],
-    qr/\(.*Mac OS 8\.5.*\)/ => ["Mac System 8.5"],
-    qr/\(.*Mac OS 8\.1.*\)/ => [("Mac System 8.1", "Mac System 8.0")],
-    qr/\(.*Mac OS 8\.0.*\)/ => ["Mac System 8.0"],
-    qr/\(.*Mac OS 8[^.].*\)/ => ["Mac System 8.0"],
-    qr/\(.*Mac OS 8.*\)/ => ["Mac System 8.6"],
-    qr/\(.*Darwin.*\)/ => [("Mac OS X 10.0", "Mac OS X")],
-    # Silly
-    qr/\(.*Mac.*PowerPC.*\)/ => ["Mac System 9.x"],
-    qr/\(.*Mac.*PPC.*\)/ => ["Mac System 9.x"],
-    qr/\(.*Mac.*68k.*\)/ => ["Mac System 8.0"],
-    # Evil
-    qr/Amiga/i => ["Other"],
-    qr/WinMosaic/ => ["Windows 95"],
-    qr/\(.*32bit.*\)/ => ["Windows 95"],
-    qr/\(.*16bit.*\)/ => ["Windows 3.1"],
-    qr/\(.*PowerPC.*\)/ => ["Mac System 9.x"],
-    qr/\(.*PPC.*\)/ => ["Mac System 9.x"],
-    qr/\(.*68K.*\)/ => ["Mac System 8.0"],
+
+  # Sun
+  qr/\(.*Solaris.*\)/      => ["Solaris"],
+  qr/\(.*SunOS 5.11.*\)/   => [("OpenSolaris", "Opensolaris", "Solaris 11")],
+  qr/\(.*SunOS 5.10.*\)/   => ["Solaris 10"],
+  qr/\(.*SunOS 5.9.*\)/    => ["Solaris 9"],
+  qr/\(.*SunOS 5.8.*\)/    => ["Solaris 8"],
+  qr/\(.*SunOS 5.7.*\)/    => ["Solaris 7"],
+  qr/\(.*SunOS 5.6.*\)/    => ["Solaris 6"],
+  qr/\(.*SunOS 5.5.*\)/    => ["Solaris 5"],
+  qr/\(.*SunOS 5.*\)/      => ["Solaris"],
+  qr/\(.*SunOS.*sun4u.*\)/ => ["Solaris"],
+  qr/\(.*SunOS.*i86pc.*\)/ => ["Solaris"],
+  qr/\(.*SunOS.*\)/        => ["SunOS"],
+
+  # BSD
+  qr/\(.*BSD\/(?:OS|386).*\)/ => ["BSDI"],
+  qr/\(.*FreeBSD.*\)/         => ["FreeBSD"],
+  qr/\(.*OpenBSD.*\)/         => ["OpenBSD"],
+  qr/\(.*NetBSD.*\)/          => ["NetBSD"],
+
+  # Misc POSIX
+  qr/\(.*IRIX.*\)/    => ["IRIX"],
+  qr/\(.*OSF.*\)/     => ["OSF/1"],
+  qr/\(.*Linux.*\)/   => ["Linux"],
+  qr/\(.*BeOS.*\)/    => ["BeOS"],
+  qr/\(.*AIX.*\)/     => ["AIX"],
+  qr/\(.*OS\/2.*\)/   => ["OS/2"],
+  qr/\(.*QNX.*\)/     => ["Neutrino"],
+  qr/\(.*VMS.*\)/     => ["OpenVMS"],
+  qr/\(.*HP-?UX.*\)/  => ["HP-UX"],
+  qr/\(.*Android.*\)/ => ["Android"],
+
+  # Windows
+  qr/\(.*Windows XP.*\)/         => ["Windows XP"],
+  qr/\(.*Windows NT 10\.0.*\)/   => ["Windows 10"],
+  qr/\(.*Windows NT 6\.4.*\)/    => ["Windows 10"],
+  qr/\(.*Windows NT 6\.3.*\)/    => ["Windows 8.1"],
+  qr/\(.*Windows NT 6\.2.*\)/    => ["Windows 8"],
+  qr/\(.*Windows NT 6\.1.*\)/    => ["Windows 7"],
+  qr/\(.*Windows NT 6\.0.*\)/    => ["Windows Vista"],
+  qr/\(.*Windows NT 5\.2.*\)/    => ["Windows Server 2003"],
+  qr/\(.*Windows NT 5\.1.*\)/    => ["Windows XP"],
+  qr/\(.*Windows 2000.*\)/       => ["Windows 2000"],
+  qr/\(.*Windows NT 5.*\)/       => ["Windows 2000"],
+  qr/\(.*Win.*9[8x].*4\.9.*\)/   => ["Windows ME"],
+  qr/\(.*Win(?:dows |)M[Ee].*\)/ => ["Windows ME"],
+  qr/\(.*Win(?:dows |)98.*\)/    => ["Windows 98"],
+  qr/\(.*Win(?:dows |)95.*\)/    => ["Windows 95"],
+  qr/\(.*Win(?:dows |)16.*\)/    => ["Windows 3.1"],
+  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"],
+  qr/\(.*Mac OS X (?:|Mach-O |\()10.5.*\)/ => ["Mac OS X 10.5"],
+  qr/\(.*Mac OS X (?:|Mach-O |\()10.4.*\)/ => ["Mac OS X 10.4"],
+  qr/\(.*Mac OS X (?:|Mach-O |\()10.3.*\)/ => ["Mac OS X 10.3"],
+  qr/\(.*Mac OS X (?:|Mach-O |\()10.2.*\)/ => ["Mac OS X 10.2"],
+  qr/\(.*Mac OS X (?:|Mach-O |\()10.1.*\)/ => ["Mac OS X 10.1"],
+
+  # Unfortunately, OS X 10.4 was the first to support Intel. This is fallback
+  # support because some browsers refused to include the OS Version.
+  qr/\(.*Intel.*Mac OS X.*\)/ => ["Mac OS X 10.4"],
+
+  # OS X 10.3 is the most likely default version of PowerPC Macs
+  # OS X 10.0 is more for configurations which didn't setup 10.x versions
+  qr/\(.*Mac OS X.*\)/     => [("Mac OS X 10.3",  "Mac OS X 10.0", "Mac OS X")],
+  qr/\(.*Mac OS 9.*\)/     => [("Mac System 9.x", "Mac System 9.0")],
+  qr/\(.*Mac OS 8\.6.*\)/  => [("Mac System 8.6", "Mac System 8.5")],
+  qr/\(.*Mac OS 8\.5.*\)/  => ["Mac System 8.5"],
+  qr/\(.*Mac OS 8\.1.*\)/  => [("Mac System 8.1", "Mac System 8.0")],
+  qr/\(.*Mac OS 8\.0.*\)/  => ["Mac System 8.0"],
+  qr/\(.*Mac OS 8[^.].*\)/ => ["Mac System 8.0"],
+  qr/\(.*Mac OS 8.*\)/     => ["Mac System 8.6"],
+  qr/\(.*Darwin.*\)/       => [("Mac OS X 10.0",  "Mac OS X")],
+
+  # Silly
+  qr/\(.*Mac.*PowerPC.*\)/ => ["Mac System 9.x"],
+  qr/\(.*Mac.*PPC.*\)/     => ["Mac System 9.x"],
+  qr/\(.*Mac.*68k.*\)/     => ["Mac System 8.0"],
+
+  # Evil
+  qr/Amiga/i          => ["Other"],
+  qr/WinMosaic/       => ["Windows 95"],
+  qr/\(.*32bit.*\)/   => ["Windows 95"],
+  qr/\(.*16bit.*\)/   => ["Windows 3.1"],
+  qr/\(.*PowerPC.*\)/ => ["Mac System 9.x"],
+  qr/\(.*PPC.*\)/     => ["Mac System 9.x"],
+  qr/\(.*68K.*\)/     => ["Mac System 8.0"],
 );
 
 sub detect_platform {
-    my $userAgent = $ENV{'HTTP_USER_AGENT'};
-    my @detected;
-    my $iterator = natatime(2, PLATFORMS_MAP);
-    while (my($re, $ra) = $iterator->()) {
-        if ($userAgent =~ $re) {
-            push @detected, @$ra;
-        }
+  my $userAgent = $ENV{'HTTP_USER_AGENT'};
+  my @detected;
+  my $iterator = natatime(2, PLATFORMS_MAP);
+  while (my ($re, $ra) = $iterator->()) {
+    if ($userAgent =~ $re) {
+      push @detected, @$ra;
     }
-    return _pick_valid_field_value('rep_platform', @detected);
+  }
+  return _pick_valid_field_value('rep_platform', @detected);
 }
 
 sub detect_op_sys {
-    my $userAgent = $ENV{'HTTP_USER_AGENT'} || '';
-    my @detected;
-    my $iterator = natatime(2, OS_MAP);
-    while (my($re, $ra) = $iterator->()) {
-        if ($userAgent =~ $re) {
-            push @detected, @$ra;
-        }
+  my $userAgent = $ENV{'HTTP_USER_AGENT'} || '';
+  my @detected;
+  my $iterator = natatime(2, OS_MAP);
+  while (my ($re, $ra) = $iterator->()) {
+    if ($userAgent =~ $re) {
+      push @detected, @$ra;
     }
-    push(@detected, "Windows") if grep(/^Windows /, @detected);
-    push(@detected, "Mac OS") if grep(/^Mac /, @detected);
-    return _pick_valid_field_value('op_sys', @detected);
+  }
+  push(@detected, "Windows") if grep(/^Windows /, @detected);
+  push(@detected, "Mac OS")  if grep(/^Mac /,     @detected);
+  return _pick_valid_field_value('op_sys', @detected);
 }
 
 # Takes the name of a field and a list of possible values for that field.
@@ -197,11 +221,11 @@ sub detect_op_sys {
 # field.
 # Returns 'Other' if none of the values match.
 sub _pick_valid_field_value {
-    my ($field, @values) = @_;
-    foreach my $value (@values) {
-        return $value if check_field($field, $value, undef, 1);
-    }
-    return DEFAULT_VALUE;
+  my ($field, @values) = @_;
+  foreach my $value (@values) {
+    return $value if check_field($field, $value, undef, 1);
+  }
+  return DEFAULT_VALUE;
 }
 
 1;
diff --git a/Bugzilla/Util.pm b/Bugzilla/Util.pm
index 57ce5f6b6..0edd361ce 100644
--- a/Bugzilla/Util.pm
+++ b/Bugzilla/Util.pm
@@ -13,18 +13,18 @@ use warnings;
 
 use parent 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 i_am_webservice correct_urlbase remote_ip
-                             validate_ip do_ssl_redirect_if_required use_attachbase
-                             diff_arrays on_main_db
-                             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 display_value disable_utf8
-                             detect_encoding email_filter
-                             join_activity_entries read_text write_text);
+  html_quote url_quote xml_quote
+  css_class_quote html_light_quote
+  i_am_cgi i_am_webservice correct_urlbase remote_ip
+  validate_ip do_ssl_redirect_if_required use_attachbase
+  diff_arrays on_main_db
+  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 display_value disable_utf8
+  detect_encoding email_filter
+  join_activity_entries read_text write_text);
 
 use Bugzilla::Constants;
 use Bugzilla::RNG qw(irand);
@@ -43,642 +43,684 @@ use File::Basename qw(dirname);
 use File::Temp qw(tempfile);
 
 sub trick_taint {
-    require Carp;
-    Carp::confess("Undef to trick_taint") unless defined $_[0];
-    my $match = $_[0] =~ /^(.*)$/s;
-    $_[0] = $match ? $1 : undef;
-    return (defined($_[0]));
+  require Carp;
+  Carp::confess("Undef to trick_taint") unless defined $_[0];
+  my $match = $_[0] =~ /^(.*)$/s;
+  $_[0] = $match ? $1 : undef;
+  return (defined($_[0]));
 }
 
 sub detaint_natural {
-    my $match = $_[0] =~ /^([0-9]+)$/;
-    $_[0] = $match ? int($1) : undef;
-    return (defined($_[0]));
+  my $match = $_[0] =~ /^([0-9]+)$/;
+  $_[0] = $match ? int($1) : undef;
+  return (defined($_[0]));
 }
 
 sub detaint_signed {
-    my $match = $_[0] =~ /^([-+]?[0-9]+)$/;
-    # The "int()" call removes any leading plus sign.
-    $_[0] = $match ? int($1) : undef;
-    return (defined($_[0]));
+  my $match = $_[0] =~ /^([-+]?[0-9]+)$/;
+
+  # The "int()" call removes any leading plus sign.
+  $_[0] = $match ? int($1) : undef;
+  return (defined($_[0]));
 }
 
 # Bug 120030: Override html filter to obscure the '@' in user
 #             visible strings.
 # Bug 319331: Handle BiDi disruptions.
 sub html_quote {
-    my $var = shift;
-    $var =~ s/&/&/g;
-    $var =~ s//>/g;
-    $var =~ s/"/"/g;
-    # Obscure '@'.
-    $var =~ s/\@/\@/g;
-
-    state $use_utf8 = Bugzilla->params->{'utf8'};
-
-    if ($use_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:
-        # --------------------------------------------------------
-        # |Code  |Name                      |UTF-8 representation|
-        # |------|--------------------------|--------------------|
-        # |U+202a|Left-To-Right Embedding   |0xe2 0x80 0xaa      |
-        # |U+202b|Right-To-Left Embedding   |0xe2 0x80 0xab      |
-        # |U+202c|Pop Directional Formatting|0xe2 0x80 0xac      |
-        # |U+202d|Left-To-Right Override    |0xe2 0x80 0xad      |
-        # |U+202e|Right-To-Left Override    |0xe2 0x80 0xae      |
-        # --------------------------------------------------------
-        #
-        # The following are characters influencing BiDi, too, but
-        # they can be spared from filtering because they don't
-        # influence more than one character right or left:
-        # --------------------------------------------------------
-        # |Code  |Name                      |UTF-8 representation|
-        # |------|--------------------------|--------------------|
-        # |U+200e|Left-To-Right Mark        |0xe2 0x80 0x8e      |
-        # |U+200f|Right-To-Left Mark        |0xe2 0x80 0x8f      |
-        # --------------------------------------------------------
-        $var =~ tr/\x{202a}-\x{202e}//d;
-    }
-    return $var;
+  my $var = shift;
+  $var =~ s/&/&/g;
+  $var =~ s//>/g;
+  $var =~ s/"/"/g;
+
+  # Obscure '@'.
+  $var =~ s/\@/\@/g;
+
+  state $use_utf8 = Bugzilla->params->{'utf8'};
+
+  if ($use_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:
+    # --------------------------------------------------------
+    # |Code  |Name                      |UTF-8 representation|
+    # |------|--------------------------|--------------------|
+    # |U+202a|Left-To-Right Embedding   |0xe2 0x80 0xaa      |
+    # |U+202b|Right-To-Left Embedding   |0xe2 0x80 0xab      |
+    # |U+202c|Pop Directional Formatting|0xe2 0x80 0xac      |
+    # |U+202d|Left-To-Right Override    |0xe2 0x80 0xad      |
+    # |U+202e|Right-To-Left Override    |0xe2 0x80 0xae      |
+    # --------------------------------------------------------
+    #
+    # The following are characters influencing BiDi, too, but
+    # they can be spared from filtering because they don't
+    # influence more than one character right or left:
+    # --------------------------------------------------------
+    # |Code  |Name                      |UTF-8 representation|
+    # |------|--------------------------|--------------------|
+    # |U+200e|Left-To-Right Mark        |0xe2 0x80 0x8e      |
+    # |U+200f|Right-To-Left Mark        |0xe2 0x80 0x8f      |
+    # --------------------------------------------------------
+    $var =~ tr/\x{202a}-\x{202e}//d;
+  }
+  return $var;
 }
 
 sub read_text {
-    my ($filename) = @_;
-    open my $fh, '<:encoding(utf-8)', $filename;
-    local $/ = undef;
-    my $content = <$fh>;
-    close $fh;
-    return $content;
+  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;
+  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.
-    # There is no need to recreate the HTML::Scrubber object again and again.
-    my $scrubber = Bugzilla->process_cache->{html_scrubber};
-
-    # List of allowed HTML elements having no attributes.
-    my @allow = qw(b strong em i u p br abbr acronym ins del cite code var
-                   dfn samp kbd big small sub sup tt dd dt dl ul li ol
-                   fieldset legend);
-
-    if (!Bugzilla->feature('html_desc')) {
-        my $safe = join('|', @allow);
-        my $chr = chr(1);
-
-        # First, escape safe elements.
-        $text =~ s#<($safe)>#$chr$1$chr#go;
-        $text =~ s##$chr/$1$chr#go;
-        # Now filter < and >.
-        $text =~ s#<#<#g;
-        $text =~ s#>#>#g;
-        # Restore safe elements.
-        $text =~ s#$chr/($safe)$chr##go;
-        $text =~ s#$chr($safe)$chr#<$1>#go;
-        return $text;
-    }
-    elsif (!$scrubber) {
-        # We can be less restrictive. We can accept elements with attributes.
-        push(@allow, qw(a blockquote q span));
-
-        # Allowed protocols.
-        my $safe_protocols = join('|', SAFE_PROTOCOLS);
-        my $protocol_regexp = qr{(^(?:$safe_protocols):|^[^:]+$)}i;
-
-        # Deny all elements and attributes unless explicitly authorized.
-        my @default = (0 => {
-                             id    => 1,
-                             name  => 1,
-                             class => 1,
-                             '*'   => 0, # Reject all other attributes.
-                            }
-                       );
-
-        # Specific rules for allowed elements. If no specific rule is set
-        # for a given element, then the default is used.
-        my @rules = (a => {
-                           href   => $protocol_regexp,
-                           target => qr{^(?:_blank|_parent|_self|_top)$}i,
-                           title  => 1,
-                           id     => 1,
-                           name   => 1,
-                           class  => 1,
-                           '*'    => 0, # Reject all other attributes.
-                          },
-                     blockquote => {
-                                    cite => $protocol_regexp,
-                                    id    => 1,
-                                    name  => 1,
-                                    class => 1,
-                                    '*'  => 0, # Reject all other attributes.
-                                   },
-                     'q' => {
-                             cite => $protocol_regexp,
-                             id    => 1,
-                             name  => 1,
-                             class => 1,
-                             '*'  => 0, # Reject all other attributes.
-                          },
-                    );
-
-        Bugzilla->process_cache->{html_scrubber} = $scrubber =
-          HTML::Scrubber->new(default => \@default,
-                              allow   => \@allow,
-                              rules   => \@rules,
-                              comment => 0,
-                              process => 0);
-    }
-    return $scrubber->scrub($text);
+  my ($text) = @_;
+
+  # admin/table.html.tmpl calls |FILTER html_light| many times.
+  # There is no need to recreate the HTML::Scrubber object again and again.
+  my $scrubber = Bugzilla->process_cache->{html_scrubber};
+
+  # List of allowed HTML elements having no attributes.
+  my @allow = qw(b strong em i u p br abbr acronym ins del cite code var
+    dfn samp kbd big small sub sup tt dd dt dl ul li ol
+    fieldset legend);
+
+  if (!Bugzilla->feature('html_desc')) {
+    my $safe = join('|', @allow);
+    my $chr = chr(1);
+
+    # First, escape safe elements.
+    $text =~ s#<($safe)>#$chr$1$chr#go;
+    $text =~ s##$chr/$1$chr#go;
+
+    # Now filter < and >.
+    $text =~ s#<#<#g;
+    $text =~ s#>#>#g;
+
+    # Restore safe elements.
+    $text =~ s#$chr/($safe)$chr##go;
+    $text =~ s#$chr($safe)$chr#<$1>#go;
+    return $text;
+  }
+  elsif (!$scrubber) {
+
+    # We can be less restrictive. We can accept elements with attributes.
+    push(@allow, qw(a blockquote q span));
+
+    # Allowed protocols.
+    my $safe_protocols = join('|', SAFE_PROTOCOLS);
+    my $protocol_regexp = qr{(^(?:$safe_protocols):|^[^:]+$)}i;
+
+    # Deny all elements and attributes unless explicitly authorized.
+    my @default = (
+      0 => {
+        id    => 1,
+        name  => 1,
+        class => 1,
+        '*'   => 0,    # Reject all other attributes.
+      }
+    );
+
+    # Specific rules for allowed elements. If no specific rule is set
+    # for a given element, then the default is used.
+    my @rules = (
+      a => {
+        href   => $protocol_regexp,
+        target => qr{^(?:_blank|_parent|_self|_top)$}i,
+        title  => 1,
+        id     => 1,
+        name   => 1,
+        class  => 1,
+        '*'    => 0,                                      # Reject all other attributes.
+      },
+      blockquote => {
+        cite  => $protocol_regexp,
+        id    => 1,
+        name  => 1,
+        class => 1,
+        '*'   => 0,                                       # Reject all other attributes.
+      },
+      'q' => {
+        cite  => $protocol_regexp,
+        id    => 1,
+        name  => 1,
+        class => 1,
+        '*'   => 0,                                       # Reject all other attributes.
+      },
+    );
+
+    Bugzilla->process_cache->{html_scrubber} = $scrubber = HTML::Scrubber->new(
+      default => \@default,
+      allow   => \@allow,
+      rules   => \@rules,
+      comment => 0,
+      process => 0
+    );
+  }
+  return $scrubber->scrub($text);
 }
 
 sub email_filter {
-    my ($toencode) = @_;
-    if (!Bugzilla->user->id) {
-        my @emails = Email::Address->parse($toencode);
-        if (scalar @emails) {
-            my @hosts = map { quotemeta($_->host) } @emails;
-            my $hosts_re = join('|', @hosts);
-            $toencode =~ s/\@(?:$hosts_re)//g;
-            return $toencode;
-        }
+  my ($toencode) = @_;
+  if (!Bugzilla->user->id) {
+    my @emails = Email::Address->parse($toencode);
+    if (scalar @emails) {
+      my @hosts = map { quotemeta($_->host) } @emails;
+      my $hosts_re = join('|', @hosts);
+      $toencode =~ s/\@(?:$hosts_re)//g;
+      return $toencode;
     }
-    return $toencode;
+  }
+  return $toencode;
 }
 
 # This originally came from CGI.pm, by Lincoln D. Stein
 sub url_quote {
-    my ($toencode) = (@_);
-    utf8::encode($toencode) # The below regex works only on bytes
-        if Bugzilla->params->{'utf8'} && utf8::is_utf8($toencode);
-    $toencode =~ s/([^a-zA-Z0-9_\-.])/uc sprintf("%%%02x",ord($1))/eg;
-    return $toencode;
+  my ($toencode) = (@_);
+  utf8::encode($toencode)    # The below regex works only on bytes
+    if Bugzilla->params->{'utf8'} && utf8::is_utf8($toencode);
+  $toencode =~ s/([^a-zA-Z0-9_\-.])/uc sprintf("%%%02x",ord($1))/eg;
+  return $toencode;
 }
 
 sub css_class_quote {
-    my ($toencode) = (@_);
-    $toencode =~ s#[ /]#_#g;
-    $toencode =~ s/([^a-zA-Z0-9_\-.])/uc sprintf("&#x%x;",ord($1))/eg;
-    return $toencode;
+  my ($toencode) = (@_);
+  $toencode =~ s#[ /]#_#g;
+  $toencode =~ s/([^a-zA-Z0-9_\-.])/uc sprintf("&#x%x;",ord($1))/eg;
+  return $toencode;
 }
 
 sub xml_quote {
-    my ($var) = (@_);
-    $var =~ s/\&/\&/g;
-    $var =~ s//\>/g;
-    $var =~ s/\"/\"/g;
-    $var =~ s/\'/\'/g;
-    
-    # the following nukes characters disallowed by the XML 1.0
-    # spec, Production 2.2. 1.0 declares that only the following 
-    # are valid:
-    # (#x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF])
-    $var =~ s/([\x{0001}-\x{0008}]|
+  my ($var) = (@_);
+  $var =~ s/\&/\&/g;
+  $var =~ s//\>/g;
+  $var =~ s/\"/\"/g;
+  $var =~ s/\'/\'/g;
+
+  # the following nukes characters disallowed by the XML 1.0
+  # spec, Production 2.2. 1.0 declares that only the following
+  # are valid:
+  # (#x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF])
+  $var =~ s/([\x{0001}-\x{0008}]|
                [\x{000B}-\x{000C}]|
                [\x{000E}-\x{001F}]|
                [\x{D800}-\x{DFFF}]|
                [\x{FFFE}-\x{FFFF}])//gx;
-    return $var;
+  return $var;
 }
 
 sub i_am_cgi {
-    # I use SERVER_SOFTWARE because it's required to be
-    # defined for all requests in the CGI spec.
-    return exists $ENV{'SERVER_SOFTWARE'} ? 1 : 0;
+
+  # I use SERVER_SOFTWARE because it's required to be
+  # defined for all requests in the CGI spec.
+  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
-           || $usage_mode == USAGE_MODE_REST;
+  my $usage_mode = Bugzilla->usage_mode;
+  return
+       $usage_mode == USAGE_MODE_XMLRPC
+    || $usage_mode == USAGE_MODE_JSON
+    || $usage_mode == USAGE_MODE_REST;
 }
 
 # 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).
 sub do_ssl_redirect_if_required {
-    return if !i_am_cgi();
-    return if !Bugzilla->params->{'ssl_redirect'};
-
-    my $sslbase = Bugzilla->params->{'sslbase'};
-    
-    # If we're already running under SSL, never redirect.
-    return if uc($ENV{HTTPS} || '') eq 'ON';
-    # Never redirect if there isn't an sslbase.
-    return if !$sslbase;
-    Bugzilla->cgi->redirect_to_https();
+  return if !i_am_cgi();
+  return if !Bugzilla->params->{'ssl_redirect'};
+
+  my $sslbase = Bugzilla->params->{'sslbase'};
+
+  # If we're already running under SSL, never redirect.
+  return if uc($ENV{HTTPS} || '') eq 'ON';
+
+  # Never redirect if there isn't an sslbase.
+  return if !$sslbase;
+  Bugzilla->cgi->redirect_to_https();
 }
 
 sub correct_urlbase {
-    my $ssl = Bugzilla->params->{'ssl_redirect'};
-    my $urlbase = Bugzilla->params->{'urlbase'};
-    my $sslbase = Bugzilla->params->{'sslbase'};
-
-    if (!$sslbase) {
-        return $urlbase;
-    }
-    elsif ($ssl) {
-        return $sslbase;
-    }
-    else {
-        # Return what the user currently uses.
-        return (uc($ENV{HTTPS} || '') eq 'ON') ? $sslbase : $urlbase;
-    }
+  my $ssl     = Bugzilla->params->{'ssl_redirect'};
+  my $urlbase = Bugzilla->params->{'urlbase'};
+  my $sslbase = Bugzilla->params->{'sslbase'};
+
+  if (!$sslbase) {
+    return $urlbase;
+  }
+  elsif ($ssl) {
+    return $sslbase;
+  }
+  else {
+    # Return what the user currently uses.
+    return (uc($ENV{HTTPS} || '') eq 'ON') ? $sslbase : $urlbase;
+  }
 }
 
 sub remote_ip {
-    my $ip = $ENV{'REMOTE_ADDR'} || '127.0.0.1';
-    my @proxies = split(/[\s,]+/, Bugzilla->params->{'inbound_proxies'});
-
-    # If the IP address is one of our trusted proxies, then we look at
-    # the X-Forwarded-For header to determine the real remote IP address.
-    if ($ENV{'HTTP_X_FORWARDED_FOR'} && first { $_ eq $ip } @proxies) {
-        my @ips = split(/[\s,]+/, $ENV{'HTTP_X_FORWARDED_FOR'});
-        # This header can contain several IP addresses. We want the
-        # IP address of the machine which connected to our proxies as
-        # all other IP addresses may be fake or internal ones.
-        # Note that this may block a whole external proxy, but we have
-        # no way to determine if this proxy is malicious or trustable.
-        foreach my $remote_ip (reverse @ips) {
-            if (!first { $_ eq $remote_ip } @proxies) {
-                # Keep the original IP address if the remote IP is invalid.
-                $ip = validate_ip($remote_ip) || $ip;
-                last;
-            }
-        }
+  my $ip = $ENV{'REMOTE_ADDR'} || '127.0.0.1';
+  my @proxies = split(/[\s,]+/, Bugzilla->params->{'inbound_proxies'});
+
+  # If the IP address is one of our trusted proxies, then we look at
+  # the X-Forwarded-For header to determine the real remote IP address.
+  if ($ENV{'HTTP_X_FORWARDED_FOR'} && first { $_ eq $ip } @proxies) {
+    my @ips = split(/[\s,]+/, $ENV{'HTTP_X_FORWARDED_FOR'});
+
+    # This header can contain several IP addresses. We want the
+    # IP address of the machine which connected to our proxies as
+    # all other IP addresses may be fake or internal ones.
+    # Note that this may block a whole external proxy, but we have
+    # no way to determine if this proxy is malicious or trustable.
+    foreach my $remote_ip (reverse @ips) {
+      if (!first { $_ eq $remote_ip } @proxies) {
+
+        # Keep the original IP address if the remote IP is invalid.
+        $ip = validate_ip($remote_ip) || $ip;
+        last;
+      }
     }
-    return $ip;
+  }
+  return $ip;
 }
 
 sub validate_ip {
-    my $ip = shift;
-    return is_ipv4($ip) || is_ipv6($ip);
+  my $ip = shift;
+  return is_ipv4($ip) || is_ipv6($ip);
 }
 
 # Copied from Data::Validate::IP::is_ipv4().
 sub is_ipv4 {
-    my $ip = shift;
-    return unless defined $ip;
+  my $ip = shift;
+  return unless defined $ip;
 
-    my @octets = $ip =~ /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
-    return unless scalar(@octets) == 4;
+  my @octets = $ip =~ /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
+  return unless scalar(@octets) == 4;
 
-    foreach my $octet (@octets) {
-        return unless ($octet >= 0 && $octet <= 255 && $octet !~ /^0\d{1,2}$/);
-    }
+  foreach my $octet (@octets) {
+    return unless ($octet >= 0 && $octet <= 255 && $octet !~ /^0\d{1,2}$/);
+  }
 
-    # The IP address is valid and can now be detainted.
-    return join('.', @octets);
+  # The IP address is valid and can now be detainted.
+  return join('.', @octets);
 }
 
 # Copied from Data::Validate::IP::is_ipv6().
 sub is_ipv6 {
-    my $ip = shift;
-    return unless defined $ip;
-
-    # If there is a :: then there must be only one :: and the length
-    # can be variable. Without it, the length must be 8 groups.
-    my @chunks = split(':', $ip);
-
-    # Need to check if the last chunk is an IPv4 address, if it is we
-    # pop it off and exempt it from the normal IPv6 checking and stick
-    # it back on at the end. If there is only one chunk and it's an IPv4
-    # address, then it isn't an IPv6 address.
-    my $ipv4;
-    my $expected_chunks = 8;
-    if (@chunks > 1 && is_ipv4($chunks[$#chunks])) {
-        $ipv4 = pop(@chunks);
-        $expected_chunks--;
-    }
+  my $ip = shift;
+  return unless defined $ip;
+
+  # If there is a :: then there must be only one :: and the length
+  # can be variable. Without it, the length must be 8 groups.
+  my @chunks = split(':', $ip);
+
+  # Need to check if the last chunk is an IPv4 address, if it is we
+  # pop it off and exempt it from the normal IPv6 checking and stick
+  # it back on at the end. If there is only one chunk and it's an IPv4
+  # address, then it isn't an IPv6 address.
+  my $ipv4;
+  my $expected_chunks = 8;
+  if (@chunks > 1 && is_ipv4($chunks[$#chunks])) {
+    $ipv4 = pop(@chunks);
+    $expected_chunks--;
+  }
+
+  my $empty = 0;
+
+  # Workaround to handle trailing :: being valid.
+  if ($ip =~ /[0-9a-f]{1,4}::$/) {
+    $empty++;
 
-    my $empty = 0;
-    # Workaround to handle trailing :: being valid.
-    if ($ip =~ /[0-9a-f]{1,4}::$/) {
-        $empty++;
     # Single trailing ':' is invalid.
-    } elsif ($ip =~ /:$/) {
-        return;
-    }
+  }
+  elsif ($ip =~ /:$/) {
+    return;
+  }
 
-    foreach my $chunk (@chunks) {
-        return unless $chunk =~ /^[0-9a-f]{0,4}$/i;
-        $empty++ if $chunk eq '';
-    }
-    # More than one :: block is bad, but if it starts with :: it will
-    # look like two, so we need an exception.
-    if ($empty == 2 && $ip =~ /^::/) {
-        # This is ok
-    } elsif ($empty > 1) {
-        return;
-    }
+  foreach my $chunk (@chunks) {
+    return unless $chunk =~ /^[0-9a-f]{0,4}$/i;
+    $empty++ if $chunk eq '';
+  }
+
+  # More than one :: block is bad, but if it starts with :: it will
+  # look like two, so we need an exception.
+  if ($empty == 2 && $ip =~ /^::/) {
+
+    # This is ok
+  }
+  elsif ($empty > 1) {
+    return;
+  }
 
-    push(@chunks, $ipv4) if $ipv4;
-    # Need 8 chunks, or we need an empty section that could be filled
-    # to represent the missing '0' sections.
-    return unless (@chunks == $expected_chunks || @chunks < $expected_chunks && $empty);
+  push(@chunks, $ipv4) if $ipv4;
 
-    my $ipv6 = join(':', @chunks);
-    # The IP address is valid and can now be detainted.
-    trick_taint($ipv6);
+  # Need 8 chunks, or we need an empty section that could be filled
+  # to represent the missing '0' sections.
+  return
+    unless (@chunks == $expected_chunks || @chunks < $expected_chunks && $empty);
 
-    # Need to handle the exception of trailing :: being valid.
-    return "${ipv6}::" if $ip =~ /::$/;
-    return $ipv6;
+  my $ipv6 = join(':', @chunks);
+
+  # The IP address is valid and can now be detainted.
+  trick_taint($ipv6);
+
+  # Need to handle the exception of trailing :: being valid.
+  return "${ipv6}::" if $ip =~ /::$/;
+  return $ipv6;
 }
 
 sub use_attachbase {
-    my $attachbase = Bugzilla->params->{'attachment_base'};
-    return ($attachbase ne ''
-            && $attachbase ne Bugzilla->params->{'urlbase'}
-            && $attachbase ne Bugzilla->params->{'sslbase'}) ? 1 : 0;
+  my $attachbase = Bugzilla->params->{'attachment_base'};
+  return ($attachbase ne ''
+      && $attachbase ne Bugzilla->params->{'urlbase'}
+      && $attachbase ne Bugzilla->params->{'sslbase'}) ? 1 : 0;
 }
 
 sub diff_arrays {
-    my ($old_ref, $new_ref, $attrib) = @_;
-    $attrib ||= 'name';
-
-    my (%counts, %pos);
-    # We are going to alter the old array.
-    my @old = @$old_ref;
-    my $i = 0;
-
-    # $counts{foo}-- means old, $counts{foo}++ means new.
-    # If $counts{foo} becomes positive, then we are adding new items,
-    # else we simply cancel one old existing item. Remaining items
-    # in the old list have been removed.
-    foreach (@old) {
-        next unless defined $_;
-        my $value = blessed($_) ? $_->$attrib : $_;
-        $counts{$value}--;
-        push @{$pos{$value}}, $i++;
+  my ($old_ref, $new_ref, $attrib) = @_;
+  $attrib ||= 'name';
+
+  my (%counts, %pos);
+
+  # We are going to alter the old array.
+  my @old = @$old_ref;
+  my $i   = 0;
+
+  # $counts{foo}-- means old, $counts{foo}++ means new.
+  # If $counts{foo} becomes positive, then we are adding new items,
+  # else we simply cancel one old existing item. Remaining items
+  # in the old list have been removed.
+  foreach (@old) {
+    next unless defined $_;
+    my $value = blessed($_) ? $_->$attrib : $_;
+    $counts{$value}--;
+    push @{$pos{$value}}, $i++;
+  }
+  my @added;
+  foreach (@$new_ref) {
+    next unless defined $_;
+    my $value = blessed($_) ? $_->$attrib : $_;
+    if (++$counts{$value} > 0) {
+
+      # Ignore empty strings, but objects having an empty string
+      # as attribute are fine.
+      push(@added, $_) unless ($value eq '' && !blessed($_));
     }
-    my @added;
-    foreach (@$new_ref) {
-        next unless defined $_;
-        my $value = blessed($_) ? $_->$attrib : $_;
-        if (++$counts{$value} > 0) {
-            # Ignore empty strings, but objects having an empty string
-            # as attribute are fine.
-            push(@added, $_) unless ($value eq '' && !blessed($_));
-        }
-        else {
-            my $old_pos = shift @{$pos{$value}};
-            $old[$old_pos] = undef;
-        }
+    else {
+      my $old_pos = shift @{$pos{$value}};
+      $old[$old_pos] = undef;
     }
-    # Ignore canceled items as well as empty strings.
-    my @removed = grep { defined $_ && $_ ne '' } @old;
-    return (\@removed, \@added);
+  }
+
+  # Ignore canceled items as well as empty strings.
+  my @removed = grep { defined $_ && $_ ne '' } @old;
+  return (\@removed, \@added);
 }
 
 sub trim {
-    my ($str) = @_;
-    if ($str) {
-      $str =~ s/^\s+//g;
-      $str =~ s/\s+$//g;
-    }
-    return $str;
+  my ($str) = @_;
+  if ($str) {
+    $str =~ s/^\s+//g;
+    $str =~ s/\s+$//g;
+  }
+  return $str;
 }
 
 sub wrap_comment {
-    my ($comment, $cols) = @_;
-    my $wrappedcomment = "";
-
-    # Use 'local', as recommended by Text::Wrap's perldoc.
-    local $Text::Wrap::columns = $cols || COMMENT_COLS;
-    # Make words that are longer than COMMENT_COLS not wrap.
-    local $Text::Wrap::huge    = 'overflow';
-    # Don't mess with tabs.
-    local $Text::Wrap::unexpand = 0;
-
-    # If the line starts with ">", don't wrap it. Otherwise, wrap.
-    foreach my $line (split(/\r\n|\r|\n/, $comment)) {
-      if ($line =~ qr/^>/) {
-        $wrappedcomment .= ($line . "\n");
-      }
-      else {
-        $wrappedcomment .= (wrap('', '', $line) . "\n");
-      }
+  my ($comment, $cols) = @_;
+  my $wrappedcomment = "";
+
+  # Use 'local', as recommended by Text::Wrap's perldoc.
+  local $Text::Wrap::columns = $cols || COMMENT_COLS;
+
+  # Make words that are longer than COMMENT_COLS not wrap.
+  local $Text::Wrap::huge = 'overflow';
+
+  # Don't mess with tabs.
+  local $Text::Wrap::unexpand = 0;
+
+  # If the line starts with ">", don't wrap it. Otherwise, wrap.
+  foreach my $line (split(/\r\n|\r|\n/, $comment)) {
+    if ($line =~ qr/^>/) {
+      $wrappedcomment .= ($line . "\n");
     }
+    else {
+      $wrappedcomment .= (wrap('', '', $line) . "\n");
+    }
+  }
 
-    chomp($wrappedcomment); # Text::Wrap adds an extra newline at the end.
-    return $wrappedcomment;
+  chomp($wrappedcomment);    # Text::Wrap adds an extra newline at the end.
+  return $wrappedcomment;
 }
 
 sub find_wrap_point {
-    my ($string, $maxpos) = @_;
-    if (!$string) { return 0 }
-    if (length($string) < $maxpos) { return length($string) }
-    my $wrappoint = rindex($string, ",", $maxpos); # look for comma
-    if ($wrappoint <= 0) {  # can't find comma
-        $wrappoint = rindex($string, " ", $maxpos); # look for space
-        if ($wrappoint <= 0) {  # can't find space
-            $wrappoint = rindex($string, "-", $maxpos); # look for hyphen
-            if ($wrappoint <= 0) {  # can't find hyphen
-                $wrappoint = $maxpos;  # just truncate it
-            } else {
-                $wrappoint++; # leave hyphen on the left side
-            }
-        }
+  my ($string, $maxpos) = @_;
+  if (!$string) { return 0 }
+  if (length($string) < $maxpos) { return length($string) }
+  my $wrappoint = rindex($string, ",", $maxpos);    # look for comma
+  if ($wrappoint <= 0) {                            # can't find comma
+    $wrappoint = rindex($string, " ", $maxpos);     # look for space
+    if ($wrappoint <= 0) {                          # can't find space
+      $wrappoint = rindex($string, "-", $maxpos);    # look for hyphen
+      if ($wrappoint <= 0) {                         # can't find hyphen
+        $wrappoint = $maxpos;                        # just truncate it
+      }
+      else {
+        $wrappoint++;                                # leave hyphen on the left side
+      }
     }
-    return $wrappoint;
+  }
+  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;
-        }
-    }
+  my ($field, $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;
-    }
+  # We need to insert characters as these were removed by old
+  # LogActivityEntry code.
+
+  return $new_change if $current_change eq '';
 
-    # All other fields get a space unless the first character of the second
-    # string is a comma or space
+  # 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;
+      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;
-    local $Text::Wrap::unexpand = 0;
-    local $Text::Wrap::huge = 'wrap';
-    
-    my $wrapped = wrap('', '', $string);
-    chomp($wrapped);
-    return $wrapped;
+  my ($string, $columns) = @_;
+  local $Text::Wrap::columns  = $columns;
+  local $Text::Wrap::unexpand = 0;
+  local $Text::Wrap::huge     = 'wrap';
+
+  my $wrapped = wrap('', '', $string);
+  chomp($wrapped);
+  return $wrapped;
 }
 
 sub format_time {
-    my ($date, $format, $timezone) = @_;
-
-    # If $format is not set, try to guess the correct date format.
-    if (!$format) {
-        if (!ref $date
-            && $date =~ /^(\d{4})[-\.](\d{2})[-\.](\d{2}) (\d{2}):(\d{2})(:(\d{2}))?$/) 
-        {
-            my $sec = $7;
-            if (defined $sec) {
-                $format = "%Y-%m-%d %T %Z";
-            } else {
-                $format = "%Y-%m-%d %R %Z";
-            }
-        } else {
-            # Default date format. See DateTime for other formats available.
-            $format = "%Y-%m-%d %R %Z";
-        }
-    }
-
-    my $dt = ref $date ? $date : datetime_from($date, $timezone);
-    $date = defined $dt ? $dt->strftime($format) : '';
-    return trim($date);
-}
-
-sub datetime_from {
-    my ($date, $timezone) = @_;
-
-    # In the database, this is the "0" date.
-    return undef if $date =~ /^0000/;
+  my ($date, $format, $timezone) = @_;
 
-    my @time;
-    # Most dates will be in this format, avoid strptime's generic parser
-    if ($date =~ /^(\d{4})[\.-](\d{2})[\.-](\d{2})(?: (\d{2}):(\d{2}):(\d{2}))?$/) {
-        @time = ($6, $5, $4, $3, $2 - 1, $1 - 1900, undef);
+  # If $format is not set, try to guess the correct date format.
+  if (!$format) {
+    if (!ref $date
+      && $date =~ /^(\d{4})[-\.](\d{2})[-\.](\d{2}) (\d{2}):(\d{2})(:(\d{2}))?$/)
+    {
+      my $sec = $7;
+      if (defined $sec) {
+        $format = "%Y-%m-%d %T %Z";
+      }
+      else {
+        $format = "%Y-%m-%d %R %Z";
+      }
     }
     else {
-        @time = strptime($date);
-    }
-
-    unless (scalar @time) {
-        # If an unknown timezone is passed (such as MSK, for Moskow),
-        # strptime() is unable to parse the date. We try again, but we first
-        # remove the timezone.
-        $date =~ s/\s+\S+$//;
-        @time = strptime($date);
+      # Default date format. See DateTime for other formats available.
+      $format = "%Y-%m-%d %R %Z";
     }
+  }
 
-    return undef if !@time;
-
-    # 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],
-        # Months start from 0 (January).
-        month  => $time[4] + 1,
-        day    => $time[3],
-        hour   => $time[2],
-        minute => $time[1],
-        # DateTime doesn't like fractional seconds.
-        # Also, sometimes seconds are undef.
-        second => defined($time[0]) ? int($time[0]) : undef,
-        # If a timezone was specified, use it. Otherwise, use the
-        # local timezone.
-        time_zone => DateTime::TimeZone->offset_as_string($time[6])
-                     || Bugzilla->local_timezone,
-    );
-
-    # If something wasn't specified in the date, it's best to just not
-    # pass it to DateTime at all. (This is important for doing datetime_from
-    # on the deadline field, which is usually just a date with no time.)
-    foreach my $arg (keys %args) {
-        delete $args{$arg} if !defined $args{$arg};
-    }
-
-    # This module takes time to load and is only used here, so we
-    # |require| it here rather than |use| it.
-    require DateTime;
-    my $dt = new DateTime(\%args);
+  my $dt = ref $date ? $date : datetime_from($date, $timezone);
+  $date = defined $dt ? $dt->strftime($format) : '';
+  return trim($date);
+}
 
-    # Now display the date using the given timezone,
-    # or the user's timezone if none is given.
-    $dt->set_time_zone($timezone || Bugzilla->user->timezone);
-    return $dt;
+sub datetime_from {
+  my ($date, $timezone) = @_;
+
+  # In the database, this is the "0" date.
+  return undef if $date =~ /^0000/;
+
+  my @time;
+
+  # Most dates will be in this format, avoid strptime's generic parser
+  if ($date =~ /^(\d{4})[\.-](\d{2})[\.-](\d{2})(?: (\d{2}):(\d{2}):(\d{2}))?$/) {
+    @time = ($6, $5, $4, $3, $2 - 1, $1 - 1900, undef);
+  }
+  else {
+    @time = strptime($date);
+  }
+
+  unless (scalar @time) {
+
+    # If an unknown timezone is passed (such as MSK, for Moskow),
+    # strptime() is unable to parse the date. We try again, but we first
+    # remove the timezone.
+    $date =~ s/\s+\S+$//;
+    @time = strptime($date);
+  }
+
+  return undef if !@time;
+
+  # 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],
+
+    # Months start from 0 (January).
+    month  => $time[4] + 1,
+    day    => $time[3],
+    hour   => $time[2],
+    minute => $time[1],
+
+    # DateTime doesn't like fractional seconds.
+    # Also, sometimes seconds are undef.
+    second => defined($time[0]) ? int($time[0]) : undef,
+
+    # If a timezone was specified, use it. Otherwise, use the
+    # local timezone.
+    time_zone => DateTime::TimeZone->offset_as_string($time[6])
+      || Bugzilla->local_timezone,
+  );
+
+  # If something wasn't specified in the date, it's best to just not
+  # pass it to DateTime at all. (This is important for doing datetime_from
+  # on the deadline field, which is usually just a date with no time.)
+  foreach my $arg (keys %args) {
+    delete $args{$arg} if !defined $args{$arg};
+  }
+
+  # This module takes time to load and is only used here, so we
+  # |require| it here rather than |use| it.
+  require DateTime;
+  my $dt = new DateTime(\%args);
+
+  # Now display the date using the given timezone,
+  # or the user's timezone if none is given.
+  $dt->set_time_zone($timezone || Bugzilla->user->timezone);
+  return $dt;
 }
 
 sub bz_crypt {
-    my ($password, $salt) = @_;
-
-    my $algorithm;
-    if (!defined $salt) {
-        # If you don't use a salt, then people can create tables of
-        # hashes that map to particular passwords, and then break your
-        # hashing very easily if they have a large-enough table of common
-        # (or even uncommon) passwords. So we generate a unique salt for
-        # each password in the database, and then just prepend it to
-        # the hash.
-        $salt = generate_random_password(PASSWORD_SALT_LENGTH);
-        $algorithm = PASSWORD_DIGEST_ALGORITHM;
-    }
-
-    # We append the algorithm used to the string. This is good because then
-    # we can change the algorithm being used, in the future, without 
-    # disrupting the validation of existing passwords. Also, this tells
-    # us if a password is using the old "crypt" method of hashing passwords,
-    # because the algorithm will be missing from the string.
-    if ($salt =~ /{([^}]+)}$/) {
-        $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) {
-        # Crypt the password.
-        $crypted_password = crypt($password, $salt);
-    }
-    else {
-        my $hasher = Digest->new($algorithm);
-        # Newly created salts won't yet have a comma.
-        ($salt) = $salt =~ /^([^,]+),?/;
-        $hasher->add($password, $salt);
-        $crypted_password = $salt . ',' . $hasher->b64digest . "{$algorithm}";
-    }
-
-    # Return the crypted password.
-    return $crypted_password;
+  my ($password, $salt) = @_;
+
+  my $algorithm;
+  if (!defined $salt) {
+
+    # If you don't use a salt, then people can create tables of
+    # hashes that map to particular passwords, and then break your
+    # hashing very easily if they have a large-enough table of common
+    # (or even uncommon) passwords. So we generate a unique salt for
+    # each password in the database, and then just prepend it to
+    # the hash.
+    $salt      = generate_random_password(PASSWORD_SALT_LENGTH);
+    $algorithm = PASSWORD_DIGEST_ALGORITHM;
+  }
+
+  # We append the algorithm used to the string. This is good because then
+  # we can change the algorithm being used, in the future, without
+  # disrupting the validation of existing passwords. Also, this tells
+  # us if a password is using the old "crypt" method of hashing passwords,
+  # because the algorithm will be missing from the string.
+  if ($salt =~ /{([^}]+)}$/) {
+    $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) {
+
+    # Crypt the password.
+    $crypted_password = crypt($password, $salt);
+  }
+  else {
+    my $hasher = Digest->new($algorithm);
+
+    # Newly created salts won't yet have a comma.
+    ($salt) = $salt =~ /^([^,]+),?/;
+    $hasher->add($password, $salt);
+    $crypted_password = $salt . ',' . $hasher->b64digest . "{$algorithm}";
+  }
+
+  # Return the crypted password.
+  return $crypted_password;
 }
 
 # If you want to understand the security of strings generated by this
@@ -688,191 +730,199 @@ sub bz_crypt {
 # by the number of characters you generate, and that gets you the equivalent
 # strength of the string in bits.
 sub generate_random_password {
-    my $size = shift || 10; # default to 10 chars if nothing specified
-    return join("", map{ ('0'..'9','a'..'z','A'..'Z')[irand 62] } (1..$size));
+  my $size = shift || 10;    # default to 10 chars if nothing specified
+  return
+    join("", map { ('0' .. '9', 'a' .. 'z', 'A' .. 'Z')[irand 62] } (1 .. $size));
 }
 
 sub validate_email_syntax {
-    my ($addr) = @_;
-    my $match = Bugzilla->params->{'emailregexp'};
-    my $email = $addr . Bugzilla->params->{'emailsuffix'};
-    # This regexp follows RFC 2822 section 3.4.1.
-    my $addr_spec = $Email::Address::addr_spec;
-    # 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.
-    # 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 0;
+  my ($addr) = @_;
+  my $match  = Bugzilla->params->{'emailregexp'};
+  my $email  = $addr . Bugzilla->params->{'emailsuffix'};
+
+  # This regexp follows RFC 2822 section 3.4.1.
+  my $addr_spec = $Email::Address::addr_spec;
+
+  # 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.
+  # 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 0;
 }
 
 sub check_email_syntax {
-    my ($addr) = @_;
+  my ($addr) = @_;
 
-    unless (validate_email_syntax(@_)) {
-        my $email = $addr . Bugzilla->params->{'emailsuffix'};
-        ThrowUserError('illegal_email_address', { addr => $email });
-    }
+  unless (validate_email_syntax(@_)) {
+    my $email = $addr . Bugzilla->params->{'emailsuffix'};
+    ThrowUserError('illegal_email_address', {addr => $email});
+  }
 }
 
 sub validate_date {
-    my ($date) = @_;
-    my $date2;
-
-    # $ts is undefined if the parser fails.
-    my $ts = str2time($date);
-    if ($ts) {
-        $date2 = time2str("%Y-%m-%d", $ts);
-
-        $date =~ s/(\d+)-0*(\d+?)-0*(\d+?)/$1-$2-$3/; 
-        $date2 =~ s/(\d+)-0*(\d+?)-0*(\d+?)/$1-$2-$3/;
-    }
-    my $ret = ($ts && $date eq $date2);
-    return $ret ? 1 : 0;
+  my ($date) = @_;
+  my $date2;
+
+  # $ts is undefined if the parser fails.
+  my $ts = str2time($date);
+  if ($ts) {
+    $date2 = time2str("%Y-%m-%d", $ts);
+
+    $date =~ s/(\d+)-0*(\d+?)-0*(\d+?)/$1-$2-$3/;
+    $date2 =~ s/(\d+)-0*(\d+?)-0*(\d+?)/$1-$2-$3/;
+  }
+  my $ret = ($ts && $date eq $date2);
+  return $ret ? 1 : 0;
 }
 
 sub validate_time {
-    my ($time) = @_;
-    my $time2;
-
-    # $ts is undefined if the parser fails.
-    my $ts = str2time($time);
-    if ($ts) {
-        $time2 = time2str("%H:%M:%S", $ts);
-        if ($time =~ /^(\d{1,2}):(\d\d)(?::(\d\d))?$/) {
-            $time = sprintf("%02d:%02d:%02d", $1, $2, $3 || 0);
-        }
+  my ($time) = @_;
+  my $time2;
+
+  # $ts is undefined if the parser fails.
+  my $ts = str2time($time);
+  if ($ts) {
+    $time2 = time2str("%H:%M:%S", $ts);
+    if ($time =~ /^(\d{1,2}):(\d\d)(?::(\d\d))?$/) {
+      $time = sprintf("%02d:%02d:%02d", $1, $2, $3 || 0);
     }
-    my $ret = ($ts && $time eq $time2);
-    return $ret ? 1 : 0;
+  }
+  my $ret = ($ts && $time eq $time2);
+  return $ret ? 1 : 0;
 }
 
 sub is_7bit_clean {
-    return $_[0] !~ /[^\x20-\x7E\x0A\x0D]/;
+  return $_[0] !~ /[^\x20-\x7E\x0A\x0D]/;
 }
 
 sub clean_text {
-    my $dtext = shift;
-    if ($dtext) {
-        # change control characters into a space
-        $dtext =~ s/[\x00-\x1F\x7F]+/ /g;
-    }
-    return trim($dtext);
+  my $dtext = shift;
+  if ($dtext) {
+
+    # change control characters into a space
+    $dtext =~ s/[\x00-\x1F\x7F]+/ /g;
+  }
+  return trim($dtext);
 }
 
 sub on_main_db (&) {
-    my $code = shift;
-    my $original_dbh = Bugzilla->dbh;
-    Bugzilla->request_cache->{dbh} = Bugzilla->dbh_main;
-    $code->();
-    Bugzilla->request_cache->{dbh} = $original_dbh;
+  my $code         = shift;
+  my $original_dbh = Bugzilla->dbh;
+  Bugzilla->request_cache->{dbh} = Bugzilla->dbh_main;
+  $code->();
+  Bugzilla->request_cache->{dbh} = $original_dbh;
 }
 
 sub get_text {
-    my ($name, $vars) = @_;
-    my $template = Bugzilla->template_inner;
-    $vars ||= {};
-    $vars->{'message'} = $name;
-    my $message;
-    $template->process('global/message.txt.tmpl', $vars, \$message)
-      || ThrowTemplateError($template->error());
-
-    # Remove the indenting that exists in messages.html.tmpl.
-    $message =~ s/^    //gm;
-    return $message;
+  my ($name, $vars) = @_;
+  my $template = Bugzilla->template_inner;
+  $vars ||= {};
+  $vars->{'message'} = $name;
+  my $message;
+  $template->process('global/message.txt.tmpl', $vars, \$message)
+    || ThrowTemplateError($template->error());
+
+  # Remove the indenting that exists in messages.html.tmpl.
+  $message =~ s/^    //gm;
+  return $message;
 }
 
 sub template_var {
-    my $name = shift;
-    my $request_cache = Bugzilla->request_cache;
-    my $cache = $request_cache->{util_template_var} ||= {};
-    my $lang = $request_cache->{template_current_lang}->[0] || '';
-    return $cache->{$lang}->{$name} if defined $cache->{$lang};
-
-    my $template = Bugzilla->template_inner($lang);
-    my %vars;
-    # Note: If we suddenly start needing a lot of template_var variables,
-    # they should move into their own template, not field-descs.
-    $template->process('global/field-descs.none.tmpl',
-                       { vars => \%vars, in_template_var => 1 })
-      || ThrowTemplateError($template->error());
-
-    $cache->{$lang} = \%vars;
-    return $vars{$name};
+  my $name          = shift;
+  my $request_cache = Bugzilla->request_cache;
+  my $cache         = $request_cache->{util_template_var} ||= {};
+  my $lang          = $request_cache->{template_current_lang}->[0] || '';
+  return $cache->{$lang}->{$name} if defined $cache->{$lang};
+
+  my $template = Bugzilla->template_inner($lang);
+  my %vars;
+
+  # Note: If we suddenly start needing a lot of template_var variables,
+  # they should move into their own template, not field-descs.
+  $template->process('global/field-descs.none.tmpl',
+    {vars => \%vars, in_template_var => 1})
+    || ThrowTemplateError($template->error());
+
+  $cache->{$lang} = \%vars;
+  return $vars{$name};
 }
 
 sub display_value {
-    my ($field, $value) = @_;
-    return template_var('value_descs')->{$field}->{$value} // $value;
+  my ($field, $value) = @_;
+  return template_var('value_descs')->{$field}->{$value} // $value;
 }
 
 sub disable_utf8 {
-    if (Bugzilla->params->{'utf8'}) {
-        binmode STDOUT, ':bytes'; # Turn off UTF8 encoding.
-    }
+  if (Bugzilla->params->{'utf8'}) {
+    binmode STDOUT, ':bytes';    # Turn off UTF8 encoding.
+  }
 }
 
 use constant UTF8_ACCIDENTAL => qw(shiftjis big5-eten euc-kr euc-jp);
 
 sub detect_encoding {
-    my $data = shift;
-
-    Bugzilla->feature('detect_charset')
-      || ThrowUserError('feature_disabled', { feature => 'detect_charset' });
-
-    require Encode::Detect::Detector;
-    import Encode::Detect::Detector 'detect';
-
-    my $encoding = detect($data);
-    $encoding = resolve_alias($encoding) if $encoding;
-
-    # Encode::Detect is bad at detecting certain charsets, but Encode::Guess
-    # is better at them. Here's the details:
-
-    # shiftjis, big5-eten, euc-kr, and euc-jp: (Encode::Detect
-    # tends to accidentally mis-detect UTF-8 strings as being
-    # these encodings.)
-    if ($encoding && grep($_ eq $encoding, UTF8_ACCIDENTAL)) {
-        $encoding = undef;
-        my $decoder = guess_encoding($data, UTF8_ACCIDENTAL);
-        $encoding = $decoder->name if ref $decoder;
-    }
-
-    # Encode::Detect sometimes mis-detects various ISO encodings as iso-8859-8,
-    # or cp1255, but Encode::Guess can usually tell which one it is.
-    if ($encoding && ($encoding eq 'iso-8859-8' || $encoding eq 'cp1255')) {
-        my $decoded_as = _guess_iso($data, 'iso-8859-8', 
-            # These are ordered this way because it gives the most 
-            # accurate results.
-            qw(cp1252 iso-8859-7 iso-8859-2));
-        $encoding = $decoded_as if $decoded_as;
-    }
+  my $data = shift;
+
+  Bugzilla->feature('detect_charset')
+    || ThrowUserError('feature_disabled', {feature => 'detect_charset'});
+
+  require Encode::Detect::Detector;
+  import Encode::Detect::Detector 'detect';
+
+  my $encoding = detect($data);
+  $encoding = resolve_alias($encoding) if $encoding;
+
+  # Encode::Detect is bad at detecting certain charsets, but Encode::Guess
+  # is better at them. Here's the details:
+
+  # shiftjis, big5-eten, euc-kr, and euc-jp: (Encode::Detect
+  # tends to accidentally mis-detect UTF-8 strings as being
+  # these encodings.)
+  if ($encoding && grep($_ eq $encoding, UTF8_ACCIDENTAL)) {
+    $encoding = undef;
+    my $decoder = guess_encoding($data, UTF8_ACCIDENTAL);
+    $encoding = $decoder->name if ref $decoder;
+  }
+
+  # Encode::Detect sometimes mis-detects various ISO encodings as iso-8859-8,
+  # or cp1255, but Encode::Guess can usually tell which one it is.
+  if ($encoding && ($encoding eq 'iso-8859-8' || $encoding eq 'cp1255')) {
+    my $decoded_as = _guess_iso(
+      $data, 'iso-8859-8',
+
+      # These are ordered this way because it gives the most
+      # accurate results.
+      qw(cp1252 iso-8859-7 iso-8859-2)
+    );
+    $encoding = $decoded_as if $decoded_as;
+  }
 
-    return $encoding;
+  return $encoding;
 }
 
 # A helper for detect_encoding.
 sub _guess_iso {
-    my ($data, $versus, @isos) = (shift, shift, shift);
-
-    my $encoding;
-    foreach my $iso (@isos) {
-        my $decoder = guess_encoding($data, ($iso, $versus));
-        if (ref $decoder) {
-            $encoding = $decoder->name if ref $decoder;
-            last;
-        }
+  my ($data, $versus, @isos) = (shift, shift, shift);
+
+  my $encoding;
+  foreach my $iso (@isos) {
+    my $decoder = guess_encoding($data, ($iso, $versus));
+    if (ref $decoder) {
+      $encoding = $decoder->name if ref $decoder;
+      last;
     }
-    return $encoding;
+  }
+  return $encoding;
 }
 
 1;
diff --git a/Bugzilla/Version.pm b/Bugzilla/Version.pm
index 4b332ff2b..6a5930574 100644
--- a/Bugzilla/Version.pm
+++ b/Bugzilla/Version.pm
@@ -26,134 +26,131 @@ use Scalar::Util qw(blessed);
 
 use constant DEFAULT_VERSION => 'unspecified';
 
-use constant DB_TABLE => 'versions';
+use constant DB_TABLE   => 'versions';
 use constant NAME_FIELD => 'value';
+
 # This is "id" because it has to be filled in and id is probably the fastest.
 # We do a custom sort in new_from_list below.
 use constant LIST_ORDER => 'id';
 
 use constant DB_COLUMNS => qw(
-    id
-    value
-    product_id
-    isactive
+  id
+  value
+  product_id
+  isactive
 );
 
-use constant REQUIRED_FIELD_MAP => {
-    product_id => 'product',
-};
+use constant REQUIRED_FIELD_MAP => {product_id => 'product',};
 
 use constant UPDATE_COLUMNS => qw(
-    value
-    isactive
+  value
+  isactive
 );
 
 use constant VALIDATORS => {
-    product  => \&_check_product,
-    value    => \&_check_value,
-    isactive => \&Bugzilla::Object::check_boolean,
+  product  => \&_check_product,
+  value    => \&_check_value,
+  isactive => \&Bugzilla::Object::check_boolean,
 };
 
-use constant VALIDATOR_DEPENDENCIES => {
-    value => ['product'],
-};
+use constant VALIDATOR_DEPENDENCIES => {value => ['product'],};
 
 ################################
 # Methods
 ################################
 
 sub new {
-    my $class = shift;
-    my $param = shift;
-    my $dbh = Bugzilla->dbh;
-
-    my $product;
-    if (ref $param and !defined $param->{id}) {
-        $product = $param->{product};
-        my $name = $param->{name};
-        if (!defined $product) {
-            ThrowCodeError('bad_arg',
-                {argument => 'product',
-                 function => "${class}::new"});
-        }
-        if (!defined $name) {
-            ThrowCodeError('bad_arg',
-                {argument => 'name',
-                 function => "${class}::new"});
-        }
-
-        my $condition = 'product_id = ? AND value = ?';
-        my @values = ($product->id, $name);
-        $param = { condition => $condition, values => \@values };
+  my $class = shift;
+  my $param = shift;
+  my $dbh   = Bugzilla->dbh;
+
+  my $product;
+  if (ref $param and !defined $param->{id}) {
+    $product = $param->{product};
+    my $name = $param->{name};
+    if (!defined $product) {
+      ThrowCodeError('bad_arg', {argument => 'product', function => "${class}::new"});
+    }
+    if (!defined $name) {
+      ThrowCodeError('bad_arg', {argument => 'name', function => "${class}::new"});
     }
 
-    unshift @_, $param;
-    return $class->SUPER::new(@_);
+    my $condition = 'product_id = ? AND value = ?';
+    my @values = ($product->id, $name);
+    $param = {condition => $condition, values => \@values};
+  }
+
+  unshift @_, $param;
+  return $class->SUPER::new(@_);
 }
 
 sub new_from_list {
-    my $self = shift;
-    my $list = $self->SUPER::new_from_list(@_);
-    return [sort { vers_cmp(lc($a->name), lc($b->name)) } @$list];
+  my $self = shift;
+  my $list = $self->SUPER::new_from_list(@_);
+  return [sort { vers_cmp(lc($a->name), lc($b->name)) } @$list];
 }
 
 sub run_create_validators {
-    my $class  = shift;
-    my $params = $class->SUPER::run_create_validators(@_);
-    my $product = delete $params->{product};
-    $params->{product_id} = $product->id;
-    return $params;
+  my $class   = shift;
+  my $params  = $class->SUPER::run_create_validators(@_);
+  my $product = delete $params->{product};
+  $params->{product_id} = $product->id;
+  return $params;
 }
 
 sub bug_count {
-    my $self = shift;
-    my $dbh = Bugzilla->dbh;
+  my $self = shift;
+  my $dbh  = Bugzilla->dbh;
 
-    if (!defined $self->{'bug_count'}) {
-        $self->{'bug_count'} = $dbh->selectrow_array(qq{
+  if (!defined $self->{'bug_count'}) {
+    $self->{'bug_count'} = $dbh->selectrow_array(
+      qq{
             SELECT COUNT(*) FROM bugs
             WHERE product_id = ? AND version = ?}, undef,
-            ($self->product_id, $self->name)) || 0;
-    }
-    return $self->{'bug_count'};
+      ($self->product_id, $self->name)
+    ) || 0;
+  }
+  return $self->{'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}) {
-        $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;
+  my $self = shift;
+  my $dbh  = Bugzilla->dbh;
+
+  $dbh->bz_start_transaction();
+  my ($changes, $old_self) = $self->SUPER::update(@_);
+
+  if (exists $changes->{value}) {
+    $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;
 }
 
 sub remove_from_db {
-    my $self = shift;
-    my $dbh = Bugzilla->dbh;
+  my $self = shift;
+  my $dbh  = Bugzilla->dbh;
 
-    $dbh->bz_start_transaction();
+  $dbh->bz_start_transaction();
 
-    # Products must have at least one version.
-    if (scalar(@{$self->product->versions}) == 1) {
-        ThrowUserError('version_is_last', { version => $self });
-    }
+  # Products must have at least one version.
+  if (scalar(@{$self->product->versions}) == 1) {
+    ThrowUserError('version_is_last', {version => $self});
+  }
 
-    # The version cannot be removed if there are bugs
-    # associated with it.
-    if ($self->bug_count) {
-        ThrowUserError("version_has_bugs", { nb => $self->bug_count });
-    }
-    $self->SUPER::remove_from_db();
+  # The version cannot be removed if there are bugs
+  # associated with it.
+  if ($self->bug_count) {
+    ThrowUserError("version_has_bugs", {nb => $self->bug_count});
+  }
+  $self->SUPER::remove_from_db();
 
-    $dbh->bz_commit_transaction();
+  $dbh->bz_commit_transaction();
 }
 
 ###############################
@@ -161,45 +158,47 @@ sub remove_from_db {
 ###############################
 
 sub product_id { return $_[0]->{'product_id'}; }
-sub is_active  { return $_[0]->{'isactive'};   }
+sub is_active  { return $_[0]->{'isactive'}; }
 
 sub product {
-    my $self = shift;
+  my $self = shift;
 
-    require Bugzilla::Product;
-    $self->{'product'} ||= new Bugzilla::Product($self->product_id);
-    return $self->{'product'};
+  require Bugzilla::Product;
+  $self->{'product'} ||= new Bugzilla::Product($self->product_id);
+  return $self->{'product'};
 }
 
 ################################
 # Validators
 ################################
 
-sub set_value    { $_[0]->set('value', $_[1]);    }
+sub set_value    { $_[0]->set('value',    $_[1]); }
 sub set_isactive { $_[0]->set('isactive', $_[1]); }
 
 sub _check_value {
-    my ($invocant, $name, undef, $params) = @_;
-    my $product = blessed($invocant) ? $invocant->product : $params->{product};
-
-    $name = trim($name);
-    $name || ThrowUserError('version_blank_name');
-    # Remove unprintable characters
-    $name = clean_text($name);
-
-    my $version = new Bugzilla::Version({ product => $product, name => $name });
-    if ($version && (!ref $invocant || $version->id != $invocant->id)) {
-        ThrowUserError('version_already_exists', { name    => $version->name,
-                                                   product => $product->name });
-    }
-    return $name;
+  my ($invocant, $name, undef, $params) = @_;
+  my $product = blessed($invocant) ? $invocant->product : $params->{product};
+
+  $name = trim($name);
+  $name || ThrowUserError('version_blank_name');
+
+  # Remove unprintable characters
+  $name = clean_text($name);
+
+  my $version = new Bugzilla::Version({product => $product, name => $name});
+  if ($version && (!ref $invocant || $version->id != $invocant->id)) {
+    ThrowUserError('version_already_exists',
+      {name => $version->name, product => $product->name});
+  }
+  return $name;
 }
 
 sub _check_product {
-    my ($invocant, $product) = @_;
-    $product || ThrowCodeError('param_required',
-                    { function => "$invocant->create", param => 'product' });
-    return Bugzilla->user->check_can_admin_product($product->name);
+  my ($invocant, $product) = @_;
+  $product
+    || ThrowCodeError('param_required',
+    {function => "$invocant->create", param => 'product'});
+  return Bugzilla->user->check_can_admin_product($product->name);
 }
 
 ###############################
@@ -209,44 +208,52 @@ sub _check_product {
 # This is taken straight from Sort::Versions 1.5, which is not included
 # with perl by default.
 sub vers_cmp {
-    my ($a, $b) = @_;
-
-    # Remove leading zeroes - Bug 344661
-    $a =~ s/^0*(\d.+)/$1/;
-    $b =~ s/^0*(\d.+)/$1/;
-
-    my @A = ($a =~ /([-.]|\d+|[^-.\d]+)/g);
-    my @B = ($b =~ /([-.]|\d+|[^-.\d]+)/g);
-
-    my ($A, $B);
-    while (@A and @B) {
-        $A = shift @A;
-        $B = shift @B;
-        if ($A eq '-' and $B eq '-') {
-            next;
-        } elsif ( $A eq '-' ) {
-            return -1;
-        } elsif ( $B eq '-') {
-            return 1;
-        } elsif ($A eq '.' and $B eq '.') {
-            next;
-        } elsif ( $A eq '.' ) {
-            return -1;
-        } elsif ( $B eq '.' ) {
-            return 1;
-        } elsif ($A =~ /^\d+$/ and $B =~ /^\d+$/) {
-            if ($A =~ /^0/ || $B =~ /^0/) {
-                return $A cmp $B if $A cmp $B;
-            } else {
-                return $A <=> $B if $A <=> $B;
-            }
-        } else {
-            $A = uc $A;
-            $B = uc $B;
-            return $A cmp $B if $A cmp $B;
-        }
+  my ($a, $b) = @_;
+
+  # Remove leading zeroes - Bug 344661
+  $a =~ s/^0*(\d.+)/$1/;
+  $b =~ s/^0*(\d.+)/$1/;
+
+  my @A = ($a =~ /([-.]|\d+|[^-.\d]+)/g);
+  my @B = ($b =~ /([-.]|\d+|[^-.\d]+)/g);
+
+  my ($A, $B);
+  while (@A and @B) {
+    $A = shift @A;
+    $B = shift @B;
+    if ($A eq '-' and $B eq '-') {
+      next;
+    }
+    elsif ($A eq '-') {
+      return -1;
+    }
+    elsif ($B eq '-') {
+      return 1;
+    }
+    elsif ($A eq '.' and $B eq '.') {
+      next;
+    }
+    elsif ($A eq '.') {
+      return -1;
+    }
+    elsif ($B eq '.') {
+      return 1;
+    }
+    elsif ($A =~ /^\d+$/ and $B =~ /^\d+$/) {
+      if ($A =~ /^0/ || $B =~ /^0/) {
+        return $A cmp $B if $A cmp $B;
+      }
+      else {
+        return $A <=> $B if $A <=> $B;
+      }
+    }
+    else {
+      $A = uc $A;
+      $B = uc $B;
+      return $A cmp $B if $A cmp $B;
     }
-    return @A <=> @B;
+  }
+  return @A <=> @B;
 }
 
 1;
diff --git a/Bugzilla/WebService.pm b/Bugzilla/WebService.pm
index f80813744..2630e3565 100644
--- a/Bugzilla/WebService.pm
+++ b/Bugzilla/WebService.pm
@@ -5,7 +5,7 @@
 # This Source Code Form is "Incompatible With Secondary Licenses", as
 # defined by the Mozilla Public License, v. 2.0.
 
-# This is the base class for $self in WebService method calls. For the 
+# This is the base class for $self in WebService method calls. For the
 # actual RPC server, see Bugzilla::WebService::Server and its subclasses.
 package Bugzilla::WebService;
 
@@ -17,11 +17,12 @@ use Bugzilla::WebService::Server;
 
 # Used by the JSON-RPC server to convert incoming date fields apprpriately.
 use constant DATE_FIELDS => {};
+
 # Used by the JSON-RPC server to convert incoming base64 fields appropriately.
 use constant BASE64_FIELDS => {};
 
 # For some methods, we shouldn't call Bugzilla->login before we call them
-use constant LOGIN_EXEMPT => { };
+use constant LOGIN_EXEMPT => {};
 
 # Used to allow methods to be called in the JSON-RPC WebService via GET.
 # Methods that can modify data MUST not be listed here.
@@ -32,8 +33,8 @@ use constant READ_ONLY => ();
 use constant PUBLIC_METHODS => ();
 
 sub login_exempt {
-    my ($class, $method) = @_;
-    return $class->LOGIN_EXEMPT->{$method};
+  my ($class, $method) = @_;
+  return $class->LOGIN_EXEMPT->{$method};
 }
 
 1;
diff --git a/Bugzilla/WebService/Bug.pm b/Bugzilla/WebService/Bug.pm
index b07d3cb01..a5de3b68e 100644
--- a/Bugzilla/WebService/Bug.pm
+++ b/Bugzilla/WebService/Bug.pm
@@ -19,7 +19,8 @@ use Bugzilla::Constants;
 use Bugzilla::Error;
 use Bugzilla::Field;
 use Bugzilla::WebService::Constants;
-use Bugzilla::WebService::Util qw(extract_flags filter filter_wants validate translate);
+use Bugzilla::WebService::Util
+  qw(extract_flags filter filter_wants validate translate);
 use Bugzilla::Bug;
 use Bugzilla::BugMail;
 use Bugzilla::Util qw(trick_taint trim diff_arrays detaint_natural);
@@ -43,58 +44,54 @@ use Storable qw(dclone);
 use constant PRODUCT_SPECIFIC_FIELDS => qw(version target_milestone component);
 
 use constant DATE_FIELDS => {
-    comments => ['new_since'],
-    history  => ['new_since'],
-    search   => ['last_change_time', 'creation_time'],
+  comments => ['new_since'],
+  history  => ['new_since'],
+  search   => ['last_change_time', 'creation_time'],
 };
 
-use constant BASE64_FIELDS => {
-    add_attachment => ['data'],
-};
+use constant BASE64_FIELDS => {add_attachment => ['data'],};
 
 use constant READ_ONLY => qw(
-    attachments
-    comments
-    fields
-    get
-    history
-    legal_values
-    search
+  attachments
+  comments
+  fields
+  get
+  history
+  legal_values
+  search
 );
 
 use constant PUBLIC_METHODS => qw(
-    add_attachment
-    add_comment
-    attachments
-    comments
-    create
-    fields
-    get
-    history
-    legal_values
-    possible_duplicates
-    render_comment
-    search
-    search_comment_tags
-    update
-    update_attachment
-    update_comment_tags
-    update_see_also
-    update_tags
+  add_attachment
+  add_comment
+  attachments
+  comments
+  create
+  fields
+  get
+  history
+  legal_values
+  possible_duplicates
+  render_comment
+  search
+  search_comment_tags
+  update
+  update_attachment
+  update_comment_tags
+  update_see_also
+  update_tags
 );
 
-use constant ATTACHMENT_MAPPED_SETTERS => {
-    file_name => 'filename',
-    summary   => 'description',
-};
+use constant ATTACHMENT_MAPPED_SETTERS =>
+  {file_name => 'filename', summary => 'description',};
 
 use constant ATTACHMENT_MAPPED_RETURNS => {
-    description => 'summary',
-    ispatch     => 'is_patch',
-    isprivate   => 'is_private',
-    isobsolete  => 'is_obsolete',
-    filename    => 'file_name',
-    mimetype    => 'content_type',
+  description => 'summary',
+  ispatch     => 'is_patch',
+  isprivate   => 'is_private',
+  isobsolete  => 'is_obsolete',
+  filename    => 'file_name',
+  mimetype    => 'content_type',
 };
 
 ###########
@@ -102,1089 +99,1104 @@ use constant ATTACHMENT_MAPPED_RETURNS => {
 ###########
 
 sub fields {
-    my ($self, $params) = validate(@_, 'ids', 'names');
+  my ($self, $params) = validate(@_, 'ids', 'names');
 
-    Bugzilla->switch_to_shadow_db();
+  Bugzilla->switch_to_shadow_db();
 
-    my @fields;
-    if (defined $params->{ids}) {
-        my $ids = $params->{ids};
-        foreach my $id (@$ids) {
-            my $loop_field = Bugzilla::Field->check({ id => $id });
-            push(@fields, $loop_field);
-        }
+  my @fields;
+  if (defined $params->{ids}) {
+    my $ids = $params->{ids};
+    foreach my $id (@$ids) {
+      my $loop_field = Bugzilla::Field->check({id => $id});
+      push(@fields, $loop_field);
     }
-
-    if (defined $params->{names}) {
-        my $names = $params->{names};
-        foreach my $field_name (@$names) {
-            my $loop_field = Bugzilla::Field->check($field_name);
-            # Don't push in duplicate fields if we also asked for this field
-            # in "ids".
-            if (!grep($_->id == $loop_field->id, @fields)) {
-                push(@fields, $loop_field);
-            }
-        }
+  }
+
+  if (defined $params->{names}) {
+    my $names = $params->{names};
+    foreach my $field_name (@$names) {
+      my $loop_field = Bugzilla::Field->check($field_name);
+
+      # Don't push in duplicate fields if we also asked for this field
+      # in "ids".
+      if (!grep($_->id == $loop_field->id, @fields)) {
+        push(@fields, $loop_field);
+      }
     }
-
-    if (!defined $params->{ids} and !defined $params->{names}) {
-        @fields = @{ Bugzilla->fields({ obsolete => 0 }) };
+  }
+
+  if (!defined $params->{ids} and !defined $params->{names}) {
+    @fields = @{Bugzilla->fields({obsolete => 0})};
+  }
+
+  my @fields_out;
+  foreach my $field (@fields) {
+    my $visibility_field
+      = $field->visibility_field ? $field->visibility_field->name : undef;
+    my $vis_values = $field->visibility_values;
+    my $value_field = $field->value_field ? $field->value_field->name : undef;
+
+    my (@values, $has_values);
+    if ( ($field->is_select and $field->name ne 'product')
+      or grep($_ eq $field->name, PRODUCT_SPECIFIC_FIELDS)
+      or $field->name eq 'keywords')
+    {
+      $has_values = 1;
+      @values = @{$self->_legal_field_values({field => $field})};
     }
 
-    my @fields_out;
-    foreach my $field (@fields) {
-        my $visibility_field = $field->visibility_field
-                               ? $field->visibility_field->name : undef;
-        my $vis_values = $field->visibility_values;
-        my $value_field = $field->value_field
-                          ? $field->value_field->name : undef;
-
-        my (@values, $has_values);
-        if ( ($field->is_select and $field->name ne 'product')
-             or grep($_ eq $field->name, PRODUCT_SPECIFIC_FIELDS)
-             or $field->name eq 'keywords')
-        {
-             $has_values = 1;
-             @values = @{ $self->_legal_field_values({ field => $field }) };
-        } 
-
-        if (grep($_ eq $field->name, PRODUCT_SPECIFIC_FIELDS)) {
-             $value_field = 'product';
-        }
+    if (grep($_ eq $field->name, PRODUCT_SPECIFIC_FIELDS)) {
+      $value_field = 'product';
+    }
 
-        my %field_data = (
-           id                => $self->type('int', $field->id),
-           type              => $self->type('int', $field->type),
-           is_custom         => $self->type('boolean', $field->custom),
-           name              => $self->type('string', $field->name),
-           display_name      => $self->type('string', $field->description),
-           is_mandatory      => $self->type('boolean', $field->is_mandatory),
-           is_on_bug_entry   => $self->type('boolean', $field->enter_bug),
-           visibility_field  => $self->type('string', $visibility_field),
-           visibility_values =>
-              [ map { $self->type('string', $_->name) } @$vis_values ],
-        );
-        if ($has_values) {
-           $field_data{value_field} = $self->type('string', $value_field);
-           $field_data{values}      = \@values;
-        };
-        push(@fields_out, filter $params, \%field_data);
+    my %field_data = (
+      id               => $self->type('int',     $field->id),
+      type             => $self->type('int',     $field->type),
+      is_custom        => $self->type('boolean', $field->custom),
+      name             => $self->type('string',  $field->name),
+      display_name     => $self->type('string',  $field->description),
+      is_mandatory     => $self->type('boolean', $field->is_mandatory),
+      is_on_bug_entry  => $self->type('boolean', $field->enter_bug),
+      visibility_field => $self->type('string',  $visibility_field),
+      visibility_values => [map { $self->type('string', $_->name) } @$vis_values],
+    );
+    if ($has_values) {
+      $field_data{value_field} = $self->type('string', $value_field);
+      $field_data{values} = \@values;
     }
+    push(@fields_out, filter $params, \%field_data);
+  }
 
-    return { fields => \@fields_out };
+  return {fields => \@fields_out};
 }
 
 sub _legal_field_values {
-    my ($self, $params) = @_;
-    my $field = $params->{field};
-    my $field_name = $field->name;
-    my $user = Bugzilla->user;
-
-    my @result;
-    if (grep($_ eq $field_name, PRODUCT_SPECIFIC_FIELDS)) {
-        my @list;
-        if ($field_name eq 'version') {
-            @list = Bugzilla::Version->get_all;
-        }
-        elsif ($field_name eq 'component') {
-            @list = Bugzilla::Component->get_all;
-        }
-        else {
-            @list = Bugzilla::Milestone->get_all;
-        }
+  my ($self, $params) = @_;
+  my $field      = $params->{field};
+  my $field_name = $field->name;
+  my $user       = Bugzilla->user;
+
+  my @result;
+  if (grep($_ eq $field_name, PRODUCT_SPECIFIC_FIELDS)) {
+    my @list;
+    if ($field_name eq 'version') {
+      @list = Bugzilla::Version->get_all;
+    }
+    elsif ($field_name eq 'component') {
+      @list = Bugzilla::Component->get_all;
+    }
+    else {
+      @list = Bugzilla::Milestone->get_all;
+    }
 
-        foreach my $value (@list) {
-            my $sortkey = $field_name eq 'target_milestone'
-                          ? $value->sortkey : 0;
-            # XXX This is very slow for large numbers of values.
-            my $product_name = $value->product->name;
-            if ($user->can_see_product($product_name)) {
-                push(@result, {
-                    name     => $self->type('string', $value->name),
-                    sort_key => $self->type('int', $sortkey),
-                    sortkey  => $self->type('int', $sortkey), # deprecated
-                    visibility_values => [$self->type('string', $product_name)],
-                    is_active         => $self->type('boolean', $value->is_active),
-                });
-            }
-        }
+    foreach my $value (@list) {
+      my $sortkey = $field_name eq 'target_milestone' ? $value->sortkey : 0;
+
+      # XXX This is very slow for large numbers of values.
+      my $product_name = $value->product->name;
+      if ($user->can_see_product($product_name)) {
+        push(
+          @result,
+          {
+            name              => $self->type('string',  $value->name),
+            sort_key          => $self->type('int',     $sortkey),
+            sortkey           => $self->type('int',     $sortkey),            # deprecated
+            visibility_values => [$self->type('string', $product_name)],
+            is_active         => $self->type('boolean', $value->is_active),
+          }
+        );
+      }
     }
+  }
+
+  elsif ($field_name eq 'bug_status') {
+    my @status_all     = Bugzilla::Status->get_all;
+    my $initial_status = bless(
+      {
+        id            => 0,
+        name          => '',
+        is_open       => 1,
+        sortkey       => 0,
+        can_change_to => Bugzilla::Status->can_change_to
+      },
+      'Bugzilla::Status'
+    );
+    unshift(@status_all, $initial_status);
+
+    foreach my $status (@status_all) {
+      my @can_change_to;
+      foreach my $change_to (@{$status->can_change_to}) {
+
+        # There's no need to note that a status can transition
+        # to itself.
+        next if $change_to->id == $status->id;
+        my %change_to_hash = (
+          name => $self->type('string', $change_to->name),
+          comment_required =>
+            $self->type('boolean', $change_to->comment_required_on_change_from($status)),
+        );
+        push(@can_change_to, \%change_to_hash);
+      }
 
-    elsif ($field_name eq 'bug_status') {
-        my @status_all = Bugzilla::Status->get_all;
-        my $initial_status = bless({ id => 0, name => '', is_open => 1, sortkey => 0,
-                                     can_change_to => Bugzilla::Status->can_change_to },
-                                   'Bugzilla::Status');
-        unshift(@status_all, $initial_status);
-
-        foreach my $status (@status_all) {
-            my @can_change_to;
-            foreach my $change_to (@{ $status->can_change_to }) {
-                # There's no need to note that a status can transition
-                # to itself.
-                next if $change_to->id == $status->id;
-                my %change_to_hash = (
-                    name => $self->type('string', $change_to->name),
-                    comment_required => $self->type('boolean', 
-                        $change_to->comment_required_on_change_from($status)),
-                );
-                push(@can_change_to, \%change_to_hash);
-            }
-
-            push (@result, {
-               name     => $self->type('string', $status->name),
-               is_open  => $self->type('boolean', $status->is_open),
-               sort_key => $self->type('int', $status->sortkey),
-               sortkey  => $self->type('int', $status->sortkey), # deprecated
-               can_change_to => \@can_change_to,
-               visibility_values => [],
-            });
+      push(
+        @result,
+        {
+          name              => $self->type('string',  $status->name),
+          is_open           => $self->type('boolean', $status->is_open),
+          sort_key          => $self->type('int',     $status->sortkey),
+          sortkey           => $self->type('int',     $status->sortkey),    # deprecated
+          can_change_to     => \@can_change_to,
+          visibility_values => [],
         }
+      );
     }
+  }
 
-    elsif ($field_name eq 'keywords') {
-        my @legal_keywords = Bugzilla::Keyword->get_all;
-        foreach my $value (@legal_keywords) {
-            push (@result, {
-               name     => $self->type('string', $value->name),
-               description => $self->type('string', $value->description),
-            });
+  elsif ($field_name eq 'keywords') {
+    my @legal_keywords = Bugzilla::Keyword->get_all;
+    foreach my $value (@legal_keywords) {
+      push(
+        @result,
+        {
+          name        => $self->type('string', $value->name),
+          description => $self->type('string', $value->description),
         }
+      );
     }
-    else {
-        my @values = Bugzilla::Field::Choice->type($field)->get_all();
-        foreach my $value (@values) {
-            my $vis_val = $value->visibility_value;
-            push(@result, {
-                name     => $self->type('string', $value->name),
-                sort_key => $self->type('int'   , $value->sortkey),
-                sortkey  => $self->type('int'   , $value->sortkey), # deprecated
-                visibility_values => [
-                    defined $vis_val ? $self->type('string', $vis_val->name) 
-                                     : ()
-                ],
-            });
+  }
+  else {
+    my @values = Bugzilla::Field::Choice->type($field)->get_all();
+    foreach my $value (@values) {
+      my $vis_val = $value->visibility_value;
+      push(
+        @result,
+        {
+          name     => $self->type('string', $value->name),
+          sort_key => $self->type('int',    $value->sortkey),
+          sortkey  => $self->type('int',    $value->sortkey),    # deprecated
+          visibility_values =>
+            [defined $vis_val ? $self->type('string', $vis_val->name) : ()],
         }
+      );
     }
+  }
 
-    return \@result;
+  return \@result;
 }
 
 sub comments {
-    my ($self, $params) = validate(@_, 'ids', 'comment_ids');
+  my ($self, $params) = validate(@_, 'ids', 'comment_ids');
 
-    if (!(defined $params->{ids} || defined $params->{comment_ids})) {
-        ThrowCodeError('params_required',
-                       { function => 'Bug.comments',
-                         params   => ['ids', 'comment_ids'] });
-    }
+  if (!(defined $params->{ids} || defined $params->{comment_ids})) {
+    ThrowCodeError('params_required',
+      {function => 'Bug.comments', params => ['ids', 'comment_ids']});
+  }
 
-    my $bug_ids = $params->{ids} || [];
-    my $comment_ids = $params->{comment_ids} || [];
-
-    my $dbh  = Bugzilla->switch_to_shadow_db();
-    my $user = Bugzilla->user;
-
-    my %bugs;
-    foreach my $bug_id (@$bug_ids) {
-        my $bug = Bugzilla::Bug->check($bug_id);
-        # We want the API to always return comments in the same order.
-   
-        my $comments = $bug->comments({ order => 'oldest_to_newest',
-                                        after => $params->{new_since} });
-        my @result;
-        foreach my $comment (@$comments) {
-            next if $comment->is_private && !$user->is_insider;
-            push(@result, $self->_translate_comment($comment, $params));
-        }
-        $bugs{$bug->id}{'comments'} = \@result;
+  my $bug_ids     = $params->{ids}         || [];
+  my $comment_ids = $params->{comment_ids} || [];
+
+  my $dbh  = Bugzilla->switch_to_shadow_db();
+  my $user = Bugzilla->user;
+
+  my %bugs;
+  foreach my $bug_id (@$bug_ids) {
+    my $bug = Bugzilla::Bug->check($bug_id);
+
+    # We want the API to always return comments in the same order.
+
+    my $comments
+      = $bug->comments({order => 'oldest_to_newest', after => $params->{new_since}
+      });
+    my @result;
+    foreach my $comment (@$comments) {
+      next if $comment->is_private && !$user->is_insider;
+      push(@result, $self->_translate_comment($comment, $params));
+    }
+    $bugs{$bug->id}{'comments'} = \@result;
+  }
+
+  my %comments;
+  if (scalar @$comment_ids) {
+    my @ids = map { trim($_) } @$comment_ids;
+    my $comment_data = Bugzilla::Comment->new_from_list(\@ids);
+
+    # See if we were passed any invalid comment ids.
+    my %got_ids = map { $_->id => 1 } @$comment_data;
+    foreach my $comment_id (@ids) {
+      if (!$got_ids{$comment_id}) {
+        ThrowUserError('comment_id_invalid', {id => $comment_id});
+      }
     }
 
-    my %comments;
-    if (scalar @$comment_ids) {
-        my @ids = map { trim($_) } @$comment_ids;
-        my $comment_data = Bugzilla::Comment->new_from_list(\@ids);
-
-        # See if we were passed any invalid comment ids.
-        my %got_ids = map { $_->id => 1 } @$comment_data;
-        foreach my $comment_id (@ids) {
-            if (!$got_ids{$comment_id}) {
-                ThrowUserError('comment_id_invalid', { id => $comment_id });
-            }
-        }
- 
-        # Now make sure that we can see all the associated bugs.
-        my %got_bug_ids = map { $_->bug_id => 1 } @$comment_data;
-        Bugzilla::Bug->check($_) foreach (keys %got_bug_ids);
-
-        foreach my $comment (@$comment_data) {
-            if ($comment->is_private && !$user->is_insider) {
-                ThrowUserError('comment_is_private', { id => $comment->id });
-            }
-            $comments{$comment->id} =
-                $self->_translate_comment($comment, $params);
-        }
+    # Now make sure that we can see all the associated bugs.
+    my %got_bug_ids = map { $_->bug_id => 1 } @$comment_data;
+    Bugzilla::Bug->check($_) foreach (keys %got_bug_ids);
+
+    foreach my $comment (@$comment_data) {
+      if ($comment->is_private && !$user->is_insider) {
+        ThrowUserError('comment_is_private', {id => $comment->id});
+      }
+      $comments{$comment->id} = $self->_translate_comment($comment, $params);
     }
+  }
 
-    return { bugs => \%bugs, comments => \%comments };
+  return {bugs => \%bugs, comments => \%comments};
 }
 
 sub render_comment {
-    my ($self, $params) = @_;
+  my ($self, $params) = @_;
 
-    unless (defined $params->{text}) {
-        ThrowCodeError('params_required',
-                       { function => 'Bug.render_comment',
-                         params   => ['text'] });
-    }
+  unless (defined $params->{text}) {
+    ThrowCodeError('params_required',
+      {function => 'Bug.render_comment', params => ['text']});
+  }
 
-    Bugzilla->switch_to_shadow_db();
-    my $bug = $params->{id} ? Bugzilla::Bug->check($params->{id}) : undef;
+  Bugzilla->switch_to_shadow_db();
+  my $bug = $params->{id} ? Bugzilla::Bug->check($params->{id}) : undef;
 
-    my $tmpl = '[% text FILTER quoteUrls(bug) %]';
-    my $html;
-    my $template = Bugzilla->template;
-    $template->process(
-        \$tmpl,
-        { bug => $bug, text => $params->{text}},
-        \$html
-    );
+  my $tmpl = '[% text FILTER quoteUrls(bug) %]';
+  my $html;
+  my $template = Bugzilla->template;
+  $template->process(\$tmpl, {bug => $bug, text => $params->{text}}, \$html);
 
-    return { html => $html };
+  return {html => $html};
 }
 
 # Helper for Bug.comments
 sub _translate_comment {
-    my ($self, $comment, $filters, $types, $prefix) = @_;
-    my $attach_id = $comment->is_about_attachment ? $comment->extra_data
-                                                  : undef;
-
-    my $comment_hash = {
-        id         => $self->type('int', $comment->id),
-        bug_id     => $self->type('int', $comment->bug_id),
-        creator    => $self->type('email', $comment->author->login),
-        time       => $self->type('dateTime', $comment->creation_ts),
-        creation_time => $self->type('dateTime', $comment->creation_ts),
-        is_private => $self->type('boolean', $comment->is_private),
-        text       => $self->type('string', $comment->body_full),
-        attachment_id => $self->type('int', $attach_id),
-        count      => $self->type('int', $comment->count),
-    };
-
-    # Don't load comment tags unless enabled
-    if (Bugzilla->params->{'comment_taggers_group'}) {
-        $comment_hash->{tags} = [
-            map { $self->type('string', $_) }
-            @{ $comment->tags }
-        ];
-    }
-
-    return filter($filters, $comment_hash, $types, $prefix);
+  my ($self, $comment, $filters, $types, $prefix) = @_;
+  my $attach_id = $comment->is_about_attachment ? $comment->extra_data : undef;
+
+  my $comment_hash = {
+    id            => $self->type('int',      $comment->id),
+    bug_id        => $self->type('int',      $comment->bug_id),
+    creator       => $self->type('email',    $comment->author->login),
+    time          => $self->type('dateTime', $comment->creation_ts),
+    creation_time => $self->type('dateTime', $comment->creation_ts),
+    is_private    => $self->type('boolean',  $comment->is_private),
+    text          => $self->type('string',   $comment->body_full),
+    attachment_id => $self->type('int',      $attach_id),
+    count         => $self->type('int',      $comment->count),
+  };
+
+  # Don't load comment tags unless enabled
+  if (Bugzilla->params->{'comment_taggers_group'}) {
+    $comment_hash->{tags} = [map { $self->type('string', $_) } @{$comment->tags}];
+  }
+
+  return filter($filters, $comment_hash, $types, $prefix);
 }
 
 sub get {
-    my ($self, $params) = validate(@_, 'ids');
-
-    Bugzilla->switch_to_shadow_db() unless Bugzilla->user->id;
-
-    my $ids = $params->{ids};
-    defined $ids || ThrowCodeError('param_required', { param => 'ids' });
-
-    my (@bugs, @faults, @hashes);
-
-    # Cache permissions for bugs. This highly reduces the number of calls to the DB.
-    # visible_bugs() is only able to handle bug IDs, so we have to skip aliases.
-    my @int = grep { $_ =~ /^\d+$/ } @$ids;
-    Bugzilla->user->visible_bugs(\@int);
-
-    foreach my $bug_id (@$ids) {
-        my $bug;
-        if ($params->{permissive}) {
-            eval { $bug = Bugzilla::Bug->check($bug_id); };
-            if ($@) {
-                push(@faults, {id => $bug_id,
-                               faultString => $@->faultstring,
-                               faultCode => $@->faultcode,
-                              }
-                    );
-                undef $@;
-                next;
-            }
-        }
-        else {
-            $bug = Bugzilla::Bug->check($bug_id);
-        }
-        push(@bugs, $bug);
-        push(@hashes, $self->_bug_to_hash($bug, $params));
+  my ($self, $params) = validate(@_, 'ids');
+
+  Bugzilla->switch_to_shadow_db() unless Bugzilla->user->id;
+
+  my $ids = $params->{ids};
+  defined $ids || ThrowCodeError('param_required', {param => 'ids'});
+
+  my (@bugs, @faults, @hashes);
+
+  # Cache permissions for bugs. This highly reduces the number of calls to the DB.
+  # visible_bugs() is only able to handle bug IDs, so we have to skip aliases.
+  my @int = grep { $_ =~ /^\d+$/ } @$ids;
+  Bugzilla->user->visible_bugs(\@int);
+
+  foreach my $bug_id (@$ids) {
+    my $bug;
+    if ($params->{permissive}) {
+      eval { $bug = Bugzilla::Bug->check($bug_id); };
+      if ($@) {
+        push(@faults,
+          {id => $bug_id, faultString => $@->faultstring, faultCode => $@->faultcode,});
+        undef $@;
+        next;
+      }
     }
+    else {
+      $bug = Bugzilla::Bug->check($bug_id);
+    }
+    push(@bugs, $bug);
+    push(@hashes, $self->_bug_to_hash($bug, $params));
+  }
 
-    # Set the ETag before inserting the update tokens
-    # since the tokens will always be unique even if
-    # the data has not changed.
-    $self->bz_etag(\@hashes);
+  # Set the ETag before inserting the update tokens
+  # since the tokens will always be unique even if
+  # the data has not changed.
+  $self->bz_etag(\@hashes);
 
-    $self->_add_update_tokens($params, \@bugs, \@hashes);
+  $self->_add_update_tokens($params, \@bugs, \@hashes);
 
-    return { bugs => \@hashes, faults => \@faults };
+  return {bugs => \@hashes, faults => \@faults};
 }
 
-# this is a function that gets bug activity for list of bug ids 
+# this is a function that gets bug activity for list of bug ids
 # it can be called as the following:
 # $call = $rpc->call( 'Bug.history', { ids => [1,2] });
 sub history {
-    my ($self, $params) = validate(@_, 'ids');
-
-    Bugzilla->switch_to_shadow_db();
-
-    my $ids = $params->{ids};
-    defined $ids || ThrowCodeError('param_required', { param => 'ids' });
-
-    my %api_name = reverse %{ Bugzilla::Bug::FIELD_MAP() };
-    $api_name{'bug_group'} = 'groups';
-
-    my @return;
-    foreach my $bug_id (@$ids) {
-        my %item;
-        my $bug = Bugzilla::Bug->check($bug_id);
-        $bug_id = $bug->id;
-        $item{id} = $self->type('int', $bug_id);
-
-        my ($activity) = $bug->get_activity(undef, $params->{new_since});
-
-        my @history;
-        foreach my $changeset (@$activity) {
-            my %bug_history;
-            $bug_history{when} = $self->type('dateTime', $changeset->{when});
-            $bug_history{who}  = $self->type('string', $changeset->{who});
-            $bug_history{changes} = [];
-            foreach my $change (@{ $changeset->{changes} }) {
-                my $api_field = $api_name{$change->{fieldname}} || $change->{fieldname};
-                my $attach_id = delete $change->{attachid};
-                if ($attach_id) {
-                    $change->{attachment_id} = $self->type('int', $attach_id);
-                }
-                $change->{removed} = $self->type('string', $change->{removed});
-                $change->{added}   = $self->type('string', $change->{added});
-                $change->{field_name} = $self->type('string', $api_field);
-                delete $change->{fieldname};
-                push (@{$bug_history{changes}}, $change);
-            }
-            
-            push (@history, \%bug_history);
+  my ($self, $params) = validate(@_, 'ids');
+
+  Bugzilla->switch_to_shadow_db();
+
+  my $ids = $params->{ids};
+  defined $ids || ThrowCodeError('param_required', {param => 'ids'});
+
+  my %api_name = reverse %{Bugzilla::Bug::FIELD_MAP()};
+  $api_name{'bug_group'} = 'groups';
+
+  my @return;
+  foreach my $bug_id (@$ids) {
+    my %item;
+    my $bug = Bugzilla::Bug->check($bug_id);
+    $bug_id = $bug->id;
+    $item{id} = $self->type('int', $bug_id);
+
+    my ($activity) = $bug->get_activity(undef, $params->{new_since});
+
+    my @history;
+    foreach my $changeset (@$activity) {
+      my %bug_history;
+      $bug_history{when} = $self->type('dateTime', $changeset->{when});
+      $bug_history{who}  = $self->type('string',   $changeset->{who});
+      $bug_history{changes} = [];
+      foreach my $change (@{$changeset->{changes}}) {
+        my $api_field = $api_name{$change->{fieldname}} || $change->{fieldname};
+        my $attach_id = delete $change->{attachid};
+        if ($attach_id) {
+          $change->{attachment_id} = $self->type('int', $attach_id);
         }
+        $change->{removed}    = $self->type('string', $change->{removed});
+        $change->{added}      = $self->type('string', $change->{added});
+        $change->{field_name} = $self->type('string', $api_field);
+        delete $change->{fieldname};
+        push(@{$bug_history{changes}}, $change);
+      }
+
+      push(@history, \%bug_history);
+    }
 
-        $item{history} = \@history;
+    $item{history} = \@history;
 
-        # alias is returned in case users passes a mixture of ids and aliases
-        # then they get to know which bug activity relates to which value  
-        # they passed
-        $item{alias} = [ map { $self->type('string', $_) } @{ $bug->alias } ];
+    # alias is returned in case users passes a mixture of ids and aliases
+    # then they get to know which bug activity relates to which value
+    # they passed
+    $item{alias} = [map { $self->type('string', $_) } @{$bug->alias}];
 
-        push(@return, \%item);
-    }
+    push(@return, \%item);
+  }
 
-    return { bugs => \@return };
+  return {bugs => \@return};
 }
 
 sub search {
-    my ($self, $params) = @_;
-    my $user = Bugzilla->user;
-    my $dbh  = Bugzilla->dbh;
-
-    Bugzilla->switch_to_shadow_db();
-
-    my $match_params = dclone($params);
-    delete $match_params->{include_fields};
-    delete $match_params->{exclude_fields};
-
-    # Determine whether this is a quicksearch query
-    if (exists $match_params->{quicksearch}) {
-        my $quicksearch = quicksearch($match_params->{'quicksearch'});
-        my $cgi = Bugzilla::CGI->new($quicksearch);
-        $match_params = $cgi->Vars;
-    }
-
-    if ( defined($match_params->{offset}) and !defined($match_params->{limit}) ) {
-        ThrowCodeError('param_required',
-                       { param => 'limit', function => 'Bug.search()' });
-    }
-
-    my $max_results = Bugzilla->params->{max_search_results};
-    unless (defined $match_params->{limit} && $match_params->{limit} == 0) {
-        if (!defined $match_params->{limit} || $match_params->{limit} > $max_results) {
-            $match_params->{limit} = $max_results;
-        }
-    }
-    else {
-        delete $match_params->{limit};
-        delete $match_params->{offset};
-    }
+  my ($self, $params) = @_;
+  my $user = Bugzilla->user;
+  my $dbh  = Bugzilla->dbh;
 
-    $match_params = Bugzilla::Bug::map_fields($match_params);
+  Bugzilla->switch_to_shadow_db();
 
-    my %options = ( fields => ['bug_id'] );
+  my $match_params = dclone($params);
+  delete $match_params->{include_fields};
+  delete $match_params->{exclude_fields};
 
-    # Find the highest custom field id
-    my @field_ids = grep(/^f(\d+)$/, keys %$match_params);
-    my $last_field_id = @field_ids ? max @field_ids + 1 : 1;
+  # Determine whether this is a quicksearch query
+  if (exists $match_params->{quicksearch}) {
+    my $quicksearch = quicksearch($match_params->{'quicksearch'});
+    my $cgi         = Bugzilla::CGI->new($quicksearch);
+    $match_params = $cgi->Vars;
+  }
 
-    # Do special search types for certain fields.
-    if (my $change_when = delete $match_params->{'delta_ts'}) {
-        $match_params->{"f${last_field_id}"} = 'delta_ts';
-        $match_params->{"o${last_field_id}"} = 'greaterthaneq';
-        $match_params->{"v${last_field_id}"} = $change_when;
-        $last_field_id++;
-    }
-    if (my $creation_when = delete $match_params->{'creation_ts'}) {
-        $match_params->{"f${last_field_id}"} = 'creation_ts';
-        $match_params->{"o${last_field_id}"} = 'greaterthaneq';
-        $match_params->{"v${last_field_id}"} = $creation_when;
-        $last_field_id++;
-    }
-
-    # Some fields require a search type such as short desc, keywords, etc.
-    foreach my $param (qw(short_desc longdesc status_whiteboard bug_file_loc)) {
-        if (defined $match_params->{$param} && !defined $match_params->{$param . '_type'}) {
-            $match_params->{$param . '_type'} = 'allwordssubstr';
-        }
-    }
-    if (defined $match_params->{'keywords'} && !defined $match_params->{'keywords_type'}) {
-        $match_params->{'keywords_type'} = 'allwords';
-    }
+  if (defined($match_params->{offset}) and !defined($match_params->{limit})) {
+    ThrowCodeError('param_required',
+      {param => 'limit', function => 'Bug.search()'});
+  }
 
-    # Backwards compatibility with old method regarding role search
-    $match_params->{'reporter'} = delete $match_params->{'creator'} if $match_params->{'creator'};
-    foreach my $role (qw(assigned_to reporter qa_contact longdesc cc)) {
-        next if !exists $match_params->{$role};
-        my $value = delete $match_params->{$role};
-        $match_params->{"f${last_field_id}"} = $role;
-        $match_params->{"o${last_field_id}"} = "anywordssubstr";
-        $match_params->{"v${last_field_id}"} = ref $value ? join(" ", @{$value}) : $value;
-        $last_field_id++;
+  my $max_results = Bugzilla->params->{max_search_results};
+  unless (defined $match_params->{limit} && $match_params->{limit} == 0) {
+    if (!defined $match_params->{limit} || $match_params->{limit} > $max_results) {
+      $match_params->{limit} = $max_results;
     }
-
-    # If no other parameters have been passed other than limit and offset
-    # then we throw error if system is configured to do so.
-    if (!grep(!/^(limit|offset)$/, keys %$match_params)
-        && !Bugzilla->params->{search_allow_no_criteria})
+  }
+  else {
+    delete $match_params->{limit};
+    delete $match_params->{offset};
+  }
+
+  $match_params = Bugzilla::Bug::map_fields($match_params);
+
+  my %options = (fields => ['bug_id']);
+
+  # Find the highest custom field id
+  my @field_ids = grep(/^f(\d+)$/, keys %$match_params);
+  my $last_field_id = @field_ids ? max @field_ids + 1 : 1;
+
+  # Do special search types for certain fields.
+  if (my $change_when = delete $match_params->{'delta_ts'}) {
+    $match_params->{"f${last_field_id}"} = 'delta_ts';
+    $match_params->{"o${last_field_id}"} = 'greaterthaneq';
+    $match_params->{"v${last_field_id}"} = $change_when;
+    $last_field_id++;
+  }
+  if (my $creation_when = delete $match_params->{'creation_ts'}) {
+    $match_params->{"f${last_field_id}"} = 'creation_ts';
+    $match_params->{"o${last_field_id}"} = 'greaterthaneq';
+    $match_params->{"v${last_field_id}"} = $creation_when;
+    $last_field_id++;
+  }
+
+  # Some fields require a search type such as short desc, keywords, etc.
+  foreach my $param (qw(short_desc longdesc status_whiteboard bug_file_loc)) {
+    if (defined $match_params->{$param}
+      && !defined $match_params->{$param . '_type'})
     {
-        ThrowUserError('buglist_parameters_required');
+      $match_params->{$param . '_type'} = 'allwordssubstr';
     }
-
-    $options{order}  = [ split(/\s*,\s*/, delete $match_params->{order}) ] if $match_params->{order};
-    $options{params} = $match_params;
-
-    my $search = new Bugzilla::Search(%options);
-    my ($data) = $search->data;
-
-    if (!scalar @$data) {
-        return { bugs => [] };
-    }
-
-    # Search.pm won't return bugs that the user shouldn't see so no filtering is needed.
-    my @bug_ids = map { $_->[0] } @$data;
-    my %bug_objects = map { $_->id => $_ } @{ Bugzilla::Bug->new_from_list(\@bug_ids) };
-    my @bugs = map { $bug_objects{$_} } @bug_ids;
-    @bugs = map { $self->_bug_to_hash($_, $params) } @bugs;
-
-    return { bugs => \@bugs };
+  }
+  if (defined $match_params->{'keywords'}
+    && !defined $match_params->{'keywords_type'})
+  {
+    $match_params->{'keywords_type'} = 'allwords';
+  }
+
+  # Backwards compatibility with old method regarding role search
+  $match_params->{'reporter'} = delete $match_params->{'creator'}
+    if $match_params->{'creator'};
+  foreach my $role (qw(assigned_to reporter qa_contact longdesc cc)) {
+    next if !exists $match_params->{$role};
+    my $value = delete $match_params->{$role};
+    $match_params->{"f${last_field_id}"} = $role;
+    $match_params->{"o${last_field_id}"} = "anywordssubstr";
+    $match_params->{"v${last_field_id}"}
+      = ref $value ? join(" ", @{$value}) : $value;
+    $last_field_id++;
+  }
+
+  # If no other parameters have been passed other than limit and offset
+  # then we throw error if system is configured to do so.
+  if ( !grep(!/^(limit|offset)$/, keys %$match_params)
+    && !Bugzilla->params->{search_allow_no_criteria})
+  {
+    ThrowUserError('buglist_parameters_required');
+  }
+
+  $options{order} = [split(/\s*,\s*/, delete $match_params->{order})]
+    if $match_params->{order};
+  $options{params} = $match_params;
+
+  my $search = new Bugzilla::Search(%options);
+  my ($data) = $search->data;
+
+  if (!scalar @$data) {
+    return {bugs => []};
+  }
+
+# Search.pm won't return bugs that the user shouldn't see so no filtering is needed.
+  my @bug_ids = map { $_->[0] } @$data;
+  my %bug_objects
+    = map { $_->id => $_ } @{Bugzilla::Bug->new_from_list(\@bug_ids)};
+  my @bugs = map { $bug_objects{$_} } @bug_ids;
+  @bugs = map { $self->_bug_to_hash($_, $params) } @bugs;
+
+  return {bugs => \@bugs};
 }
 
 sub possible_duplicates {
-    my ($self, $params) = validate(@_, 'products');
-    my $user = Bugzilla->user;
-
-    Bugzilla->switch_to_shadow_db();
-
-    # Undo the array-ification that validate() does, for "summary".
-    $params->{summary} || ThrowCodeError('param_required',
-        { function => 'Bug.possible_duplicates', param => 'summary' });
-
-    my @products;
-    foreach my $name (@{ $params->{'products'} || [] }) {
-        my $object = $user->can_enter_product($name, THROW_ERROR);
-        push(@products, $object);
-    }
-
-    my $possible_dupes = Bugzilla::Bug->possible_duplicates(
-        { summary => $params->{summary}, products => \@products,
-          limit   => $params->{limit} });
-    my @hashes = map { $self->_bug_to_hash($_, $params) } @$possible_dupes;
-    $self->_add_update_tokens($params, $possible_dupes, \@hashes);
-    return { bugs => \@hashes };
+  my ($self, $params) = validate(@_, 'products');
+  my $user = Bugzilla->user;
+
+  Bugzilla->switch_to_shadow_db();
+
+  # Undo the array-ification that validate() does, for "summary".
+  $params->{summary}
+    || ThrowCodeError('param_required',
+    {function => 'Bug.possible_duplicates', param => 'summary'});
+
+  my @products;
+  foreach my $name (@{$params->{'products'} || []}) {
+    my $object = $user->can_enter_product($name, THROW_ERROR);
+    push(@products, $object);
+  }
+
+  my $possible_dupes = Bugzilla::Bug->possible_duplicates({
+    summary  => $params->{summary},
+    products => \@products,
+    limit    => $params->{limit}
+  });
+  my @hashes = map { $self->_bug_to_hash($_, $params) } @$possible_dupes;
+  $self->_add_update_tokens($params, $possible_dupes, \@hashes);
+  return {bugs => \@hashes};
 }
 
 sub update {
-    my ($self, $params) = validate(@_, 'ids');
+  my ($self, $params) = validate(@_, 'ids');
 
-    my $user = Bugzilla->login(LOGIN_REQUIRED);
-    my $dbh = Bugzilla->dbh;
+  my $user = Bugzilla->login(LOGIN_REQUIRED);
+  my $dbh  = Bugzilla->dbh;
 
-    # We skip certain fields because their set_ methods actually use
-    # the external names instead of the internal names.
-    $params = Bugzilla::Bug::map_fields($params, 
-        { summary => 1, platform => 1, severity => 1, url => 1 });
+  # We skip certain fields because their set_ methods actually use
+  # the external names instead of the internal names.
+  $params = Bugzilla::Bug::map_fields($params,
+    {summary => 1, platform => 1, severity => 1, url => 1});
 
-    my $ids = delete $params->{ids};
-    defined $ids || ThrowCodeError('param_required', { param => 'ids' });
+  my $ids = delete $params->{ids};
+  defined $ids || ThrowCodeError('param_required', {param => 'ids'});
 
-    my @bugs = map { Bugzilla::Bug->check_for_edit($_) } @$ids;
+  my @bugs = map { Bugzilla::Bug->check_for_edit($_) } @$ids;
 
-    my %values = %$params;
-    $values{other_bugs} = \@bugs;
+  my %values = %$params;
+  $values{other_bugs} = \@bugs;
 
-    if (exists $values{comment} and exists $values{comment}{comment}) {
-        $values{comment}{body} = delete $values{comment}{comment};
-    }
+  if (exists $values{comment} and exists $values{comment}{comment}) {
+    $values{comment}{body} = delete $values{comment}{comment};
+  }
 
-    # Prevent bugs that could be triggered by specifying fields that
-    # have valid "set_" functions in Bugzilla::Bug, but shouldn't be
-    # called using those field names.
-    delete $values{dependencies};
+  # Prevent bugs that could be triggered by specifying fields that
+  # have valid "set_" functions in Bugzilla::Bug, but shouldn't be
+  # called using those field names.
+  delete $values{dependencies};
 
-    # For backwards compatibility, treat alias string or array as a set action
-    if (exists $values{alias}) {
-        if (not ref $values{alias}) {
-            $values{alias} = { set => [ $values{alias} ] };
-        }
-        elsif (ref $values{alias} eq 'ARRAY') {
-            $values{alias} = { set => $values{alias} };
-        }
+  # For backwards compatibility, treat alias string or array as a set action
+  if (exists $values{alias}) {
+    if (not ref $values{alias}) {
+      $values{alias} = {set => [$values{alias}]};
     }
-
-    my $flags = delete $values{flags};
-
-    foreach my $bug (@bugs) {
-        $bug->set_all(\%values);
-        if ($flags) {
-            my ($old_flags, $new_flags) = extract_flags($flags, $bug);
-            $bug->set_flags($old_flags, $new_flags);
-        }
+    elsif (ref $values{alias} eq 'ARRAY') {
+      $values{alias} = {set => $values{alias}};
     }
+  }
 
-    my %all_changes;
-    $dbh->bz_start_transaction();
-    foreach my $bug (@bugs) {
-        $all_changes{$bug->id} = $bug->update();
-    }
-    $dbh->bz_commit_transaction();
+  my $flags = delete $values{flags};
 
-    foreach my $bug (@bugs) {
-        $bug->send_changes($all_changes{$bug->id});
+  foreach my $bug (@bugs) {
+    $bug->set_all(\%values);
+    if ($flags) {
+      my ($old_flags, $new_flags) = extract_flags($flags, $bug);
+      $bug->set_flags($old_flags, $new_flags);
     }
+  }
+
+  my %all_changes;
+  $dbh->bz_start_transaction();
+  foreach my $bug (@bugs) {
+    $all_changes{$bug->id} = $bug->update();
+  }
+  $dbh->bz_commit_transaction();
+
+  foreach my $bug (@bugs) {
+    $bug->send_changes($all_changes{$bug->id});
+  }
+
+  my %api_name = reverse %{Bugzilla::Bug::FIELD_MAP()};
+
+  # This doesn't normally belong in FIELD_MAP, but we do want to translate
+  # "bug_group" back into "groups".
+  $api_name{'bug_group'} = 'groups';
+
+  my @result;
+  foreach my $bug (@bugs) {
+    my %hash = (
+      id               => $self->type('int',      $bug->id),
+      last_change_time => $self->type('dateTime', $bug->delta_ts),
+      changes          => {},
+    );
 
-    my %api_name = reverse %{ Bugzilla::Bug::FIELD_MAP() };
-    # This doesn't normally belong in FIELD_MAP, but we do want to translate
-    # "bug_group" back into "groups".
-    $api_name{'bug_group'} = 'groups';
-
-    my @result;
-    foreach my $bug (@bugs) {
-        my %hash = (
-            id               => $self->type('int', $bug->id),
-            last_change_time => $self->type('dateTime', $bug->delta_ts),
-            changes          => {},
-        );
-
-        # alias is returned in case users pass a mixture of ids and aliases,
-        # so that they can know which set of changes relates to which value
-        # they passed.
-        $hash{alias} = [ map { $self->type('string', $_) } @{ $bug->alias } ];
-
-        my %changes = %{ $all_changes{$bug->id} };
-        foreach my $field (keys %changes) {
-            my $change = $changes{$field};
-            my $api_field = $api_name{$field} || $field;
-            # We normalize undef to an empty string, so that the API
-            # stays consistent for things like Deadline that can become
-            # empty.
-            $change->[0] = '' if !defined $change->[0];
-            $change->[1] = '' if !defined $change->[1];
-            $hash{changes}->{$api_field} = {
-                removed => $self->type('string', $change->[0]),
-                added   => $self->type('string', $change->[1]) 
-            };
-        }
-
-        push(@result, \%hash);
+    # alias is returned in case users pass a mixture of ids and aliases,
+    # so that they can know which set of changes relates to which value
+    # they passed.
+    $hash{alias} = [map { $self->type('string', $_) } @{$bug->alias}];
+
+    my %changes = %{$all_changes{$bug->id}};
+    foreach my $field (keys %changes) {
+      my $change = $changes{$field};
+      my $api_field = $api_name{$field} || $field;
+
+      # We normalize undef to an empty string, so that the API
+      # stays consistent for things like Deadline that can become
+      # empty.
+      $change->[0] = '' if !defined $change->[0];
+      $change->[1] = '' if !defined $change->[1];
+      $hash{changes}->{$api_field} = {
+        removed => $self->type('string', $change->[0]),
+        added   => $self->type('string', $change->[1])
+      };
     }
 
-    return { bugs => \@result };
+    push(@result, \%hash);
+  }
+
+  return {bugs => \@result};
 }
 
 sub create {
-    my ($self, $params) = @_;
-    my $dbh = Bugzilla->dbh;
+  my ($self, $params) = @_;
+  my $dbh = Bugzilla->dbh;
 
-    Bugzilla->login(LOGIN_REQUIRED);
+  Bugzilla->login(LOGIN_REQUIRED);
 
-    $params = Bugzilla::Bug::map_fields($params);
+  $params = Bugzilla::Bug::map_fields($params);
 
-    my $flags = delete $params->{flags};
+  my $flags = delete $params->{flags};
 
-    # We start a nested transaction in case flag setting fails
-    # we want the bug creation to roll back as well.
-    $dbh->bz_start_transaction();
+  # We start a nested transaction in case flag setting fails
+  # we want the bug creation to roll back as well.
+  $dbh->bz_start_transaction();
 
-    my $bug = Bugzilla::Bug->create($params);
+  my $bug = Bugzilla::Bug->create($params);
 
-    # Set bug flags
-    if ($flags) {
-        my ($flags, $new_flags) = extract_flags($flags, $bug);
-        $bug->set_flags($flags, $new_flags);
-        $bug->update($bug->creation_ts);
-    }
+  # Set bug flags
+  if ($flags) {
+    my ($flags, $new_flags) = extract_flags($flags, $bug);
+    $bug->set_flags($flags, $new_flags);
+    $bug->update($bug->creation_ts);
+  }
 
-    $dbh->bz_commit_transaction();
+  $dbh->bz_commit_transaction();
 
-    $bug->send_changes();
+  $bug->send_changes();
 
-    return { id => $self->type('int', $bug->bug_id) };
+  return {id => $self->type('int', $bug->bug_id)};
 }
 
 sub legal_values {
-    my ($self, $params) = @_;
+  my ($self, $params) = @_;
 
-    Bugzilla->switch_to_shadow_db();
+  Bugzilla->switch_to_shadow_db();
 
-    defined $params->{field} 
-        or ThrowCodeError('param_required', { param => 'field' });
+  defined $params->{field}
+    or ThrowCodeError('param_required', {param => 'field'});
 
-    my $field = Bugzilla::Bug::FIELD_MAP->{$params->{field}} 
-                || $params->{field};
+  my $field = Bugzilla::Bug::FIELD_MAP->{$params->{field}} || $params->{field};
 
-    my @global_selects =
-        @{ Bugzilla->fields({ is_select => 1, is_abnormal => 0 }) };
+  my @global_selects = @{Bugzilla->fields({is_select => 1, is_abnormal => 0})};
 
-    my $values;
-    if (grep($_->name eq $field, @global_selects)) {
-        # The field is a valid one.
-        trick_taint($field);
-        $values = get_legal_field_values($field);
-    }
-    elsif (grep($_ eq $field, PRODUCT_SPECIFIC_FIELDS)) {
-        my $id = $params->{product_id};
-        defined $id || ThrowCodeError('param_required',
-            { function => 'Bug.legal_values', param => 'product_id' });
-        grep($_->id eq $id, @{Bugzilla->user->get_accessible_products})
-            || ThrowUserError('product_access_denied', { id => $id });
-
-        my $product = new Bugzilla::Product($id);
-        my @objects;
-        if ($field eq 'version') {
-            @objects = @{$product->versions};
-        }
-        elsif ($field eq 'target_milestone') {
-            @objects = @{$product->milestones};
-        }
-        elsif ($field eq 'component') {
-            @objects = @{$product->components};
-        }
+  my $values;
+  if (grep($_->name eq $field, @global_selects)) {
 
-        $values = [map { $_->name } @objects];
+    # The field is a valid one.
+    trick_taint($field);
+    $values = get_legal_field_values($field);
+  }
+  elsif (grep($_ eq $field, PRODUCT_SPECIFIC_FIELDS)) {
+    my $id = $params->{product_id};
+    defined $id
+      || ThrowCodeError('param_required',
+      {function => 'Bug.legal_values', param => 'product_id'});
+    grep($_->id eq $id, @{Bugzilla->user->get_accessible_products})
+      || ThrowUserError('product_access_denied', {id => $id});
+
+    my $product = new Bugzilla::Product($id);
+    my @objects;
+    if ($field eq 'version') {
+      @objects = @{$product->versions};
     }
-    else {
-        ThrowCodeError('invalid_field_name', { field => $params->{field} });
+    elsif ($field eq 'target_milestone') {
+      @objects = @{$product->milestones};
     }
-
-    my @result;
-    foreach my $val (@$values) {
-        push(@result, $self->type('string', $val));
+    elsif ($field eq 'component') {
+      @objects = @{$product->components};
     }
 
-    return { values => \@result };
+    $values = [map { $_->name } @objects];
+  }
+  else {
+    ThrowCodeError('invalid_field_name', {field => $params->{field}});
+  }
+
+  my @result;
+  foreach my $val (@$values) {
+    push(@result, $self->type('string', $val));
+  }
+
+  return {values => \@result};
 }
 
 sub add_attachment {
-    my ($self, $params) = validate(@_, 'ids');
-    my $dbh = Bugzilla->dbh;
-
-    Bugzilla->login(LOGIN_REQUIRED);
-    defined $params->{ids}
-        || ThrowCodeError('param_required', { param => 'ids' });
-    defined $params->{data}
-        || ThrowCodeError('param_required', { param => 'data' });
-
-    my @bugs = map { Bugzilla::Bug->check_for_edit($_) } @{ $params->{ids} };
-
-    my @created;
-    $dbh->bz_start_transaction();
-    my $timestamp = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
-
-    my $flags = delete $params->{flags};
-
-    foreach my $bug (@bugs) {
-        my $attachment = Bugzilla::Attachment->create({
-            bug         => $bug,
-            creation_ts => $timestamp,
-            data        => $params->{data},
-            description => $params->{summary},
-            filename    => $params->{file_name},
-            mimetype    => $params->{content_type},
-            ispatch     => $params->{is_patch},
-            isprivate   => $params->{is_private},
-        });
-
-        if ($flags) {
-            my ($old_flags, $new_flags) = extract_flags($flags, $bug, $attachment);
-            $attachment->set_flags($old_flags, $new_flags);
-        }
+  my ($self, $params) = validate(@_, 'ids');
+  my $dbh = Bugzilla->dbh;
+
+  Bugzilla->login(LOGIN_REQUIRED);
+  defined $params->{ids}  || ThrowCodeError('param_required', {param => 'ids'});
+  defined $params->{data} || ThrowCodeError('param_required', {param => 'data'});
+
+  my @bugs = map { Bugzilla::Bug->check_for_edit($_) } @{$params->{ids}};
+
+  my @created;
+  $dbh->bz_start_transaction();
+  my $timestamp = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
+
+  my $flags = delete $params->{flags};
+
+  foreach my $bug (@bugs) {
+    my $attachment = Bugzilla::Attachment->create({
+      bug         => $bug,
+      creation_ts => $timestamp,
+      data        => $params->{data},
+      description => $params->{summary},
+      filename    => $params->{file_name},
+      mimetype    => $params->{content_type},
+      ispatch     => $params->{is_patch},
+      isprivate   => $params->{is_private},
+    });
 
-        $attachment->update($timestamp);
-        my $comment = $params->{comment} || '';
-        $attachment->bug->add_comment($comment, 
-            { isprivate  => $attachment->isprivate,
-              type       => CMT_ATTACHMENT_CREATED,
-              extra_data => $attachment->id });
-        push(@created, $attachment);
+    if ($flags) {
+      my ($old_flags, $new_flags) = extract_flags($flags, $bug, $attachment);
+      $attachment->set_flags($old_flags, $new_flags);
     }
-    $_->bug->update($timestamp) foreach @created;
-    $dbh->bz_commit_transaction();
 
-    $_->send_changes() foreach @bugs;
+    $attachment->update($timestamp);
+    my $comment = $params->{comment} || '';
+    $attachment->bug->add_comment(
+      $comment,
+      {
+        isprivate  => $attachment->isprivate,
+        type       => CMT_ATTACHMENT_CREATED,
+        extra_data => $attachment->id
+      }
+    );
+    push(@created, $attachment);
+  }
+  $_->bug->update($timestamp) foreach @created;
+  $dbh->bz_commit_transaction();
+
+  $_->send_changes() foreach @bugs;
 
-    my @created_ids = map { $_->id } @created;
+  my @created_ids = map { $_->id } @created;
 
-    return { ids => \@created_ids };
+  return {ids => \@created_ids};
 }
 
 sub update_attachment {
-    my ($self, $params) = validate(@_, 'ids');
+  my ($self, $params) = validate(@_, 'ids');
+
+  my $user = Bugzilla->login(LOGIN_REQUIRED);
+  my $dbh  = Bugzilla->dbh;
+
+  my $ids = delete $params->{ids};
+  defined $ids || ThrowCodeError('param_required', {param => 'ids'});
+
+  # Some fields cannot be sent to set_all
+  foreach my $key (qw(login password token)) {
+    delete $params->{$key};
+  }
+
+  $params = translate($params, ATTACHMENT_MAPPED_SETTERS);
+
+  # Get all the attachments, after verifying that they exist and are editable
+  my @attachments = ();
+  my %bugs        = ();
+  foreach my $id (@$ids) {
+    my $attachment = Bugzilla::Attachment->new($id)
+      || ThrowUserError("invalid_attach_id", {attach_id => $id});
+    my $bug = $attachment->bug;
+    $attachment->_check_bug;
+
+    push @attachments, $attachment;
+    $bugs{$bug->id} = $bug;
+  }
+
+  my $flags   = delete $params->{flags};
+  my $comment = delete $params->{comment};
+
+  # Update the values
+  foreach my $attachment (@attachments) {
+    my ($update_flags, $new_flags)
+      = $flags ? extract_flags($flags, $attachment->bug, $attachment) : ([], []);
+    if ($attachment->validate_can_edit) {
+      $attachment->set_all($params);
+      $attachment->set_flags($update_flags, $new_flags) if $flags;
+    }
+    elsif (scalar @$update_flags && !scalar(@$new_flags) && !scalar keys %$params) {
+
+      # Requestees can set flags targetted to them, even if they cannot
+      # edit the attachment. Flag setters can edit their own flags too.
+      my %flag_list = map { $_->{id} => $_ } @$update_flags;
+      my $flag_objs = Bugzilla::Flag->new_from_list([keys %flag_list]);
+      my @editable_flags;
+      foreach my $flag_obj (@$flag_objs) {
+        if ($flag_obj->setter_id == $user->id
+          || ($flag_obj->requestee_id && $flag_obj->requestee_id == $user->id))
+        {
+          push(@editable_flags, $flag_list{$flag_obj->id});
+        }
+      }
+      if (!scalar @editable_flags) {
+        ThrowUserError("illegal_attachment_edit", {attach_id => $attachment->id});
+      }
+      $attachment->set_flags(\@editable_flags, []);
+    }
+    else {
+      ThrowUserError("illegal_attachment_edit", {attach_id => $attachment->id});
+    }
+  }
 
-    my $user = Bugzilla->login(LOGIN_REQUIRED);
-    my $dbh = Bugzilla->dbh;
+  $dbh->bz_start_transaction();
 
-    my $ids = delete $params->{ids};
-    defined $ids || ThrowCodeError('param_required', { param => 'ids' });
+  # Do the actual update and get information to return to user
+  my @result;
+  foreach my $attachment (@attachments) {
+    my $changes = $attachment->update();
 
-    # Some fields cannot be sent to set_all
-    foreach my $key (qw(login password token)) {
-        delete $params->{$key};
+    if ($comment = trim($comment)) {
+      $attachment->bug->add_comment(
+        $comment,
+        {
+          isprivate  => $attachment->isprivate,
+          type       => CMT_ATTACHMENT_UPDATED,
+          extra_data => $attachment->id
+        }
+      );
     }
 
-    $params = translate($params, ATTACHMENT_MAPPED_SETTERS);
+    $changes = translate($changes, ATTACHMENT_MAPPED_RETURNS);
 
-    # Get all the attachments, after verifying that they exist and are editable
-    my @attachments = ();
-    my %bugs = ();
-    foreach my $id (@$ids) {
-        my $attachment = Bugzilla::Attachment->new($id)
-          || ThrowUserError("invalid_attach_id", { attach_id => $id });
-        my $bug = $attachment->bug;
-        $attachment->_check_bug;
+    my %hash = (
+      id               => $self->type('int',      $attachment->id),
+      last_change_time => $self->type('dateTime', $attachment->modification_time),
+      changes          => {},
+    );
 
-        push @attachments, $attachment;
-        $bugs{$bug->id} = $bug;
-    }
+    foreach my $field (keys %$changes) {
+      my $change = $changes->{$field};
 
-    my $flags = delete $params->{flags};
-    my $comment = delete $params->{comment};
-
-    # Update the values
-    foreach my $attachment (@attachments) {
-        my ($update_flags, $new_flags) = $flags
-            ? extract_flags($flags, $attachment->bug, $attachment)
-            : ([], []);
-        if ($attachment->validate_can_edit) {
-            $attachment->set_all($params);
-            $attachment->set_flags($update_flags, $new_flags) if $flags;
-        }
-        elsif (scalar @$update_flags && !scalar(@$new_flags) && !scalar keys %$params) {
-            # Requestees can set flags targetted to them, even if they cannot
-            # edit the attachment. Flag setters can edit their own flags too.
-            my %flag_list = map { $_->{id} => $_ } @$update_flags;
-            my $flag_objs = Bugzilla::Flag->new_from_list([ keys %flag_list ]);
-            my @editable_flags;
-            foreach my $flag_obj (@$flag_objs) {
-                if ($flag_obj->setter_id == $user->id
-                    || ($flag_obj->requestee_id && $flag_obj->requestee_id == $user->id))
-                {
-                    push(@editable_flags, $flag_list{$flag_obj->id});
-                }
-            }
-            if (!scalar @editable_flags) {
-                ThrowUserError("illegal_attachment_edit", { attach_id => $attachment->id });
-            }
-            $attachment->set_flags(\@editable_flags, []);
-        }
-        else {
-            ThrowUserError("illegal_attachment_edit", { attach_id => $attachment->id });
-        }
+      # We normalize undef to an empty string, so that the API
+      # stays consistent for things like Deadline that can become
+      # empty.
+      $hash{changes}->{$field} = {
+        removed => $self->type('string', $change->[0] // ''),
+        added   => $self->type('string', $change->[1] // '')
+      };
     }
 
-    $dbh->bz_start_transaction();
-
-    # Do the actual update and get information to return to user
-    my @result;
-    foreach my $attachment (@attachments) {
-        my $changes = $attachment->update();
-
-        if ($comment = trim($comment)) {
-            $attachment->bug->add_comment($comment,
-                { isprivate  => $attachment->isprivate,
-                  type       => CMT_ATTACHMENT_UPDATED,
-                  extra_data => $attachment->id });
-        }
+    push(@result, \%hash);
+  }
 
-        $changes = translate($changes, ATTACHMENT_MAPPED_RETURNS);
+  $dbh->bz_commit_transaction();
 
-        my %hash = (
-            id               => $self->type('int', $attachment->id),
-            last_change_time => $self->type('dateTime', $attachment->modification_time),
-            changes          => {},
-        );
+  # Email users about the change
+  foreach my $bug (values %bugs) {
+    $bug->update();
+    $bug->send_changes();
+  }
 
-        foreach my $field (keys %$changes) {
-            my $change = $changes->{$field};
+  # Return the information to the user
+  return {attachments => \@result};
+}
 
-            # We normalize undef to an empty string, so that the API
-            # stays consistent for things like Deadline that can become
-            # empty.
-            $hash{changes}->{$field} = {
-                removed => $self->type('string', $change->[0] // ''),
-                added   => $self->type('string', $change->[1] // '')
-            };
-        }
+sub add_comment {
+  my ($self, $params) = @_;
 
-        push(@result, \%hash);
-    }
+  # The user must login in order add a comment
+  my $user = Bugzilla->login(LOGIN_REQUIRED);
 
-    $dbh->bz_commit_transaction();
+  # Check parameters
+  defined $params->{id} || ThrowCodeError('param_required', {param => 'id'});
+  my $comment = $params->{comment};
+  (defined $comment && trim($comment) ne '')
+    || ThrowCodeError('param_required', {param => 'comment'});
 
-    # Email users about the change
-    foreach my $bug (values %bugs) {
-        $bug->update();
-        $bug->send_changes();
-    }
+  my $bug = Bugzilla::Bug->check_for_edit($params->{id});
 
-    # Return the information to the user
-    return { attachments => \@result };
-}
+  # Backwards-compatibility for versions before 3.6
+  if (defined $params->{private}) {
+    $params->{is_private} = delete $params->{private};
+  }
 
-sub add_comment {
-    my ($self, $params) = @_;
-
-    # The user must login in order add a comment
-    my $user = Bugzilla->login(LOGIN_REQUIRED);
-
-    # Check parameters
-    defined $params->{id} 
-        || ThrowCodeError('param_required', { param => 'id' }); 
-    my $comment = $params->{comment}; 
-    (defined $comment && trim($comment) ne '')
-        || ThrowCodeError('param_required', { param => 'comment' });
-    
-    my $bug = Bugzilla::Bug->check_for_edit($params->{id});
-
-    # Backwards-compatibility for versions before 3.6    
-    if (defined $params->{private}) {
-        $params->{is_private} = delete $params->{private};
-    }
-    # Append comment
-    $bug->add_comment($comment, { isprivate => $params->{is_private},
-                                  work_time => $params->{work_time} });
-    $bug->update();
+  # Append comment
+  $bug->add_comment($comment,
+    {isprivate => $params->{is_private}, work_time => $params->{work_time}});
+  $bug->update();
 
-    my $new_comment_id = $bug->{added_comments}[0]->id;
+  my $new_comment_id = $bug->{added_comments}[0]->id;
 
-    # Send mail.
-    Bugzilla::BugMail::Send($bug->bug_id, { changer => $user });
+  # Send mail.
+  Bugzilla::BugMail::Send($bug->bug_id, {changer => $user});
 
-    return { id => $self->type('int', $new_comment_id) };
+  return {id => $self->type('int', $new_comment_id)};
 }
 
 sub update_see_also {
-    my ($self, $params) = @_;
-
-    my $user = Bugzilla->login(LOGIN_REQUIRED);
-
-    # Check parameters
-    $params->{ids}
-        || ThrowCodeError('param_required', { param => 'id' });
-    my ($add, $remove) = @$params{qw(add remove)};
-    ($add || $remove)
-        or ThrowCodeError('params_required', { params => ['add', 'remove'] });
-
-    my @bugs;
-    foreach my $id (@{ $params->{ids} }) {
-        my $bug = Bugzilla::Bug->check_for_edit($id);
-        push(@bugs, $bug);
-        if ($remove) {
-            $bug->remove_see_also($_) foreach @$remove;
-        }
-        if ($add) {
-            $bug->add_see_also($_) foreach @$add;
-        }
+  my ($self, $params) = @_;
+
+  my $user = Bugzilla->login(LOGIN_REQUIRED);
+
+  # Check parameters
+  $params->{ids} || ThrowCodeError('param_required', {param => 'id'});
+  my ($add, $remove) = @$params{qw(add remove)};
+  ($add || $remove)
+    or ThrowCodeError('params_required', {params => ['add', 'remove']});
+
+  my @bugs;
+  foreach my $id (@{$params->{ids}}) {
+    my $bug = Bugzilla::Bug->check_for_edit($id);
+    push(@bugs, $bug);
+    if ($remove) {
+      $bug->remove_see_also($_) foreach @$remove;
     }
-    
-    my %changes;
-    foreach my $bug (@bugs) {
-        my $change = $bug->update();
-        if (my $see_also = $change->{see_also}) {
-            $changes{$bug->id}->{see_also} = {
-                removed => [split(', ', $see_also->[0])],
-                added   => [split(', ', $see_also->[1])],
-            };
-        }
-        else {
-            # We still want a changes entry, for API consistency.
-            $changes{$bug->id}->{see_also} = { added => [], removed => [] };
-        }
-
-        Bugzilla::BugMail::Send($bug->id, { changer => $user });
+    if ($add) {
+      $bug->add_see_also($_) foreach @$add;
+    }
+  }
+
+  my %changes;
+  foreach my $bug (@bugs) {
+    my $change = $bug->update();
+    if (my $see_also = $change->{see_also}) {
+      $changes{$bug->id}->{see_also} = {
+        removed => [split(', ', $see_also->[0])],
+        added   => [split(', ', $see_also->[1])],
+      };
     }
+    else {
+      # We still want a changes entry, for API consistency.
+      $changes{$bug->id}->{see_also} = {added => [], removed => []};
+    }
+
+    Bugzilla::BugMail::Send($bug->id, {changer => $user});
+  }
 
-    return { changes => \%changes };
+  return {changes => \%changes};
 }
 
 sub attachments {
-    my ($self, $params) = validate(@_, 'ids', 'attachment_ids');
+  my ($self, $params) = validate(@_, 'ids', 'attachment_ids');
 
-    Bugzilla->switch_to_shadow_db() unless Bugzilla->user->id;
+  Bugzilla->switch_to_shadow_db() unless Bugzilla->user->id;
 
-    if (!(defined $params->{ids}
-          or defined $params->{attachment_ids}))
-    {
-        ThrowCodeError('param_required',
-                       { function => 'Bug.attachments', 
-                         params   => ['ids', 'attachment_ids'] });
+  if (!(defined $params->{ids} or defined $params->{attachment_ids})) {
+    ThrowCodeError('param_required',
+      {function => 'Bug.attachments', params => ['ids', 'attachment_ids']});
+  }
+
+  my $ids        = $params->{ids}            || [];
+  my $attach_ids = $params->{attachment_ids} || [];
+
+  my %bugs;
+  foreach my $bug_id (@$ids) {
+    my $bug = Bugzilla::Bug->check($bug_id);
+    $bugs{$bug->id} = [];
+    foreach my $attach (@{$bug->attachments}) {
+      push @{$bugs{$bug->id}}, $self->_attachment_to_hash($attach, $params);
     }
-
-    my $ids = $params->{ids} || [];
-    my $attach_ids = $params->{attachment_ids} || [];
-
-    my %bugs;
-    foreach my $bug_id (@$ids) {
-        my $bug = Bugzilla::Bug->check($bug_id);
-        $bugs{$bug->id} = [];
-        foreach my $attach (@{$bug->attachments}) {
-            push @{$bugs{$bug->id}},
-                $self->_attachment_to_hash($attach, $params);
-        }
+  }
+
+  my %attachments;
+  foreach my $attach (@{Bugzilla::Attachment->new_from_list($attach_ids)}) {
+    Bugzilla::Bug->check($attach->bug_id);
+    if ($attach->isprivate && !Bugzilla->user->is_insider) {
+      ThrowUserError('auth_failure',
+        {action => 'access', object => 'attachment', attach_id => $attach->id});
     }
+    $attachments{$attach->id} = $self->_attachment_to_hash($attach, $params);
+  }
 
-    my %attachments;
-    foreach my $attach (@{Bugzilla::Attachment->new_from_list($attach_ids)}) {
-        Bugzilla::Bug->check($attach->bug_id);
-        if ($attach->isprivate && !Bugzilla->user->is_insider) {
-            ThrowUserError('auth_failure', {action    => 'access',
-                                            object    => 'attachment',
-                                            attach_id => $attach->id});
-        }
-        $attachments{$attach->id} =
-            $self->_attachment_to_hash($attach, $params);
-    }
-
-    return { bugs => \%bugs, attachments => \%attachments };
+  return {bugs => \%bugs, attachments => \%attachments};
 }
 
 sub update_tags {
-    my ($self, $params) = @_;
+  my ($self, $params) = @_;
 
-    Bugzilla->login(LOGIN_REQUIRED);
+  Bugzilla->login(LOGIN_REQUIRED);
 
-    my $ids  = $params->{ids};
-    my $tags = $params->{tags};
+  my $ids  = $params->{ids};
+  my $tags = $params->{tags};
 
-    ThrowCodeError('param_required',
-                   { function => 'Bug.update_tags', 
-                     param    => 'ids' }) if !defined $ids;
+  ThrowCodeError('param_required',
+    {function => 'Bug.update_tags', param => 'ids'})
+    if !defined $ids;
 
-    ThrowCodeError('param_required',
-                   { function => 'Bug.update_tags', 
-                     param    => 'tags' }) if !defined $tags;
+  ThrowCodeError('param_required',
+    {function => 'Bug.update_tags', param => 'tags'})
+    if !defined $tags;
 
-    my %changes;
-    foreach my $bug_id (@$ids) {
-        my $bug = Bugzilla::Bug->check($bug_id);
-        my @old_tags = @{ $bug->tags };
+  my %changes;
+  foreach my $bug_id (@$ids) {
+    my $bug      = Bugzilla::Bug->check($bug_id);
+    my @old_tags = @{$bug->tags};
 
-        $bug->remove_tag($_) foreach @{ $tags->{remove} || [] };
-        $bug->add_tag($_) foreach @{ $tags->{add} || [] };
+    $bug->remove_tag($_) foreach @{$tags->{remove} || []};
+    $bug->add_tag($_)    foreach @{$tags->{add}    || []};
 
-        my ($removed, $added) = diff_arrays(\@old_tags, $bug->tags);
+    my ($removed, $added) = diff_arrays(\@old_tags, $bug->tags);
 
-        my @removed = map { $self->type('string', $_) } @$removed;
-        my @added   = map { $self->type('string', $_) } @$added;
+    my @removed = map { $self->type('string', $_) } @$removed;
+    my @added   = map { $self->type('string', $_) } @$added;
 
-        $changes{$bug->id}->{tags} = {
-            removed => \@removed,
-            added   => \@added
-        };
-    }
+    $changes{$bug->id}->{tags} = {removed => \@removed, added => \@added};
+  }
 
-    return { changes => \%changes };
+  return {changes => \%changes};
 }
 
 sub update_comment_tags {
-    my ($self, $params) = @_;
-
-    my $user = Bugzilla->login(LOGIN_REQUIRED);
-    Bugzilla->params->{'comment_taggers_group'}
-        || ThrowUserError("comment_tag_disabled");
-    $user->can_tag_comments
-        || ThrowUserError("auth_failure",
-                          { group  => Bugzilla->params->{'comment_taggers_group'},
-                            action => "update",
-                            object => "comment_tags" });
-
-    my $comment_id  = $params->{comment_id}
-        // ThrowCodeError('param_required',
-                          { 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();
-    if ($comment->is_private && !$user->is_insider) {
-        ThrowUserError('comment_is_private', { id => $comment_id });
-    }
+  my ($self, $params) = @_;
 
-    my $dbh = Bugzilla->dbh;
-    $dbh->bz_start_transaction();
-    foreach my $tag (@{ $params->{add} || [] }) {
-        $comment->add_tag($tag) if defined $tag;
-    }
-    foreach my $tag (@{ $params->{remove} || [] }) {
-        $comment->remove_tag($tag) if defined $tag;
+  my $user = Bugzilla->login(LOGIN_REQUIRED);
+  Bugzilla->params->{'comment_taggers_group'}
+    || ThrowUserError("comment_tag_disabled");
+  $user->can_tag_comments || ThrowUserError(
+    "auth_failure",
+    {
+      group  => Bugzilla->params->{'comment_taggers_group'},
+      action => "update",
+      object => "comment_tags"
     }
-    $comment->update();
-    $dbh->bz_commit_transaction();
-
-    return $comment->tags;
+  );
+
+  my $comment_id = $params->{comment_id} // ThrowCodeError('param_required',
+    {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();
+  if ($comment->is_private && !$user->is_insider) {
+    ThrowUserError('comment_is_private', {id => $comment_id});
+  }
+
+  my $dbh = Bugzilla->dbh;
+  $dbh->bz_start_transaction();
+  foreach my $tag (@{$params->{add} || []}) {
+    $comment->add_tag($tag) if defined $tag;
+  }
+  foreach my $tag (@{$params->{remove} || []}) {
+    $comment->remove_tag($tag) if defined $tag;
+  }
+  $comment->update();
+  $dbh->bz_commit_transaction();
+
+  return $comment->tags;
 }
 
 sub search_comment_tags {
-    my ($self, $params) = @_;
-
-    Bugzilla->login(LOGIN_REQUIRED);
-    Bugzilla->params->{'comment_taggers_group'}
-        || ThrowUserError("comment_tag_disabled");
-    Bugzilla->user->can_tag_comments
-        || ThrowUserError("auth_failure", { group  => Bugzilla->params->{'comment_taggers_group'},
-                                            action => "search",
-                                            object => "comment_tags"});
-
-    my $query = $params->{query};
-    $query
-        // ThrowCodeError('param_required', { param => 'query' });
-    my $limit = $params->{limit} || 7;
-    detaint_natural($limit)
-        || ThrowCodeError('param_must_be_numeric', { param    => 'limit',
-                                                     function => 'Bug.search_comment_tags' });
-
-
-    my $tags = Bugzilla::Comment::TagWeights->match({
-        WHERE => {
-            'tag LIKE ?' => "\%$query\%",
-        },
-        LIMIT => $limit,
+  my ($self, $params) = @_;
+
+  Bugzilla->login(LOGIN_REQUIRED);
+  Bugzilla->params->{'comment_taggers_group'}
+    || ThrowUserError("comment_tag_disabled");
+  Bugzilla->user->can_tag_comments || ThrowUserError(
+    "auth_failure",
+    {
+      group  => Bugzilla->params->{'comment_taggers_group'},
+      action => "search",
+      object => "comment_tags"
+    }
+  );
+
+  my $query = $params->{query};
+  $query // ThrowCodeError('param_required', {param => 'query'});
+  my $limit = $params->{limit} || 7;
+  detaint_natural($limit)
+    || ThrowCodeError('param_must_be_numeric',
+    {param => 'limit', function => 'Bug.search_comment_tags'});
+
+
+  my $tags
+    = Bugzilla::Comment::TagWeights->match({
+    WHERE => {'tag LIKE ?' => "\%$query\%",}, LIMIT => $limit,
     });
-    return [ map { $_->tag } @$tags ];
+  return [map { $_->tag } @$tags];
 }
 
 ##############################
@@ -1197,232 +1209,238 @@ sub search_comment_tags {
 # return them directly.
 
 sub _bug_to_hash {
-    my ($self, $bug, $params) = @_;
-
-    # All the basic bug attributes are here, in alphabetical order.
-    # A bug attribute is "basic" if it doesn't require an additional
-    # database call to get the info.
-    my %item = %{ filter $params, {
-        # No need to format $bug->deadline specially, because Bugzilla::Bug
-        # already does it for us.
-        deadline         => $self->type('string', $bug->deadline),
-        id               => $self->type('int', $bug->bug_id),
-        is_confirmed     => $self->type('boolean', $bug->everconfirmed),
-        op_sys           => $self->type('string', $bug->op_sys),
-        platform         => $self->type('string', $bug->rep_platform),
-        priority         => $self->type('string', $bug->priority),
-        resolution       => $self->type('string', $bug->resolution),
-        severity         => $self->type('string', $bug->bug_severity),
-        status           => $self->type('string', $bug->bug_status),
-        summary          => $self->type('string', $bug->short_desc),
-        target_milestone => $self->type('string', $bug->target_milestone),
-        url              => $self->type('string', $bug->bug_file_loc),
-        version          => $self->type('string', $bug->version),
-        whiteboard       => $self->type('string', $bug->status_whiteboard),
-    } };
-
-    # First we handle any fields that require extra work (such as date parsing
-    # or SQL calls).
-    if (filter_wants $params, 'alias') {
-        $item{alias} = [ map { $self->type('string', $_) } @{ $bug->alias } ];
-    }
-    if (filter_wants $params, 'assigned_to') {
-        $item{'assigned_to'} = $self->type('email', $bug->assigned_to->login);
-        $item{'assigned_to_detail'} = $self->_user_to_hash($bug->assigned_to, $params, undef, 'assigned_to');
-    }
-    if (filter_wants $params, 'blocks') {
-        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('email', $_) } @{ $bug->cc };
-        $item{'cc'} = \@cc;
-        $item{'cc_detail'} = [ map { $self->_user_to_hash($_, $params, undef, 'cc') } @{ $bug->cc_users } ];
-    }
-    if (filter_wants $params, 'creation_time') {
-        $item{'creation_time'} = $self->type('dateTime', $bug->creation_ts);
-    }
-    if (filter_wants $params, 'creator') {
-        $item{'creator'} = $self->type('email', $bug->reporter->login);
-        $item{'creator_detail'} = $self->_user_to_hash($bug->reporter, $params, undef, 'creator');
-    }
-    if (filter_wants $params, 'depends_on') {
-        my @depends_on = map { $self->type('int', $_) } @{ $bug->dependson };
-        $item{'depends_on'} = \@depends_on;
-    }
-    if (filter_wants $params, 'dupe_of') {
-        $item{'dupe_of'} = $self->type('int', $bug->dup_id);
-    }
-    if (filter_wants $params, 'groups') {
-        my @groups = map { $self->type('string', $_->name) }
-                     @{ $bug->groups_in };
-        $item{'groups'} = \@groups;
-    }
-    if (filter_wants $params, 'is_open') {
-        $item{'is_open'} = $self->type('boolean', $bug->status->is_open);
-    }
-    if (filter_wants $params, 'keywords') {
-        my @keywords = map { $self->type('string', $_->name) }
-                       @{ $bug->keyword_objects };
-        $item{'keywords'} = \@keywords;
-    }
-    if (filter_wants $params, 'last_change_time') {
-        $item{'last_change_time'} = $self->type('dateTime', $bug->delta_ts);
-    }
-    if (filter_wants $params, 'product') {
-        $item{product} = $self->type('string', $bug->product);
+  my ($self, $bug, $params) = @_;
+
+  # All the basic bug attributes are here, in alphabetical order.
+  # A bug attribute is "basic" if it doesn't require an additional
+  # database call to get the info.
+  my %item = %{filter $params,
+    {
+      # No need to format $bug->deadline specially, because Bugzilla::Bug
+      # already does it for us.
+      deadline         => $self->type('string',  $bug->deadline),
+      id               => $self->type('int',     $bug->bug_id),
+      is_confirmed     => $self->type('boolean', $bug->everconfirmed),
+      op_sys           => $self->type('string',  $bug->op_sys),
+      platform         => $self->type('string',  $bug->rep_platform),
+      priority         => $self->type('string',  $bug->priority),
+      resolution       => $self->type('string',  $bug->resolution),
+      severity         => $self->type('string',  $bug->bug_severity),
+      status           => $self->type('string',  $bug->bug_status),
+      summary          => $self->type('string',  $bug->short_desc),
+      target_milestone => $self->type('string',  $bug->target_milestone),
+      url              => $self->type('string',  $bug->bug_file_loc),
+      version          => $self->type('string',  $bug->version),
+      whiteboard       => $self->type('string',  $bug->status_whiteboard),
     }
-    if (filter_wants $params, 'qa_contact') {
-        my $qa_login = $bug->qa_contact ? $bug->qa_contact->login : '';
-        $item{'qa_contact'} = $self->type('email', $qa_login);
-        if ($bug->qa_contact) {
-            $item{'qa_contact_detail'} = $self->_user_to_hash($bug->qa_contact, $params, undef, 'qa_contact');
-        }
+  };
+
+  # First we handle any fields that require extra work (such as date parsing
+  # or SQL calls).
+  if (filter_wants $params, 'alias') {
+    $item{alias} = [map { $self->type('string', $_) } @{$bug->alias}];
+  }
+  if (filter_wants $params, 'assigned_to') {
+    $item{'assigned_to'} = $self->type('email', $bug->assigned_to->login);
+    $item{'assigned_to_detail'}
+      = $self->_user_to_hash($bug->assigned_to, $params, undef, 'assigned_to');
+  }
+  if (filter_wants $params, 'blocks') {
+    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('email', $_) } @{$bug->cc};
+    $item{'cc'} = \@cc;
+    $item{'cc_detail'}
+      = [map { $self->_user_to_hash($_, $params, undef, 'cc') } @{$bug->cc_users}];
+  }
+  if (filter_wants $params, 'creation_time') {
+    $item{'creation_time'} = $self->type('dateTime', $bug->creation_ts);
+  }
+  if (filter_wants $params, 'creator') {
+    $item{'creator'} = $self->type('email', $bug->reporter->login);
+    $item{'creator_detail'}
+      = $self->_user_to_hash($bug->reporter, $params, undef, 'creator');
+  }
+  if (filter_wants $params, 'depends_on') {
+    my @depends_on = map { $self->type('int', $_) } @{$bug->dependson};
+    $item{'depends_on'} = \@depends_on;
+  }
+  if (filter_wants $params, 'dupe_of') {
+    $item{'dupe_of'} = $self->type('int', $bug->dup_id);
+  }
+  if (filter_wants $params, 'groups') {
+    my @groups = map { $self->type('string', $_->name) } @{$bug->groups_in};
+    $item{'groups'} = \@groups;
+  }
+  if (filter_wants $params, 'is_open') {
+    $item{'is_open'} = $self->type('boolean', $bug->status->is_open);
+  }
+  if (filter_wants $params, 'keywords') {
+    my @keywords = map { $self->type('string', $_->name) } @{$bug->keyword_objects};
+    $item{'keywords'} = \@keywords;
+  }
+  if (filter_wants $params, 'last_change_time') {
+    $item{'last_change_time'} = $self->type('dateTime', $bug->delta_ts);
+  }
+  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('email', $qa_login);
+    if ($bug->qa_contact) {
+      $item{'qa_contact_detail'}
+        = $self->_user_to_hash($bug->qa_contact, $params, undef, 'qa_contact');
     }
-    if (filter_wants $params, 'see_also') {
-        my @see_also = map { $self->type('string', $_->name) }
-                       @{ $bug->see_also };
-        $item{'see_also'} = \@see_also;
+  }
+  if (filter_wants $params, 'see_also') {
+    my @see_also = map { $self->type('string', $_->name) } @{$bug->see_also};
+    $item{'see_also'} = \@see_also;
+  }
+  if (filter_wants $params, 'flags') {
+    $item{'flags'} = [map { $self->_flag_to_hash($_) } @{$bug->flags}];
+  }
+  if (filter_wants $params, 'tags', 'extra') {
+    $item{'tags'} = $bug->tags;
+  }
+
+  # And now custom fields
+  my @custom_fields = Bugzilla->active_custom_fields;
+  foreach my $field (@custom_fields) {
+    my $name = $field->name;
+    next if !filter_wants($params, $name, ['default', 'custom']);
+    if ($field->type == FIELD_TYPE_BUG_ID) {
+      $item{$name} = $self->type('int', $bug->$name);
     }
-    if (filter_wants $params, 'flags') {
-        $item{'flags'} = [ map { $self->_flag_to_hash($_) } @{$bug->flags} ];
+    elsif ($field->type == FIELD_TYPE_DATETIME || $field->type == FIELD_TYPE_DATE) {
+      $item{$name} = $self->type('dateTime', $bug->$name);
     }
-    if (filter_wants $params, 'tags', 'extra') {
-        $item{'tags'} = $bug->tags;
+    elsif ($field->type == FIELD_TYPE_MULTI_SELECT) {
+      my @values = map { $self->type('string', $_) } @{$bug->$name};
+      $item{$name} = \@values;
     }
-
-    # And now custom fields
-    my @custom_fields = Bugzilla->active_custom_fields;
-    foreach my $field (@custom_fields) {
-        my $name = $field->name;
-        next if !filter_wants($params, $name, ['default', 'custom']);
-        if ($field->type == FIELD_TYPE_BUG_ID) {
-            $item{$name} = $self->type('int', $bug->$name);
-        }
-        elsif ($field->type == FIELD_TYPE_DATETIME
-               || $field->type == FIELD_TYPE_DATE)
-        {
-            $item{$name} = $self->type('dateTime', $bug->$name);
-        }
-        elsif ($field->type == FIELD_TYPE_MULTI_SELECT) {
-            my @values = map { $self->type('string', $_) } @{ $bug->$name };
-            $item{$name} = \@values;
-        }
-        else {
-            $item{$name} = $self->type('string', $bug->$name);
-        }
+    else {
+      $item{$name} = $self->type('string', $bug->$name);
     }
+  }
 
-    # Timetracking fields are only sent if the user can see them.
-    if (Bugzilla->user->is_timetracker) {
-        if (filter_wants $params, 'estimated_time') {
-            $item{'estimated_time'} = $self->type('double', $bug->estimated_time);
-        }
-        if (filter_wants $params, 'remaining_time') {
-            $item{'remaining_time'} = $self->type('double', $bug->remaining_time);
-        }
-        if (filter_wants $params, 'actual_time') {
-            $item{'actual_time'} = $self->type('double', $bug->actual_time);
-        }
+  # Timetracking fields are only sent if the user can see them.
+  if (Bugzilla->user->is_timetracker) {
+    if (filter_wants $params, 'estimated_time') {
+      $item{'estimated_time'} = $self->type('double', $bug->estimated_time);
     }
-
-    # The "accessible" bits go here because they have long names and it
-    # makes the code look nicer to separate them out.
-    if (filter_wants $params, 'is_cc_accessible') {
-        $item{'is_cc_accessible'} = $self->type('boolean', $bug->cclist_accessible);
+    if (filter_wants $params, 'remaining_time') {
+      $item{'remaining_time'} = $self->type('double', $bug->remaining_time);
     }
-    if (filter_wants $params, 'is_creator_accessible') {
-        $item{'is_creator_accessible'} = $self->type('boolean', $bug->reporter_accessible);
+    if (filter_wants $params, 'actual_time') {
+      $item{'actual_time'} = $self->type('double', $bug->actual_time);
     }
-
-    return \%item;
+  }
+
+  # The "accessible" bits go here because they have long names and it
+  # makes the code look nicer to separate them out.
+  if (filter_wants $params, 'is_cc_accessible') {
+    $item{'is_cc_accessible'} = $self->type('boolean', $bug->cclist_accessible);
+  }
+  if (filter_wants $params, 'is_creator_accessible') {
+    $item{'is_creator_accessible'}
+      = $self->type('boolean', $bug->reporter_accessible);
+  }
+
+  return \%item;
 }
 
 sub _user_to_hash {
-    my ($self, $user, $filters, $types, $prefix) = @_;
-    my $item = filter $filters, {
-        id        => $self->type('int', $user->id),
-        real_name => $self->type('string', $user->name),
-        name      => $self->type('email', $user->login),
-        email     => $self->type('email', $user->email),
-    }, $types, $prefix;
-    return $item;
+  my ($self, $user, $filters, $types, $prefix) = @_;
+  my $item = filter $filters,
+    {
+    id        => $self->type('int',    $user->id),
+    real_name => $self->type('string', $user->name),
+    name      => $self->type('email',  $user->login),
+    email     => $self->type('email',  $user->email),
+    },
+    $types, $prefix;
+  return $item;
 }
 
 sub _attachment_to_hash {
-    my ($self, $attach, $filters, $types, $prefix) = @_;
-
-    my $item = filter $filters, {
-        creation_time    => $self->type('dateTime', $attach->attached),
-        last_change_time => $self->type('dateTime', $attach->modification_time),
-        id               => $self->type('int', $attach->id),
-        bug_id           => $self->type('int', $attach->bug_id),
-        file_name        => $self->type('string', $attach->filename),
-        summary          => $self->type('string', $attach->description),
-        content_type     => $self->type('string', $attach->contenttype),
-        is_private       => $self->type('int', $attach->isprivate),
-        is_obsolete      => $self->type('int', $attach->isobsolete),
-        is_patch         => $self->type('int', $attach->ispatch),
-    }, $types, $prefix;
-
-    # creator requires an extra lookup, so we only send them if
-    # the filter wants them.
-    if (filter_wants $filters, 'creator', $types, $prefix) {
-        $item->{'creator'} = $self->type('email', $attach->attacher->login);
-    }
-
-    if (filter_wants $filters, 'data', $types, $prefix) {
-        $item->{'data'} = $self->type('base64', $attach->data);
-    }
-
-    if (filter_wants $filters, 'size', $types, $prefix) {
-        $item->{'size'} = $self->type('int', $attach->datasize);
-    }
+  my ($self, $attach, $filters, $types, $prefix) = @_;
 
-    if (filter_wants $filters, 'flags', $types, $prefix) {
-        $item->{'flags'} = [ map { $self->_flag_to_hash($_) } @{$attach->flags} ];
-    }
-
-    return $item;
+  my $item = filter $filters,
+    {
+    creation_time    => $self->type('dateTime', $attach->attached),
+    last_change_time => $self->type('dateTime', $attach->modification_time),
+    id               => $self->type('int',      $attach->id),
+    bug_id           => $self->type('int',      $attach->bug_id),
+    file_name        => $self->type('string',   $attach->filename),
+    summary          => $self->type('string',   $attach->description),
+    content_type     => $self->type('string',   $attach->contenttype),
+    is_private       => $self->type('int',      $attach->isprivate),
+    is_obsolete      => $self->type('int',      $attach->isobsolete),
+    is_patch         => $self->type('int',      $attach->ispatch),
+    },
+    $types, $prefix;
+
+  # creator requires an extra lookup, so we only send them if
+  # the filter wants them.
+  if (filter_wants $filters, 'creator', $types, $prefix) {
+    $item->{'creator'} = $self->type('email', $attach->attacher->login);
+  }
+
+  if (filter_wants $filters, 'data', $types, $prefix) {
+    $item->{'data'} = $self->type('base64', $attach->data);
+  }
+
+  if (filter_wants $filters, 'size', $types, $prefix) {
+    $item->{'size'} = $self->type('int', $attach->datasize);
+  }
+
+  if (filter_wants $filters, 'flags', $types, $prefix) {
+    $item->{'flags'} = [map { $self->_flag_to_hash($_) } @{$attach->flags}];
+  }
+
+  return $item;
 }
 
 sub _flag_to_hash {
-    my ($self, $flag) = @_;
-
-    my $item = {
-        id                => $self->type('int', $flag->id),
-        name              => $self->type('string', $flag->name),
-        type_id           => $self->type('int', $flag->type_id),
-        creation_date     => $self->type('dateTime', $flag->creation_date), 
-        modification_date => $self->type('dateTime', $flag->modification_date), 
-        status            => $self->type('string', $flag->status)
-    };
-
-    foreach my $field (qw(setter requestee)) {
-        my $field_id = $field . "_id";
-        $item->{$field} = $self->type('email', $flag->$field->login)
-            if $flag->$field_id;
-    }
-
-    return $item;
+  my ($self, $flag) = @_;
+
+  my $item = {
+    id                => $self->type('int',      $flag->id),
+    name              => $self->type('string',   $flag->name),
+    type_id           => $self->type('int',      $flag->type_id),
+    creation_date     => $self->type('dateTime', $flag->creation_date),
+    modification_date => $self->type('dateTime', $flag->modification_date),
+    status            => $self->type('string',   $flag->status)
+  };
+
+  foreach my $field (qw(setter requestee)) {
+    my $field_id = $field . "_id";
+    $item->{$field} = $self->type('email', $flag->$field->login)
+      if $flag->$field_id;
+  }
+
+  return $item;
 }
 
 sub _add_update_tokens {
-    my ($self, $params, $bugs, $hashes) = @_;
+  my ($self, $params, $bugs, $hashes) = @_;
 
-    return if !Bugzilla->user->id;
-    return if !filter_wants($params, 'update_token');
+  return if !Bugzilla->user->id;
+  return if !filter_wants($params, 'update_token');
 
-    for(my $i = 0; $i < @$bugs; $i++) {
-        my $token = issue_hash_token([$bugs->[$i]->id, $bugs->[$i]->delta_ts]);
-        $hashes->[$i]->{'update_token'} = $self->type('string', $token);
-    }
+  for (my $i = 0; $i < @$bugs; $i++) {
+    my $token = issue_hash_token([$bugs->[$i]->id, $bugs->[$i]->delta_ts]);
+    $hashes->[$i]->{'update_token'} = $self->type('string', $token);
+  }
 }
 
 1;
diff --git a/Bugzilla/WebService/BugUserLastVisit.pm b/Bugzilla/WebService/BugUserLastVisit.pm
index 56e91ec31..128507376 100644
--- a/Bugzilla/WebService/BugUserLastVisit.pm
+++ b/Bugzilla/WebService/BugUserLastVisit.pm
@@ -19,80 +19,83 @@ use Bugzilla::WebService::Util qw( validate filter );
 use Bugzilla::Constants;
 
 use constant PUBLIC_METHODS => qw(
-    get
-    update
+  get
+  update
 );
 
 sub update {
-    my ($self, $params) = validate(@_, 'ids');
-    my $user = Bugzilla->user;
-    my $dbh  = Bugzilla->dbh;
+  my ($self, $params) = validate(@_, 'ids');
+  my $user = Bugzilla->user;
+  my $dbh  = Bugzilla->dbh;
 
-    $user->login(LOGIN_REQUIRED);
+  $user->login(LOGIN_REQUIRED);
 
-    my $ids = $params->{ids} // [];
-    ThrowCodeError('param_required', { param => 'ids' }) unless @$ids;
+  my $ids = $params->{ids} // [];
+  ThrowCodeError('param_required', {param => 'ids'}) unless @$ids;
 
-    # Cache permissions for bugs. This highly reduces the number of calls to the
-    # DB.  visible_bugs() is only able to handle bug IDs, so we have to skip
-    # aliases.
-    $user->visible_bugs([grep /^[0-9]+$/, @$ids]);
+  # Cache permissions for bugs. This highly reduces the number of calls to the
+  # DB.  visible_bugs() is only able to handle bug IDs, so we have to skip
+  # aliases.
+  $user->visible_bugs([grep /^[0-9]+$/, @$ids]);
 
-    $dbh->bz_start_transaction();
-    my @results;
-    my $last_visit_ts = $dbh->selectrow_array('SELECT NOW()');
-    foreach my $bug_id (@$ids) {
-        my $bug = Bugzilla::Bug->check({ id => $bug_id, cache => 1 });
+  $dbh->bz_start_transaction();
+  my @results;
+  my $last_visit_ts = $dbh->selectrow_array('SELECT NOW()');
+  foreach my $bug_id (@$ids) {
+    my $bug = Bugzilla::Bug->check({id => $bug_id, cache => 1});
 
-        ThrowUserError('user_not_involved', { bug_id => $bug->id })
-            unless $user->is_involved_in_bug($bug);
+    ThrowUserError('user_not_involved', {bug_id => $bug->id})
+      unless $user->is_involved_in_bug($bug);
 
-        $bug->update_user_last_visit($user, $last_visit_ts);
+    $bug->update_user_last_visit($user, $last_visit_ts);
 
-        push(
-            @results,
-            $self->_bug_user_last_visit_to_hash(
-                $bug->id, $last_visit_ts, $params
-            ));
-    }
-    $dbh->bz_commit_transaction();
+    push(@results,
+      $self->_bug_user_last_visit_to_hash($bug->id, $last_visit_ts, $params));
+  }
+  $dbh->bz_commit_transaction();
 
-    return \@results;
+  return \@results;
 }
 
 sub get {
-    my ($self, $params) = validate(@_, 'ids');
-    my $user = Bugzilla->user;
-    my $ids  = $params->{ids};
-
-    $user->login(LOGIN_REQUIRED);
-
-    my @last_visits;
-    if ($ids) {
-        # Cache permissions for bugs. This highly reduces the number of calls to
-        # the DB.  visible_bugs() is only able to handle bug IDs, so we have to
-        # skip aliases.
-        $user->visible_bugs([grep /^[0-9]+$/, @$ids]);
-
-        my %last_visit  = map { $_->bug_id => $_->last_visit_ts } @{ $user->last_visited($ids) };
-        @last_visits = map { $self->_bug_user_last_visit_to_hash($_->id, $last_visit{$_}, $params) } @$ids;
-    }
-    else {
-        @last_visits = map {
-            $self->_bug_user_last_visit_to_hash($_->bug_id, $_->last_visit_ts, $params)
-        } @{ $user->last_visited };
-    }
-
-    return \@last_visits;
+  my ($self, $params) = validate(@_, 'ids');
+  my $user = Bugzilla->user;
+  my $ids  = $params->{ids};
+
+  $user->login(LOGIN_REQUIRED);
+
+  my @last_visits;
+  if ($ids) {
+
+    # Cache permissions for bugs. This highly reduces the number of calls to
+    # the DB.  visible_bugs() is only able to handle bug IDs, so we have to
+    # skip aliases.
+    $user->visible_bugs([grep /^[0-9]+$/, @$ids]);
+
+    my %last_visit
+      = map { $_->bug_id => $_->last_visit_ts } @{$user->last_visited($ids)};
+    @last_visits
+      = map { $self->_bug_user_last_visit_to_hash($_->id, $last_visit{$_}, $params) }
+      @$ids;
+  }
+  else {
+    @last_visits = map {
+      $self->_bug_user_last_visit_to_hash($_->bug_id, $_->last_visit_ts, $params)
+    } @{$user->last_visited};
+  }
+
+  return \@last_visits;
 }
 
 sub _bug_user_last_visit_to_hash {
-    my ($self, $bug_id, $last_visit_ts, $params) = @_;
+  my ($self, $bug_id, $last_visit_ts, $params) = @_;
 
-    my %result = (id            => $self->type('int',      $bug_id),
-                  last_visit_ts => $self->type('dateTime', $last_visit_ts));
+  my %result = (
+    id            => $self->type('int',      $bug_id),
+    last_visit_ts => $self->type('dateTime', $last_visit_ts)
+  );
 
-    return filter($params, \%result);
+  return filter($params, \%result);
 }
 
 1;
diff --git a/Bugzilla/WebService/Bugzilla.pm b/Bugzilla/WebService/Bugzilla.pm
index 848cffd30..6d9563d61 100644
--- a/Bugzilla/WebService/Bugzilla.pm
+++ b/Bugzilla/WebService/Bugzilla.pm
@@ -20,158 +20,155 @@ use Bugzilla::Util qw(trick_taint);
 use DateTime;
 
 # Basic info that is needed before logins
-use constant LOGIN_EXEMPT => {
-    parameters => 1,
-    timezone => 1,
-    version => 1,
-};
+use constant LOGIN_EXEMPT => {parameters => 1, timezone => 1, version => 1,};
 
 use constant READ_ONLY => qw(
-    extensions
-    parameters
-    timezone
-    time
-    version
+  extensions
+  parameters
+  timezone
+  time
+  version
 );
 
 use constant PUBLIC_METHODS => qw(
-    extensions
-    last_audit_time
-    parameters
-    time
-    timezone
-    version
+  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
-    requirelogin
+  maintainer
+  requirelogin
 );
 
 # These parameters are guessable from the web UI when the user
 # is logged in. So it's safe to access them.
 use constant PARAMETERS_LOGGED_IN => qw(
-    allowemailchange
-    attachment_base
-    commentonchange_resolution
-    commentonduplicate
-    cookiepath
-    defaultopsys
-    defaultplatform
-    defaultpriority
-    defaultseverity
-    duplicate_or_move_bug_status
-    emailregexpdesc
-    emailsuffix
-    letsubmitterchoosemilestone
-    letsubmitterchoosepriority
-    mailfrom
-    maintainer
-    maxattachmentsize
-    maxlocalattachment
-    musthavemilestoneonaccept
-    noresolveonopenblockers
-    password_complexity
-    rememberlogin
-    requirelogin
-    search_allow_no_criteria
-    urlbase
-    use_see_also
-    useclassification
-    usemenuforusers
-    useqacontact
-    usestatuswhiteboard
-    usetargetmilestone
+  allowemailchange
+  attachment_base
+  commentonchange_resolution
+  commentonduplicate
+  cookiepath
+  defaultopsys
+  defaultplatform
+  defaultpriority
+  defaultseverity
+  duplicate_or_move_bug_status
+  emailregexpdesc
+  emailsuffix
+  letsubmitterchoosemilestone
+  letsubmitterchoosepriority
+  mailfrom
+  maintainer
+  maxattachmentsize
+  maxlocalattachment
+  musthavemilestoneonaccept
+  noresolveonopenblockers
+  password_complexity
+  rememberlogin
+  requirelogin
+  search_allow_no_criteria
+  urlbase
+  use_see_also
+  useclassification
+  usemenuforusers
+  useqacontact
+  usestatuswhiteboard
+  usetargetmilestone
 );
 
 sub version {
-    my $self = shift;
-    return { version => $self->type('string', BUGZILLA_VERSION) };
+  my $self = shift;
+  return {version => $self->type('string', BUGZILLA_VERSION)};
 }
 
 sub extensions {
-    my $self = shift;
-
-    my %retval;
-    foreach my $extension (@{ Bugzilla->extensions }) {
-        my $version = $extension->VERSION || 0;
-        my $name    = $extension->NAME;
-        $retval{$name}->{version} = $self->type('string', $version);
-    }
-    return { extensions => \%retval };
+  my $self = shift;
+
+  my %retval;
+  foreach my $extension (@{Bugzilla->extensions}) {
+    my $version = $extension->VERSION || 0;
+    my $name = $extension->NAME;
+    $retval{$name}->{version} = $self->type('string', $version);
+  }
+  return {extensions => \%retval};
 }
 
 sub timezone {
-    my $self = shift;
-    # All Webservices return times in UTC; Use UTC here for backwards compat.
-    return { timezone => $self->type('string', "+0000") };
+  my $self = shift;
+
+  # All Webservices return times in UTC; Use UTC here for backwards compat.
+  return {timezone => $self->type('string', "+0000")};
 }
 
 sub time {
-    my ($self) = @_;
-    # All Webservices return times in UTC; Use UTC here for backwards compat.
-    # Hardcode values where appropriate
-    my $dbh = Bugzilla->dbh;
-
-    my $db_time = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
-    $db_time = datetime_from($db_time, 'UTC');
-    my $now_utc = DateTime->now();
-
-    return {
-        db_time       => $self->type('dateTime', $db_time),
-        web_time      => $self->type('dateTime', $now_utc),
-        web_time_utc  => $self->type('dateTime', $now_utc),
-        tz_name       => $self->type('string', 'UTC'),
-        tz_offset     => $self->type('string', '+0000'),
-        tz_short_name => $self->type('string', 'UTC'),
-    };
+  my ($self) = @_;
+
+  # All Webservices return times in UTC; Use UTC here for backwards compat.
+  # Hardcode values where appropriate
+  my $dbh = Bugzilla->dbh;
+
+  my $db_time = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
+  $db_time = datetime_from($db_time, 'UTC');
+  my $now_utc = DateTime->now();
+
+  return {
+    db_time       => $self->type('dateTime', $db_time),
+    web_time      => $self->type('dateTime', $now_utc),
+    web_time_utc  => $self->type('dateTime', $now_utc),
+    tz_name       => $self->type('string',   'UTC'),
+    tz_offset     => $self->type('string',   '+0000'),
+    tz_short_name => $self->type('string',   'UTC'),
+  };
 }
 
 sub last_audit_time {
-    my ($self, $params) = validate(@_, 'class');
-    my $dbh = Bugzilla->dbh;
-
-    my $sql_statement = "SELECT MAX(at_time) FROM audit_log";
-    my $class_values =  $params->{class};
-    my @class_values_quoted;
-    foreach my $class_value (@$class_values) {
-        push (@class_values_quoted, $dbh->quote($class_value))
-            if $class_value =~ /^Bugzilla(::[a-zA-Z0-9_]+)*$/;
-    }
-
-    if (@class_values_quoted) {
-        $sql_statement .= " WHERE " . $dbh->sql_in('class', \@class_values_quoted);
-    }
-
-    my $last_audit_time = $dbh->selectrow_array("$sql_statement");
-
-    # All Webservices return times in UTC; Use UTC here for backwards compat.
-    # Hardcode values where appropriate
-    $last_audit_time = datetime_from($last_audit_time, 'UTC');
-
-    return {
-        last_audit_time => $self->type('dateTime', $last_audit_time)
-    };
+  my ($self, $params) = validate(@_, 'class');
+  my $dbh = Bugzilla->dbh;
+
+  my $sql_statement = "SELECT MAX(at_time) FROM audit_log";
+  my $class_values  = $params->{class};
+  my @class_values_quoted;
+  foreach my $class_value (@$class_values) {
+    push(@class_values_quoted, $dbh->quote($class_value))
+      if $class_value =~ /^Bugzilla(::[a-zA-Z0-9_]+)*$/;
+  }
+
+  if (@class_values_quoted) {
+    $sql_statement .= " WHERE " . $dbh->sql_in('class', \@class_values_quoted);
+  }
+
+  my $last_audit_time = $dbh->selectrow_array("$sql_statement");
+
+  # All Webservices return times in UTC; Use UTC here for backwards compat.
+  # Hardcode values where appropriate
+  $last_audit_time = datetime_from($last_audit_time, 'UTC');
+
+  return {last_audit_time => $self->type('dateTime', $last_audit_time)};
 }
 
 sub parameters {
-    my ($self, $args) = @_;
-    my $user = Bugzilla->login(LOGIN_OPTIONAL);
-    my $params = Bugzilla->params;
-    $args ||= {};
-
-    my @params_list = $user->in_group('tweakparams')
-                      ? keys(%$params)
-                      : $user->id ? PARAMETERS_LOGGED_IN : PARAMETERS_LOGGED_OUT;
-
-    my %parameters;
-    foreach my $param (@params_list) {
-        next unless filter_wants($args, $param);
-        $parameters{$param} = $self->type('string', $params->{$param});
-    }
-
-    return { parameters => \%parameters };
+  my ($self, $args) = @_;
+  my $user   = Bugzilla->login(LOGIN_OPTIONAL);
+  my $params = Bugzilla->params;
+  $args ||= {};
+
+  my @params_list
+    = $user->in_group('tweakparams') ? keys(%$params)
+    : $user->id                      ? PARAMETERS_LOGGED_IN
+    :                                  PARAMETERS_LOGGED_OUT;
+
+  my %parameters;
+  foreach my $param (@params_list) {
+    next unless filter_wants($args, $param);
+    $parameters{$param} = $self->type('string', $params->{$param});
+  }
+
+  return {parameters => \%parameters};
 }
 
 1;
diff --git a/Bugzilla/WebService/Classification.pm b/Bugzilla/WebService/Classification.pm
index cee597b68..ab539b339 100644
--- a/Bugzilla/WebService/Classification.pm
+++ b/Bugzilla/WebService/Classification.pm
@@ -18,65 +18,76 @@ use Bugzilla::Error;
 use Bugzilla::WebService::Util qw(filter validate params_to_objects);
 
 use constant READ_ONLY => qw(
-    get
+  get
 );
 
 use constant PUBLIC_METHODS => qw(
-    get
+  get
 );
 
 sub get {
-    my ($self, $params) = validate(@_, 'names', 'ids');
+  my ($self, $params) = validate(@_, 'names', 'ids');
 
-    defined $params->{names} || defined $params->{ids}
-        || ThrowCodeError('params_required', { function => 'Classification.get',
-                                               params => ['names', 'ids'] });
+  defined $params->{names}
+    || defined $params->{ids}
+    || ThrowCodeError('params_required',
+    {function => 'Classification.get', params => ['names', 'ids']});
 
-    my $user = Bugzilla->user;
+  my $user = Bugzilla->user;
 
-    Bugzilla->params->{'useclassification'}
-      || $user->in_group('editclassifications')
-      || ThrowUserError('auth_classification_not_enabled');
+  Bugzilla->params->{'useclassification'}
+    || $user->in_group('editclassifications')
+    || ThrowUserError('auth_classification_not_enabled');
 
-    Bugzilla->switch_to_shadow_db;
+  Bugzilla->switch_to_shadow_db;
 
-    my @classification_objs = @{ params_to_objects($params, 'Bugzilla::Classification') };
-    unless ($user->in_group('editclassifications')) {
-        my %selectable_class = map { $_->id => 1 } @{$user->get_selectable_classifications};
-        @classification_objs = grep { $selectable_class{$_->id} } @classification_objs;
-    }
+  my @classification_objs
+    = @{params_to_objects($params, 'Bugzilla::Classification')};
+  unless ($user->in_group('editclassifications')) {
+    my %selectable_class
+      = map { $_->id => 1 } @{$user->get_selectable_classifications};
+    @classification_objs = grep { $selectable_class{$_->id} } @classification_objs;
+  }
 
-    my @classifications = map { $self->_classification_to_hash($_, $params) } @classification_objs;
+  my @classifications
+    = map { $self->_classification_to_hash($_, $params) } @classification_objs;
 
-    return { classifications => \@classifications };
+  return {classifications => \@classifications};
 }
 
 sub _classification_to_hash {
-    my ($self, $classification, $params) = @_;
-
-    my $user = Bugzilla->user;
-    return unless (Bugzilla->params->{'useclassification'} || $user->in_group('editclassifications'));
-
-    my $products = $user->in_group('editclassifications') ?
-                     $classification->products : $user->get_selectable_products($classification->id);
-
-    return filter $params, {
-        id          => $self->type('int',    $classification->id),
-        name        => $self->type('string', $classification->name),
-        description => $self->type('string', $classification->description),
-        sort_key    => $self->type('int',    $classification->sortkey),
-        products    => [ map { $self->_product_to_hash($_, $params) } @$products ],
+  my ($self, $classification, $params) = @_;
+
+  my $user = Bugzilla->user;
+  return
+    unless (Bugzilla->params->{'useclassification'}
+    || $user->in_group('editclassifications'));
+
+  my $products
+    = $user->in_group('editclassifications')
+    ? $classification->products
+    : $user->get_selectable_products($classification->id);
+
+  return filter $params,
+    {
+    id          => $self->type('int',    $classification->id),
+    name        => $self->type('string', $classification->name),
+    description => $self->type('string', $classification->description),
+    sort_key    => $self->type('int',    $classification->sortkey),
+    products => [map { $self->_product_to_hash($_, $params) } @$products],
     };
 }
 
 sub _product_to_hash {
-    my ($self, $product, $params) = @_;
-
-    return filter $params, {
-       id          => $self->type('int', $product->id),
-       name        => $self->type('string', $product->name),
-       description => $self->type('string', $product->description),
-   }, undef, 'products';
+  my ($self, $product, $params) = @_;
+
+  return filter $params,
+    {
+    id          => $self->type('int',    $product->id),
+    name        => $self->type('string', $product->name),
+    description => $self->type('string', $product->description),
+    },
+    undef, 'products';
 }
 
 1;
diff --git a/Bugzilla/WebService/Component.pm b/Bugzilla/WebService/Component.pm
index 4d6723d8b..802f40c73 100644
--- a/Bugzilla/WebService/Component.pm
+++ b/Bugzilla/WebService/Component.pm
@@ -19,37 +19,36 @@ use Bugzilla::Error;
 use Bugzilla::WebService::Constants;
 use Bugzilla::WebService::Util qw(translate params_to_objects validate);
 
-use constant PUBLIC_METHODS => qw(                                                                                                                                                                                                            
-    create
+use constant PUBLIC_METHODS => qw(
+  create
 );
 
 use constant MAPPED_FIELDS => {
-    default_assignee   => 'initialowner',
-    default_qa_contact => 'initialqacontact',
-    default_cc         => 'initial_cc',
-    is_open            => 'isactive',
+  default_assignee   => 'initialowner',
+  default_qa_contact => 'initialqacontact',
+  default_cc         => 'initial_cc',
+  is_open            => 'isactive',
 };
 
 sub create {
-    my ($self, $params) = @_;
+  my ($self, $params) = @_;
 
-    my $user = Bugzilla->login(LOGIN_REQUIRED);
+  my $user = Bugzilla->login(LOGIN_REQUIRED);
 
-    $user->in_group('editcomponents')
-        || scalar @{ $user->get_products_by_permission('editcomponents') }
-        || ThrowUserError('auth_failure', { group  => 'editcomponents',
-                                            action => 'edit',
-                                            object => 'components' });
+  $user->in_group('editcomponents')
+    || scalar @{$user->get_products_by_permission('editcomponents')}
+    || ThrowUserError('auth_failure',
+    {group => 'editcomponents', action => 'edit', object => 'components'});
 
-    my $product = $user->check_can_admin_product($params->{product});
+  my $product = $user->check_can_admin_product($params->{product});
 
-    # Translate the fields
-    my $values = translate($params, MAPPED_FIELDS);
-    $values->{product} = $product;
+  # Translate the fields
+  my $values = translate($params, MAPPED_FIELDS);
+  $values->{product} = $product;
 
-    # Create the component and return the newly created id.
-    my $component = Bugzilla::Component->create($values);
-    return { id => $self->type('int', $component->id) };
+  # Create the component and return the newly created id.
+  my $component = Bugzilla::Component->create($values);
+  return {id => $self->type('int', $component->id)};
 }
 
 1;
diff --git a/Bugzilla/WebService/Constants.pm b/Bugzilla/WebService/Constants.pm
index 557a996f8..a2f83f528 100644
--- a/Bugzilla/WebService/Constants.pm
+++ b/Bugzilla/WebService/Constants.pm
@@ -14,25 +14,25 @@ use warnings;
 use parent qw(Exporter);
 
 our @EXPORT = qw(
-    WS_ERROR_CODE
+  WS_ERROR_CODE
 
-    STATUS_OK
-    STATUS_CREATED
-    STATUS_ACCEPTED
-    STATUS_NO_CONTENT
-    STATUS_MULTIPLE_CHOICES
-    STATUS_BAD_REQUEST
-    STATUS_NOT_FOUND
-    STATUS_GONE
-    REST_STATUS_CODE_MAP
+  STATUS_OK
+  STATUS_CREATED
+  STATUS_ACCEPTED
+  STATUS_NO_CONTENT
+  STATUS_MULTIPLE_CHOICES
+  STATUS_BAD_REQUEST
+  STATUS_NOT_FOUND
+  STATUS_GONE
+  REST_STATUS_CODE_MAP
 
-    ERROR_UNKNOWN_FATAL
-    ERROR_UNKNOWN_TRANSIENT
+  ERROR_UNKNOWN_FATAL
+  ERROR_UNKNOWN_TRANSIENT
 
-    XMLRPC_CONTENT_TYPE_WHITELIST
-    REST_CONTENT_TYPE_WHITELIST
+  XMLRPC_CONTENT_TYPE_WHITELIST
+  REST_CONTENT_TYPE_WHITELIST
 
-    WS_DISPATCH
+  WS_DISPATCH
 );
 
 # This maps the error names in global/*-error.html.tmpl to numbers.
@@ -54,173 +54,196 @@ our @EXPORT = qw(
 # comment that it was retired. Also, if an error changes its name, you'll
 # have to fix it here.
 use constant WS_ERROR_CODE => {
-    # Generic errors (Bugzilla::Object and others) are 50-99.    
-    object_not_specified        => 50,
-    reassign_to_empty           => 50,
-    param_required              => 50,
-    params_required             => 50,
-    undefined_field             => 50,
-    object_does_not_exist       => 51,
-    param_must_be_numeric       => 52,
-    number_not_numeric          => 52,
-    param_invalid               => 53,
-    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,
-    bug_access_denied           => 102,
-    bug_access_query            => 102,
-    # These all mean "invalid alias"
-    alias_too_long           => 103,
-    alias_in_use             => 103,
-    alias_is_numeric         => 103,
-    alias_has_comma_or_space => 103,
-    multiple_alias_not_allowed => 103,
-    # Misc. bug field errors
-    illegal_field => 104,
-    freetext_too_long => 104,
-    # Component errors
-    require_component         => 105,
-    component_name_too_long   => 105,
-    product_unknown_component => 105,
-    # Invalid Product
-    no_products         => 106,
-    entry_access_denied => 106,
-    product_access_denied => 106,
-    product_disabled    => 106,
-    # Invalid Summary
-    require_summary => 107,
-    # Invalid field name
-    invalid_field_name => 108,
-    # Not authorized to edit the bug
-    product_edit_denied => 109,
-    # Comment-related errors
-    comment_is_private => 110,
-    comment_id_invalid => 111,
-    comment_too_long => 114,
-    comment_invalid_isprivate => 117,
-    # Comment tagging
-    comment_tag_disabled => 125,
-    comment_tag_invalid => 126,
-    comment_tag_too_long => 127,
-    comment_tag_too_short => 128,
-    # See Also errors
-    bug_url_invalid => 112,
-    bug_url_too_long => 112,
-    # Insidergroup Errors
-    user_not_insider => 113,
-    # Note: 114 is above in the Comment-related section.
-    # Bug update errors
-    illegal_change => 115,
-    # Dependency errors
-    dependency_loop_single => 116,
-    dependency_loop_multi  => 116,
-    # Note: 117 is above in the Comment-related section.
-    # Dup errors
-    dupe_loop_detected => 118,
-    dupe_id_required => 119,
-    # Bug-related group errors
-    group_invalid_removal => 120,
-    group_restriction_not_allowed => 120,
-    # Status/Resolution errors
-    missing_resolution => 121,
-    resolution_not_allowed => 122,
-    illegal_bug_status_transition => 123,
-    # Flag errors
-    flag_status_invalid => 129,
-    flag_update_denied => 130,
-    flag_type_requestee_disabled => 131,
-    flag_not_unique => 132,
-    flag_type_not_unique => 133,
-    flag_type_inactive => 134,
-
-    # Authentication errors are usually 300-400.
-    invalid_login_or_password => 300,
-    account_disabled             => 301,
-    auth_invalid_email           => 302,
-    extern_id_conflict           => -303,
-    auth_failure                 => 304,
-    password_too_short           => 305,
-    password_not_complex         => 305,
-    api_key_not_valid            => 306,
-    api_key_revoked              => 306,
-    auth_invalid_token           => 307,
-
-    # Except, historically, AUTH_NODATA, which is 410.
-    login_required               => 410,
-
-    # User errors are 500-600.
-    account_exists        => 500,
-    illegal_email_address => 501,
-    auth_cant_create_account    => 501,
-    account_creation_disabled   => 501,
-    account_creation_restricted => 501,
-    password_too_short    => 502,
-    # Error 503 password_too_long no longer exists.
-    invalid_username      => 504,
-    # This is from strict_isolation, but it also basically means 
-    # "invalid user."
-    invalid_user_group    => 504,
-    user_access_by_id_denied    => 505,
-    user_access_by_match_denied => 505,
-
-    # Attachment errors are 600-700.
-    file_too_large         => 600,
-    invalid_content_type   => 601,
-    # Error 602 attachment_illegal_url no longer exists.
-    file_not_specified     => 603,
-    missing_attachment_description => 604,
-    # Error 605 attachment_url_disabled no longer exists.
-    zero_length_file       => 606,
-
-    # Product erros are 700-800
-    product_blank_name => 700,
-    product_name_too_long => 701,
-    product_name_already_in_use => 702,
-    product_name_diff_in_case => 702,
-    product_must_have_description => 703,
-    product_must_have_version => 704,
-    product_must_define_defaultmilestone => 705,
-
-    # Group errors are 800-900
-    empty_group_name => 800,
-    group_exists => 801,
-    empty_group_description => 802,
-    invalid_regexp => 803,
-    invalid_group_name => 804,
-    group_cannot_view => 805,
-
-    # Classification errors are 900-1000
-    auth_classification_not_enabled => 900,
-
-    # Search errors are 1000-1100
-    buglist_parameters_required => 1000,
-
-    # Flag type errors are 1100-1200
-    flag_type_name_invalid        => 1101,
-    flag_type_description_invalid => 1102,
-    flag_type_cc_list_invalid     => 1103,
-    flag_type_sortkey_invalid     => 1104,
-    flag_type_not_editable        => 1105,
-
-    # Component errors are 1200-1300
-    component_already_exists => 1200,
-    component_is_last        => 1201,
-    component_has_bugs       => 1202,
-
-    # Errors thrown by the WebService itself. The ones that are negative 
-    # conform to http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php
-    xmlrpc_invalid_value => -32600,
-    unknown_method       => -32601,
-    json_rpc_post_only   => 32610,
-    json_rpc_invalid_callback => 32611,
-    xmlrpc_illegal_content_type   => 32612,
-    json_rpc_illegal_content_type => 32613,
-    rest_invalid_resource         => 32614,
+
+  # Generic errors (Bugzilla::Object and others) are 50-99.
+  object_not_specified        => 50,
+  reassign_to_empty           => 50,
+  param_required              => 50,
+  params_required             => 50,
+  undefined_field             => 50,
+  object_does_not_exist       => 51,
+  param_must_be_numeric       => 52,
+  number_not_numeric          => 52,
+  param_invalid               => 53,
+  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,
+  bug_access_denied           => 102,
+  bug_access_query            => 102,
+
+  # These all mean "invalid alias"
+  alias_too_long             => 103,
+  alias_in_use               => 103,
+  alias_is_numeric           => 103,
+  alias_has_comma_or_space   => 103,
+  multiple_alias_not_allowed => 103,
+
+  # Misc. bug field errors
+  illegal_field     => 104,
+  freetext_too_long => 104,
+
+  # Component errors
+  require_component         => 105,
+  component_name_too_long   => 105,
+  product_unknown_component => 105,
+
+  # Invalid Product
+  no_products           => 106,
+  entry_access_denied   => 106,
+  product_access_denied => 106,
+  product_disabled      => 106,
+
+  # Invalid Summary
+  require_summary => 107,
+
+  # Invalid field name
+  invalid_field_name => 108,
+
+  # Not authorized to edit the bug
+  product_edit_denied => 109,
+
+  # Comment-related errors
+  comment_is_private        => 110,
+  comment_id_invalid        => 111,
+  comment_too_long          => 114,
+  comment_invalid_isprivate => 117,
+
+  # Comment tagging
+  comment_tag_disabled  => 125,
+  comment_tag_invalid   => 126,
+  comment_tag_too_long  => 127,
+  comment_tag_too_short => 128,
+
+  # See Also errors
+  bug_url_invalid  => 112,
+  bug_url_too_long => 112,
+
+  # Insidergroup Errors
+  user_not_insider => 113,
+
+  # Note: 114 is above in the Comment-related section.
+  # Bug update errors
+  illegal_change => 115,
+
+  # Dependency errors
+  dependency_loop_single => 116,
+  dependency_loop_multi  => 116,
+
+  # Note: 117 is above in the Comment-related section.
+  # Dup errors
+  dupe_loop_detected => 118,
+  dupe_id_required   => 119,
+
+  # Bug-related group errors
+  group_invalid_removal         => 120,
+  group_restriction_not_allowed => 120,
+
+  # Status/Resolution errors
+  missing_resolution            => 121,
+  resolution_not_allowed        => 122,
+  illegal_bug_status_transition => 123,
+
+  # Flag errors
+  flag_status_invalid          => 129,
+  flag_update_denied           => 130,
+  flag_type_requestee_disabled => 131,
+  flag_not_unique              => 132,
+  flag_type_not_unique         => 133,
+  flag_type_inactive           => 134,
+
+  # Authentication errors are usually 300-400.
+  invalid_login_or_password => 300,
+  account_disabled          => 301,
+  auth_invalid_email        => 302,
+  extern_id_conflict        => -303,
+  auth_failure              => 304,
+  password_too_short        => 305,
+  password_not_complex      => 305,
+  api_key_not_valid         => 306,
+  api_key_revoked           => 306,
+  auth_invalid_token        => 307,
+
+  # Except, historically, AUTH_NODATA, which is 410.
+  login_required => 410,
+
+  # User errors are 500-600.
+  account_exists              => 500,
+  illegal_email_address       => 501,
+  auth_cant_create_account    => 501,
+  account_creation_disabled   => 501,
+  account_creation_restricted => 501,
+  password_too_short          => 502,
+
+  # Error 503 password_too_long no longer exists.
+  invalid_username => 504,
+
+  # This is from strict_isolation, but it also basically means
+  # "invalid user."
+  invalid_user_group          => 504,
+  user_access_by_id_denied    => 505,
+  user_access_by_match_denied => 505,
+
+  # Attachment errors are 600-700.
+  file_too_large       => 600,
+  invalid_content_type => 601,
+
+  # Error 602 attachment_illegal_url no longer exists.
+  file_not_specified             => 603,
+  missing_attachment_description => 604,
+
+  # Error 605 attachment_url_disabled no longer exists.
+  zero_length_file => 606,
+
+  # Product erros are 700-800
+  product_blank_name                   => 700,
+  product_name_too_long                => 701,
+  product_name_already_in_use          => 702,
+  product_name_diff_in_case            => 702,
+  product_must_have_description        => 703,
+  product_must_have_version            => 704,
+  product_must_define_defaultmilestone => 705,
+
+  # Group errors are 800-900
+  empty_group_name        => 800,
+  group_exists            => 801,
+  empty_group_description => 802,
+  invalid_regexp          => 803,
+  invalid_group_name      => 804,
+  group_cannot_view       => 805,
+
+  # Classification errors are 900-1000
+  auth_classification_not_enabled => 900,
+
+  # Search errors are 1000-1100
+  buglist_parameters_required => 1000,
+
+  # Flag type errors are 1100-1200
+  flag_type_name_invalid        => 1101,
+  flag_type_description_invalid => 1102,
+  flag_type_cc_list_invalid     => 1103,
+  flag_type_sortkey_invalid     => 1104,
+  flag_type_not_editable        => 1105,
+
+  # Component errors are 1200-1300
+  component_already_exists => 1200,
+  component_is_last        => 1201,
+  component_has_bugs       => 1202,
+
+  # Errors thrown by the WebService itself. The ones that are negative
+  # conform to http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php
+  xmlrpc_invalid_value          => -32600,
+  unknown_method                => -32601,
+  json_rpc_post_only            => 32610,
+  json_rpc_invalid_callback     => 32611,
+  xmlrpc_illegal_content_type   => 32612,
+  json_rpc_illegal_content_type => 32613,
+  rest_invalid_resource         => 32614,
 };
 
 # RESTful webservices use the http status code
@@ -241,73 +264,74 @@ use constant STATUS_GONE             => 410;
 # http status code based on the error code or use the
 # default STATUS_BAD_REQUEST.
 sub REST_STATUS_CODE_MAP {
-    my $status_code_map = {
-        51       => STATUS_NOT_FOUND,
-        101      => STATUS_NOT_FOUND,
-        102      => STATUS_NOT_AUTHORIZED,
-        106      => STATUS_NOT_AUTHORIZED,
-        109      => STATUS_NOT_AUTHORIZED,
-        110      => STATUS_NOT_AUTHORIZED,
-        113      => STATUS_NOT_AUTHORIZED,
-        115      => STATUS_NOT_AUTHORIZED,
-        120      => STATUS_NOT_AUTHORIZED,
-        300      => STATUS_NOT_AUTHORIZED,
-        301      => STATUS_NOT_AUTHORIZED,
-        302      => STATUS_NOT_AUTHORIZED,
-        303      => STATUS_NOT_AUTHORIZED,
-        304      => STATUS_NOT_AUTHORIZED,
-        410      => STATUS_NOT_AUTHORIZED,
-        504      => STATUS_NOT_AUTHORIZED,
-        505      => STATUS_NOT_AUTHORIZED,
-        32614    => STATUS_NOT_FOUND,
-        _default => STATUS_BAD_REQUEST
-    };
-
-    Bugzilla::Hook::process('webservice_status_code_map',
-        { status_code_map => $status_code_map });
-
-    return $status_code_map;
-};
+  my $status_code_map = {
+    51       => STATUS_NOT_FOUND,
+    101      => STATUS_NOT_FOUND,
+    102      => STATUS_NOT_AUTHORIZED,
+    106      => STATUS_NOT_AUTHORIZED,
+    109      => STATUS_NOT_AUTHORIZED,
+    110      => STATUS_NOT_AUTHORIZED,
+    113      => STATUS_NOT_AUTHORIZED,
+    115      => STATUS_NOT_AUTHORIZED,
+    120      => STATUS_NOT_AUTHORIZED,
+    300      => STATUS_NOT_AUTHORIZED,
+    301      => STATUS_NOT_AUTHORIZED,
+    302      => STATUS_NOT_AUTHORIZED,
+    303      => STATUS_NOT_AUTHORIZED,
+    304      => STATUS_NOT_AUTHORIZED,
+    410      => STATUS_NOT_AUTHORIZED,
+    504      => STATUS_NOT_AUTHORIZED,
+    505      => STATUS_NOT_AUTHORIZED,
+    32614    => STATUS_NOT_FOUND,
+    _default => STATUS_BAD_REQUEST
+  };
+
+  Bugzilla::Hook::process('webservice_status_code_map',
+    {status_code_map => $status_code_map});
+
+  return $status_code_map;
+}
 
 # These are the fallback defaults for errors not in ERROR_CODE.
 use constant ERROR_UNKNOWN_FATAL     => -32000;
 use constant ERROR_UNKNOWN_TRANSIENT => 32000;
 
-use constant ERROR_GENERAL       => 999;
+use constant ERROR_GENERAL => 999;
 
 use constant XMLRPC_CONTENT_TYPE_WHITELIST => qw(
-    text/xml
-    application/xml
+  text/xml
+  application/xml
 );
 
 # The first content type specified is used as the default.
 use constant REST_CONTENT_TYPE_WHITELIST => qw(
-    application/json
-    application/javascript
-    text/javascript
-    text/html
+  application/json
+  application/javascript
+  text/javascript
+  text/html
 );
 
 sub WS_DISPATCH {
-    # We "require" here instead of "use" above to avoid a dependency loop.
-    require Bugzilla::Hook;
-    my %hook_dispatch;
-    Bugzilla::Hook::process('webservice', { dispatch => \%hook_dispatch });
-
-    my $dispatch = {
-        'Bugzilla'         => 'Bugzilla::WebService::Bugzilla',
-        'Bug'              => 'Bugzilla::WebService::Bug',
-        'Classification'   => 'Bugzilla::WebService::Classification',
-        'Component'        => 'Bugzilla::WebService::Component',
-        'FlagType'         => 'Bugzilla::WebService::FlagType',
-        'Group'            => 'Bugzilla::WebService::Group',
-        'Product'          => 'Bugzilla::WebService::Product',
-        'User'             => 'Bugzilla::WebService::User',
-        'BugUserLastVisit' => 'Bugzilla::WebService::BugUserLastVisit',
-        %hook_dispatch
-    };
-    return $dispatch;
-};
+
+  # We "require" here instead of "use" above to avoid a dependency loop.
+  require Bugzilla::Hook;
+  my %hook_dispatch;
+  Bugzilla::Hook::process('webservice', {dispatch => \%hook_dispatch});
+
+  my $dispatch = {
+    'Bugzilla'         => 'Bugzilla::WebService::Bugzilla',
+    'Bug'              => 'Bugzilla::WebService::Bug',
+    'Classification'   => 'Bugzilla::WebService::Classification',
+    'Component'        => 'Bugzilla::WebService::Component',
+    'FlagType'         => 'Bugzilla::WebService::FlagType',
+    'Group'            => 'Bugzilla::WebService::Group',
+    'Product'          => 'Bugzilla::WebService::Product',
+    'User'             => 'Bugzilla::WebService::User',
+    'BugUserLastVisit' => 'Bugzilla::WebService::BugUserLastVisit',
+    %hook_dispatch
+  };
+  return $dispatch;
+}
 
 1;
 
diff --git a/Bugzilla/WebService/FlagType.pm b/Bugzilla/WebService/FlagType.pm
index 9d7cce037..9dc240c7f 100644
--- a/Bugzilla/WebService/FlagType.pm
+++ b/Bugzilla/WebService/FlagType.pm
@@ -22,292 +22,308 @@ use Bugzilla::Util qw(trim);
 use List::MoreUtils qw(uniq);
 
 use constant PUBLIC_METHODS => qw(
-    create
-    get
-    update
+  create
+  get
+  update
 );
 
 sub get {
-    my ($self, $params) = @_;
-    my $dbh  = Bugzilla->switch_to_shadow_db();
-    my $user = Bugzilla->user;
-
-    defined $params->{product}
-        || ThrowCodeError('param_required',
-                          { function => 'Bug.flag_types',
-                            param   => 'product' });
-
-    my $product   = delete $params->{product};
-    my $component = delete $params->{component};
-
-    $product = Bugzilla::Product->check({ name => $product, cache => 1 });
-    $component = Bugzilla::Component->check(
-        { name => $component, product => $product, cache => 1 }) if $component;
-
-    my $flag_params = { product_id => $product->id };
-    $flag_params->{component_id} = $component->id if $component;
-    my $matched_flag_types = Bugzilla::FlagType::match($flag_params);
-
-    my $flag_types = { bug => [], attachment => [] };
-    foreach my $flag_type (@$matched_flag_types) {
-        push(@{ $flag_types->{bug} }, $self->_flagtype_to_hash($flag_type, $product))
-            if $flag_type->target_type eq 'bug';
-        push(@{ $flag_types->{attachment} }, $self->_flagtype_to_hash($flag_type, $product))
-            if $flag_type->target_type eq 'attachment';
-    }
-
-    return $flag_types;
+  my ($self, $params) = @_;
+  my $dbh  = Bugzilla->switch_to_shadow_db();
+  my $user = Bugzilla->user;
+
+  defined $params->{product}
+    || ThrowCodeError('param_required',
+    {function => 'Bug.flag_types', param => 'product'});
+
+  my $product   = delete $params->{product};
+  my $component = delete $params->{component};
+
+  $product = Bugzilla::Product->check({name => $product, cache => 1});
+  $component
+    = Bugzilla::Component->check(
+    {name => $component, product => $product, cache => 1})
+    if $component;
+
+  my $flag_params = {product_id => $product->id};
+  $flag_params->{component_id} = $component->id if $component;
+  my $matched_flag_types = Bugzilla::FlagType::match($flag_params);
+
+  my $flag_types = {bug => [], attachment => []};
+  foreach my $flag_type (@$matched_flag_types) {
+    push(@{$flag_types->{bug}}, $self->_flagtype_to_hash($flag_type, $product))
+      if $flag_type->target_type eq 'bug';
+    push(
+      @{$flag_types->{attachment}},
+      $self->_flagtype_to_hash($flag_type, $product)
+    ) if $flag_type->target_type eq 'attachment';
+  }
+
+  return $flag_types;
 }
 
 sub create {
-    my ($self, $params) = @_;
-    my $user = Bugzilla->login(LOGIN_REQUIRED);
-
-    $user->in_group('editcomponents')
-        || scalar(@{$user->get_products_by_permission('editcomponents')})
-        || ThrowUserError("auth_failure", { group => "editcomponents",
-                                         action => "add",
-                                         object => "flagtypes" });
-
-    $params->{name} || ThrowCodeError('param_required', { param => 'name' });
-    $params->{description} || ThrowCodeError('param_required', { param => 'description' });
-
-    my %args = (
-        sortkey => 1,
-        name => undef,
-        inclusions => ['0:0'],  # Default to __ALL__:__ALL__
-        cc_list => '',
-        description => undef,
-        is_requestable => 'on',
-        exclusions => [],
-        is_multiplicable => 'on',
-        request_group => '',
-        is_active => 'on',
-        is_specifically_requestable => 'on',
-        target_type => 'bug',
-        grant_group => '',
-    );
-
-    foreach my $key (keys %args) {
-        $args{$key} = $params->{$key} if defined($params->{$key});
-    }
-
-    $args{name} = trim($params->{name});
-    $args{description} = trim($params->{description});
-
-    # Is specifically requestable is actually is_requesteeable
-    if (exists $args{is_specifically_requestable}) {
-        $args{is_requesteeble} = delete $args{is_specifically_requestable};
-    }
-
-    # Default is on for the tickbox flags.
-    # If the user has set them to 'off' then undefine them so the flags are not ticked
-    foreach my $arg_name (qw(is_requestable is_multiplicable is_active is_requesteeble)) {
-        if (defined($args{$arg_name}) && ($args{$arg_name} eq '0')) {
-            $args{$arg_name} = undef;
-        }
+  my ($self, $params) = @_;
+  my $user = Bugzilla->login(LOGIN_REQUIRED);
+
+  $user->in_group('editcomponents')
+    || scalar(@{$user->get_products_by_permission('editcomponents')})
+    || ThrowUserError("auth_failure",
+    {group => "editcomponents", action => "add", object => "flagtypes"});
+
+  $params->{name} || ThrowCodeError('param_required', {param => 'name'});
+  $params->{description}
+    || ThrowCodeError('param_required', {param => 'description'});
+
+  my %args = (
+    sortkey                     => 1,
+    name                        => undef,
+    inclusions                  => ['0:0'],    # Default to __ALL__:__ALL__
+    cc_list                     => '',
+    description                 => undef,
+    is_requestable              => 'on',
+    exclusions                  => [],
+    is_multiplicable            => 'on',
+    request_group               => '',
+    is_active                   => 'on',
+    is_specifically_requestable => 'on',
+    target_type                 => 'bug',
+    grant_group                 => '',
+  );
+
+  foreach my $key (keys %args) {
+    $args{$key} = $params->{$key} if defined($params->{$key});
+  }
+
+  $args{name}        = trim($params->{name});
+  $args{description} = trim($params->{description});
+
+  # Is specifically requestable is actually is_requesteeable
+  if (exists $args{is_specifically_requestable}) {
+    $args{is_requesteeble} = delete $args{is_specifically_requestable};
+  }
+
+# Default is on for the tickbox flags.
+# If the user has set them to 'off' then undefine them so the flags are not ticked
+  foreach
+    my $arg_name (qw(is_requestable is_multiplicable is_active is_requesteeble))
+  {
+    if (defined($args{$arg_name}) && ($args{$arg_name} eq '0')) {
+      $args{$arg_name} = undef;
     }
+  }
 
-    # Process group inclusions and exclusions
-    $args{inclusions} = _process_lists($params->{inclusions}) if defined $params->{inclusions};
-    $args{exclusions} = _process_lists($params->{exclusions}) if defined $params->{exclusions};
+  # Process group inclusions and exclusions
+  $args{inclusions} = _process_lists($params->{inclusions})
+    if defined $params->{inclusions};
+  $args{exclusions} = _process_lists($params->{exclusions})
+    if defined $params->{exclusions};
 
-    my $flagtype = Bugzilla::FlagType->create(\%args);
+  my $flagtype = Bugzilla::FlagType->create(\%args);
 
-    return { id => $self->type('int', $flagtype->id)  };
+  return {id => $self->type('int', $flagtype->id)};
 }
 
 sub update {
-    my ($self, $params) = @_;
-    my $dbh = Bugzilla->dbh;
-    my $user = Bugzilla->login(LOGIN_REQUIRED);
-
-    $user->in_group('editcomponents')
-        || scalar(@{$user->get_products_by_permission('editcomponents')})
-        || ThrowUserError("auth_failure", { group  => "editcomponents",
-                                            action => "edit",
-                                            object => "flagtypes" });
-
-    defined($params->{names}) || defined($params->{ids})
-        || ThrowCodeError('params_required',
-               { function => 'FlagType.update', params => ['ids', 'names'] });
-
-    # Get the list of unique flag type ids we are updating
-    my @flag_type_ids = defined($params->{ids}) ? @{$params->{ids}} : ();
-    if (defined $params->{names}) {
-        push @flag_type_ids, map { $_->id }
-            @{ Bugzilla::FlagType::match({ name => $params->{names} }) };
+  my ($self, $params) = @_;
+  my $dbh  = Bugzilla->dbh;
+  my $user = Bugzilla->login(LOGIN_REQUIRED);
+
+  $user->in_group('editcomponents')
+    || scalar(@{$user->get_products_by_permission('editcomponents')})
+    || ThrowUserError("auth_failure",
+    {group => "editcomponents", action => "edit", object => "flagtypes"});
+
+  defined($params->{names})
+    || defined($params->{ids})
+    || ThrowCodeError('params_required',
+    {function => 'FlagType.update', params => ['ids', 'names']});
+
+  # Get the list of unique flag type ids we are updating
+  my @flag_type_ids = defined($params->{ids}) ? @{$params->{ids}} : ();
+  if (defined $params->{names}) {
+    push @flag_type_ids,
+      map { $_->id } @{Bugzilla::FlagType::match({name => $params->{names}})};
+  }
+  @flag_type_ids = uniq @flag_type_ids;
+
+  # We delete names and ids to keep only new values to set.
+  delete $params->{names};
+  delete $params->{ids};
+
+  # Process group inclusions and exclusions
+  # We removed them from $params because these are handled differently
+  my $inclusions = _process_lists(delete $params->{inclusions})
+    if defined $params->{inclusions};
+  my $exclusions = _process_lists(delete $params->{exclusions})
+    if defined $params->{exclusions};
+
+  $dbh->bz_start_transaction();
+  my %changes = ();
+
+  foreach my $flag_type_id (@flag_type_ids) {
+    my ($flagtype, $can_fully_edit)
+      = $user->check_can_admin_flagtype($flag_type_id);
+
+    if ($can_fully_edit) {
+      $flagtype->set_all($params);
+    }
+    elsif (scalar keys %$params) {
+      ThrowUserError('flag_type_not_editable', {flagtype => $flagtype});
     }
-    @flag_type_ids = uniq @flag_type_ids;
-
-    # We delete names and ids to keep only new values to set.
-    delete $params->{names};
-    delete $params->{ids};
-
-    # Process group inclusions and exclusions
-    # We removed them from $params because these are handled differently
-    my $inclusions = _process_lists(delete $params->{inclusions}) if defined $params->{inclusions};
-    my $exclusions = _process_lists(delete $params->{exclusions}) if defined $params->{exclusions};
-
-    $dbh->bz_start_transaction();
-    my %changes = ();
 
-    foreach my $flag_type_id (@flag_type_ids) {
-        my ($flagtype, $can_fully_edit) = $user->check_can_admin_flagtype($flag_type_id);
+    # Process the clusions
+    foreach my $type ('inclusions', 'exclusions') {
+      my $clusions = $type eq 'inclusions' ? $inclusions : $exclusions;
+      next if not defined $clusions;
 
-        if ($can_fully_edit) {
-            $flagtype->set_all($params);
-        }
-        elsif (scalar keys %$params) {
-            ThrowUserError('flag_type_not_editable', { flagtype => $flagtype });
-        }
+      my @extra_clusions = ();
+      if (!$user->in_group('editcomponents')) {
+        my $products = $user->get_products_by_permission('editcomponents');
 
-        # Process the clusions
-        foreach my $type ('inclusions', 'exclusions') {
-            my $clusions = $type eq 'inclusions' ? $inclusions : $exclusions;
-            next if not defined $clusions;
-
-            my @extra_clusions = ();
-            if (!$user->in_group('editcomponents')) {
-                my $products = $user->get_products_by_permission('editcomponents');
-                # Bring back the products the user cannot edit.
-                foreach my $item (values %{$flagtype->$type}) {
-                    my ($prod_id, $comp_id) = split(':', $item);
-                    push(@extra_clusions, $item) unless grep { $_->id == $prod_id } @$products;
-                }
-            }
-
-            $flagtype->set_clusions({
-                $type => [@$clusions, @extra_clusions],
-            });
+        # Bring back the products the user cannot edit.
+        foreach my $item (values %{$flagtype->$type}) {
+          my ($prod_id, $comp_id) = split(':', $item);
+          push(@extra_clusions, $item) unless grep { $_->id == $prod_id } @$products;
         }
+      }
 
-        my $returned_changes = $flagtype->update();
-        $changes{$flagtype->id} = {
-            name    => $flagtype->name,
-            changes => $returned_changes,
-        };
+      $flagtype->set_clusions({$type => [@$clusions, @extra_clusions],});
     }
-    $dbh->bz_commit_transaction();
-
-    my @result;
-    foreach my $flag_type_id (keys %changes) {
-        my %hash = (
-            id      => $self->type('int',    $flag_type_id),
-            name    => $self->type('string', $changes{$flag_type_id}{name}),
-            changes => {},
-        );
-
-        foreach my $field (keys %{ $changes{$flag_type_id}{changes} }) {
-            my $change = $changes{$flag_type_id}{changes}{$field};
-            $hash{changes}{$field} = {
-                removed => $self->type('string', $change->[0]),
-                added   => $self->type('string', $change->[1])
-            };
-        }
 
-        push(@result, \%hash);
+    my $returned_changes = $flagtype->update();
+    $changes{$flagtype->id}
+      = {name => $flagtype->name, changes => $returned_changes,};
+  }
+  $dbh->bz_commit_transaction();
+
+  my @result;
+  foreach my $flag_type_id (keys %changes) {
+    my %hash = (
+      id      => $self->type('int',    $flag_type_id),
+      name    => $self->type('string', $changes{$flag_type_id}{name}),
+      changes => {},
+    );
+
+    foreach my $field (keys %{$changes{$flag_type_id}{changes}}) {
+      my $change = $changes{$flag_type_id}{changes}{$field};
+      $hash{changes}{$field} = {
+        removed => $self->type('string', $change->[0]),
+        added   => $self->type('string', $change->[1])
+      };
     }
 
-    return { flagtypes => \@result };
+    push(@result, \%hash);
+  }
+
+  return {flagtypes => \@result};
 }
 
 sub _flagtype_to_hash {
-    my ($self, $flagtype, $product) = @_;
-    my $user = Bugzilla->user;
-
-    my @values = ('X');
-    push(@values, '?') if ($flagtype->is_requestable && $user->can_request_flag($flagtype));
-    push(@values, '+', '-') if $user->can_set_flag($flagtype);
-
-    my $item = {
-        id          => $self->type('int'    , $flagtype->id),
-        name        => $self->type('string' , $flagtype->name),
-        description => $self->type('string' , $flagtype->description),
-        type        => $self->type('string' , $flagtype->target_type),
-        values      => \@values,
-        is_active   => $self->type('boolean', $flagtype->is_active),
-        is_requesteeble  => $self->type('boolean', $flagtype->is_requesteeble),
-        is_multiplicable => $self->type('boolean', $flagtype->is_multiplicable)
-    };
-
-    if ($product) {
-        my $inclusions = $self->_flagtype_clusions_to_hash($flagtype->inclusions, $product->id);
-        my $exclusions = $self->_flagtype_clusions_to_hash($flagtype->exclusions, $product->id);
-        # if we have both inclusions and exclusions, the exclusions are redundant
-        $exclusions = [] if @$inclusions && @$exclusions;
-        # no need to return anything if there's just "any component"
-        $item->{inclusions} = $inclusions if @$inclusions && $inclusions->[0] ne '';
-        $item->{exclusions} = $exclusions if @$exclusions && $exclusions->[0] ne '';
-    }
-
-    return $item;
+  my ($self, $flagtype, $product) = @_;
+  my $user = Bugzilla->user;
+
+  my @values = ('X');
+  push(@values, '?')
+    if ($flagtype->is_requestable && $user->can_request_flag($flagtype));
+  push(@values, '+', '-') if $user->can_set_flag($flagtype);
+
+  my $item = {
+    id               => $self->type('int',     $flagtype->id),
+    name             => $self->type('string',  $flagtype->name),
+    description      => $self->type('string',  $flagtype->description),
+    type             => $self->type('string',  $flagtype->target_type),
+    values           => \@values,
+    is_active        => $self->type('boolean', $flagtype->is_active),
+    is_requesteeble  => $self->type('boolean', $flagtype->is_requesteeble),
+    is_multiplicable => $self->type('boolean', $flagtype->is_multiplicable)
+  };
+
+  if ($product) {
+    my $inclusions
+      = $self->_flagtype_clusions_to_hash($flagtype->inclusions, $product->id);
+    my $exclusions
+      = $self->_flagtype_clusions_to_hash($flagtype->exclusions, $product->id);
+
+    # if we have both inclusions and exclusions, the exclusions are redundant
+    $exclusions = [] if @$inclusions && @$exclusions;
+
+    # no need to return anything if there's just "any component"
+    $item->{inclusions} = $inclusions if @$inclusions && $inclusions->[0] ne '';
+    $item->{exclusions} = $exclusions if @$exclusions && $exclusions->[0] ne '';
+  }
+
+  return $item;
 }
 
 sub _flagtype_clusions_to_hash {
-    my ($self, $clusions, $product_id) = @_;
-    my $result = [];
-    foreach my $key (keys %$clusions) {
-        my ($prod_id, $comp_id) = split(/:/, $clusions->{$key}, 2);
-        if ($prod_id == 0 || $prod_id == $product_id) {
-            if ($comp_id) {
-                my $component = Bugzilla::Component->new({ id => $comp_id, cache => 1 });
-                push @$result, $component->name;
-            }
-            else {
-                return [ '' ];
-            }
-        }
+  my ($self, $clusions, $product_id) = @_;
+  my $result = [];
+  foreach my $key (keys %$clusions) {
+    my ($prod_id, $comp_id) = split(/:/, $clusions->{$key}, 2);
+    if ($prod_id == 0 || $prod_id == $product_id) {
+      if ($comp_id) {
+        my $component = Bugzilla::Component->new({id => $comp_id, cache => 1});
+        push @$result, $component->name;
+      }
+      else {
+        return [''];
+      }
     }
-    return $result;
+  }
+  return $result;
 }
 
 sub _process_lists {
-    my $list = shift;
-    my $user = Bugzilla->user;
-
-    my @products;
-    if ($user->in_group('editcomponents')) {
-        @products = Bugzilla::Product->get_all;
-    }
-    else {
-        @products = @{$user->get_products_by_permission('editcomponents')};
-    }
-
-    my @component_list;
-
-    foreach my $item (@$list) {
-        # A hash with products as the key and component names as the values
-        if(ref($item) eq 'HASH') {
-            while (my ($product_name, $component_names) = each %$item) {
-                my $product = Bugzilla::Product->check({name => $product_name});
-                unless (grep { $product->name eq $_->name } @products) {
-                    ThrowUserError('product_access_denied', { name => $product_name });
-                }
-                my @component_ids;
-
-                foreach my $comp_name (@$component_names) {
-                    my $component = Bugzilla::Component->check({product => $product, name => $comp_name});
-                    ThrowCodeError('param_invalid', { param => $comp_name}) unless defined $component;
-                    push @component_list, $product->id . ':' . $component->id;
-                }
-            }
+  my $list = shift;
+  my $user = Bugzilla->user;
+
+  my @products;
+  if ($user->in_group('editcomponents')) {
+    @products = Bugzilla::Product->get_all;
+  }
+  else {
+    @products = @{$user->get_products_by_permission('editcomponents')};
+  }
+
+  my @component_list;
+
+  foreach my $item (@$list) {
+
+    # A hash with products as the key and component names as the values
+    if (ref($item) eq 'HASH') {
+      while (my ($product_name, $component_names) = each %$item) {
+        my $product = Bugzilla::Product->check({name => $product_name});
+        unless (grep { $product->name eq $_->name } @products) {
+          ThrowUserError('product_access_denied', {name => $product_name});
         }
-        elsif(!ref($item)) {
-            # These are whole products
-            my $product = Bugzilla::Product->check({name => $item});
-            unless (grep { $product->name eq $_->name } @products) {
-                ThrowUserError('product_access_denied', { name => $item });
-            }
-            push @component_list, $product->id . ':0';
-        }
-        else {
-            # The user has passed something invalid
-            ThrowCodeError('param_invalid', { param => $item });
+        my @component_ids;
+
+        foreach my $comp_name (@$component_names) {
+          my $component
+            = Bugzilla::Component->check({product => $product, name => $comp_name});
+          ThrowCodeError('param_invalid', {param => $comp_name})
+            unless defined $component;
+          push @component_list, $product->id . ':' . $component->id;
         }
+      }
+    }
+    elsif (!ref($item)) {
+
+      # These are whole products
+      my $product = Bugzilla::Product->check({name => $item});
+      unless (grep { $product->name eq $_->name } @products) {
+        ThrowUserError('product_access_denied', {name => $item});
+      }
+      push @component_list, $product->id . ':0';
+    }
+    else {
+      # The user has passed something invalid
+      ThrowCodeError('param_invalid', {param => $item});
     }
+  }
 
-    return \@component_list;
+  return \@component_list;
 }
 
 1;
diff --git a/Bugzilla/WebService/Group.pm b/Bugzilla/WebService/Group.pm
index 468575a35..c92583d0b 100644
--- a/Bugzilla/WebService/Group.pm
+++ b/Bugzilla/WebService/Group.pm
@@ -17,207 +17,210 @@ use Bugzilla::Error;
 use Bugzilla::WebService::Util qw(validate translate params_to_objects);
 
 use constant PUBLIC_METHODS => qw(
-    create
-    get
-    update
+  create
+  get
+  update
 );
 
-use constant MAPPED_RETURNS => {
-    userregexp => 'user_regexp',
-    isactive => 'is_active'
-};
+use constant MAPPED_RETURNS =>
+  {userregexp => 'user_regexp', isactive => 'is_active'};
 
 sub create {
-    my ($self, $params) = @_;
-
-    Bugzilla->login(LOGIN_REQUIRED);
-    Bugzilla->user->in_group('creategroups') 
-        || ThrowUserError("auth_failure", { group  => "creategroups",
-                                            action => "add",
-                                            object => "group"});
-    # Create group
-    my $group = Bugzilla::Group->create({
-        name               => $params->{name},
-        description        => $params->{description},
-        userregexp         => $params->{user_regexp},
-        isactive           => $params->{is_active},
-        isbuggroup         => 1,
-        icon_url           => $params->{icon_url}
-    });
-    return { id => $self->type('int', $group->id) };
+  my ($self, $params) = @_;
+
+  Bugzilla->login(LOGIN_REQUIRED);
+  Bugzilla->user->in_group('creategroups')
+    || ThrowUserError("auth_failure",
+    {group => "creategroups", action => "add", object => "group"});
+
+  # Create group
+  my $group = Bugzilla::Group->create({
+    name        => $params->{name},
+    description => $params->{description},
+    userregexp  => $params->{user_regexp},
+    isactive    => $params->{is_active},
+    isbuggroup  => 1,
+    icon_url    => $params->{icon_url}
+  });
+  return {id => $self->type('int', $group->id)};
 }
 
 sub update {
-    my ($self, $params) = @_;
+  my ($self, $params) = @_;
+
+  my $dbh = Bugzilla->dbh;
+
+  Bugzilla->login(LOGIN_REQUIRED);
+  Bugzilla->user->in_group('creategroups')
+    || ThrowUserError("auth_failure",
+    {group => "creategroups", action => "edit", object => "group"});
+
+  defined($params->{names})
+    || defined($params->{ids})
+    || ThrowCodeError('params_required',
+    {function => 'Group.update', params => ['ids', 'names']});
+
+  my $group_objects = params_to_objects($params, 'Bugzilla::Group');
+
+  my %values = %$params;
+
+  # We delete names and ids to keep only new values to set.
+  delete $values{names};
+  delete $values{ids};
+
+  $dbh->bz_start_transaction();
+  foreach my $group (@$group_objects) {
+    $group->set_all(\%values);
+  }
+
+  my %changes;
+  foreach my $group (@$group_objects) {
+    my $returned_changes = $group->update();
+    $changes{$group->id} = translate($returned_changes, MAPPED_RETURNS);
+  }
+  $dbh->bz_commit_transaction();
+
+  my @result;
+  foreach my $group (@$group_objects) {
+    my %hash = (id => $group->id, changes => {},);
+    foreach my $field (keys %{$changes{$group->id}}) {
+      my $change = $changes{$group->id}->{$field};
+      $hash{changes}{$field} = {
+        removed => $self->type('string', $change->[0]),
+        added   => $self->type('string', $change->[1])
+      };
+    }
+    push(@result, \%hash);
+  }
 
-    my $dbh = Bugzilla->dbh;
+  return {groups => \@result};
+}
 
-    Bugzilla->login(LOGIN_REQUIRED);
-    Bugzilla->user->in_group('creategroups')
-        || ThrowUserError("auth_failure", { group  => "creategroups",
-                                            action => "edit",
-                                            object => "group" });
+sub get {
+  my ($self, $params) = validate(@_, 'ids', 'names', 'type');
 
-    defined($params->{names}) || defined($params->{ids})
-        || ThrowCodeError('params_required',
-               { function => 'Group.update', params => ['ids', 'names'] });
+  Bugzilla->login(LOGIN_REQUIRED);
 
-    my $group_objects = params_to_objects($params, 'Bugzilla::Group');
+  # Reject access if there is no sense in continuing.
+  my $user = Bugzilla->user;
+  my $all_groups
+    = $user->in_group('editusers') || $user->in_group('creategroups');
+  if (!$all_groups && !$user->can_bless) {
+    ThrowUserError('group_cannot_view');
+  }
 
-    my %values = %$params;
-    
-    # We delete names and ids to keep only new values to set.
-    delete $values{names};
-    delete $values{ids};
+  Bugzilla->switch_to_shadow_db();
 
-    $dbh->bz_start_transaction();
-    foreach my $group (@$group_objects) {
-        $group->set_all(\%values);
-    }
+  my $groups = [];
 
-    my %changes;
-    foreach my $group (@$group_objects) {
-        my $returned_changes = $group->update();
-        $changes{$group->id} = translate($returned_changes, MAPPED_RETURNS);
-    }
-    $dbh->bz_commit_transaction();
-
-    my @result;
-    foreach my $group (@$group_objects) {
-        my %hash = (
-            id      => $group->id,
-            changes => {},
-        );
-        foreach my $field (keys %{ $changes{$group->id} }) {
-            my $change = $changes{$group->id}->{$field};
-            $hash{changes}{$field} = {
-                removed => $self->type('string', $change->[0]),
-                added   => $self->type('string', $change->[1]) 
-            };
-        }
-       push(@result, \%hash);
-    }
+  if (defined $params->{ids}) {
 
-    return { groups => \@result };
-}
-
-sub get {
-    my ($self, $params) = validate(@_, 'ids', 'names', 'type');
+    # Get the groups by id
+    $groups = Bugzilla::Group->new_from_list($params->{ids});
+  }
 
-    Bugzilla->login(LOGIN_REQUIRED);
-
-    # Reject access if there is no sense in continuing.
-    my $user = Bugzilla->user;
-    my $all_groups = $user->in_group('editusers') || $user->in_group('creategroups');
-    if (!$all_groups && !$user->can_bless) {
-        ThrowUserError('group_cannot_view');
-    }
+  if (defined $params->{names}) {
 
-    Bugzilla->switch_to_shadow_db();
+    # Get the groups by name. Check will throw an error if a bad name is given
+    foreach my $name (@{$params->{names}}) {
 
-    my $groups = [];
+      # Skip if we got this from params->{id}
+      next if grep { $_->name eq $name } @$groups;
 
-    if (defined $params->{ids}) {
-        # Get the groups by id
-        $groups = Bugzilla::Group->new_from_list($params->{ids});
+      push @$groups, Bugzilla::Group->check({name => $name});
     }
+  }
 
-    if (defined $params->{names}) {
-        # Get the groups by name. Check will throw an error if a bad name is given
-        foreach my $name (@{$params->{names}}) {
-            # Skip if we got this from params->{id}
-            next if grep { $_->name eq $name } @$groups;
-
-            push @$groups, Bugzilla::Group->check({ name => $name });
-        }
+  if (!defined $params->{ids} && !defined $params->{names}) {
+    if ($all_groups) {
+      @$groups = Bugzilla::Group->get_all;
     }
-
-    if (!defined $params->{ids} && !defined $params->{names}) {
-        if ($all_groups) {
-            @$groups = Bugzilla::Group->get_all;
-        }
-        else {
-            # Get only groups the user has bless groups too
-            $groups = $user->bless_groups;
-        }
+    else {
+      # Get only groups the user has bless groups too
+      $groups = $user->bless_groups;
     }
+  }
 
-    # Now create a result entry for each.
-    my @groups = map { $self->_group_to_hash($params, $_) } @$groups;
-    return { groups => \@groups };
+  # Now create a result entry for each.
+  my @groups = map { $self->_group_to_hash($params, $_) } @$groups;
+  return {groups => \@groups};
 }
 
 sub _group_to_hash {
-    my ($self, $params, $group) = @_;
-    my $user = Bugzilla->user;
-
-    my $field_data = {
-        id          => $self->type('int', $group->id),
-        name        => $self->type('string', $group->name),
-        description => $self->type('string', $group->description),
-    };
-
-    if ($user->in_group('creategroups')) {
-        $field_data->{is_active}    = $self->type('boolean', $group->is_active);
-        $field_data->{is_bug_group} = $self->type('boolean', $group->is_bug_group);
-        $field_data->{user_regexp}  = $self->type('string', $group->user_regexp);
-    }
-
-    if ($params->{membership}) {
-        $field_data->{membership} = $self->_get_group_membership($group, $params);
-    }
-    return $field_data;
+  my ($self, $params, $group) = @_;
+  my $user = Bugzilla->user;
+
+  my $field_data = {
+    id          => $self->type('int',    $group->id),
+    name        => $self->type('string', $group->name),
+    description => $self->type('string', $group->description),
+  };
+
+  if ($user->in_group('creategroups')) {
+    $field_data->{is_active}    = $self->type('boolean', $group->is_active);
+    $field_data->{is_bug_group} = $self->type('boolean', $group->is_bug_group);
+    $field_data->{user_regexp}  = $self->type('string',  $group->user_regexp);
+  }
+
+  if ($params->{membership}) {
+    $field_data->{membership} = $self->_get_group_membership($group, $params);
+  }
+  return $field_data;
 }
 
 sub _get_group_membership {
-    my ($self, $group, $params) = @_;
-    my $user = Bugzilla->user;
+  my ($self, $group, $params) = @_;
+  my $user = Bugzilla->user;
 
-    my %users_only;
-    my $dbh = Bugzilla->dbh;
-    my $editusers = $user->in_group('editusers');
+  my %users_only;
+  my $dbh       = Bugzilla->dbh;
+  my $editusers = $user->in_group('editusers');
 
-    my $query = 'SELECT userid FROM profiles';
-    my $visibleGroups;
+  my $query = 'SELECT userid FROM profiles';
+  my $visibleGroups;
 
-    if (!$editusers && Bugzilla->params->{'usevisibilitygroups'}) {
-        # Show only users in visible groups.
-        $visibleGroups = $user->visible_groups_inherited;
+  if (!$editusers && Bugzilla->params->{'usevisibilitygroups'}) {
 
-        if (scalar @$visibleGroups) {
-            $query .= qq{, user_group_map AS ugm
+    # Show only users in visible groups.
+    $visibleGroups = $user->visible_groups_inherited;
+
+    if (scalar @$visibleGroups) {
+      $query .= qq{, user_group_map AS ugm
                          WHERE ugm.user_id = profiles.userid
                            AND ugm.isbless = 0
                            AND } . $dbh->sql_in('ugm.group_id', $visibleGroups);
-        }
-    } elsif ($editusers || $user->can_bless($group->id) || $user->in_group('creategroups')) {
-        $visibleGroups = 1;
-        $query .= qq{, user_group_map AS ugm
+    }
+  }
+  elsif ($editusers
+    || $user->can_bless($group->id)
+    || $user->in_group('creategroups'))
+  {
+    $visibleGroups = 1;
+    $query .= qq{, user_group_map AS ugm
                      WHERE ugm.user_id = profiles.userid
                        AND ugm.isbless = 0
                     };
-    }
-    if (!$visibleGroups) {
-        ThrowUserError('group_not_visible', { group => $group });
-    }
-
-    my $grouplist = Bugzilla::Group->flatten_group_membership($group->id);
-    $query .= ' AND ' . $dbh->sql_in('ugm.group_id', $grouplist);
-
-    my $userids = $dbh->selectcol_arrayref($query);
-    my $user_objects = Bugzilla::User->new_from_list($userids);
-    my @users =
-        map {{
-            id                => $self->type('int', $_->id),
-            real_name         => $self->type('string', $_->name),
-            name              => $self->type('string', $_->login),
-            email             => $self->type('string', $_->email),
-            can_login         => $self->type('boolean', $_->is_enabled),
-            email_enabled     => $self->type('boolean', $_->email_enabled),
-            login_denied_text => $self->type('string', $_->disabledtext),
-        }} @$user_objects;
-
-    return \@users;
+  }
+  if (!$visibleGroups) {
+    ThrowUserError('group_not_visible', {group => $group});
+  }
+
+  my $grouplist = Bugzilla::Group->flatten_group_membership($group->id);
+  $query .= ' AND ' . $dbh->sql_in('ugm.group_id', $grouplist);
+
+  my $userids      = $dbh->selectcol_arrayref($query);
+  my $user_objects = Bugzilla::User->new_from_list($userids);
+  my @users        = map { {
+    id                => $self->type('int',     $_->id),
+    real_name         => $self->type('string',  $_->name),
+    name              => $self->type('string',  $_->login),
+    email             => $self->type('string',  $_->email),
+    can_login         => $self->type('boolean', $_->is_enabled),
+    email_enabled     => $self->type('boolean', $_->email_enabled),
+    login_denied_text => $self->type('string',  $_->disabledtext),
+  } } @$user_objects;
+
+  return \@users;
 }
 
 1;
diff --git a/Bugzilla/WebService/Product.pm b/Bugzilla/WebService/Product.pm
index 94348a161..a7980172e 100644
--- a/Bugzilla/WebService/Product.pm
+++ b/Bugzilla/WebService/Product.pm
@@ -17,39 +17,36 @@ use Bugzilla::User;
 use Bugzilla::Error;
 use Bugzilla::Constants;
 use Bugzilla::WebService::Constants;
-use Bugzilla::WebService::Util qw(validate filter filter_wants translate params_to_objects);
+use Bugzilla::WebService::Util
+  qw(validate filter filter_wants translate params_to_objects);
 
 use constant READ_ONLY => qw(
-    get
-    get_accessible_products
-    get_enterable_products
-    get_selectable_products
+  get
+  get_accessible_products
+  get_enterable_products
+  get_selectable_products
 );
 
 use constant PUBLIC_METHODS => qw(
-    create
-    get
-    get_accessible_products
-    get_enterable_products
-    get_selectable_products
-    update
+  create
+  get
+  get_accessible_products
+  get_enterable_products
+  get_selectable_products
+  update
 );
 
-use constant MAPPED_FIELDS => {
-    has_unconfirmed => 'allows_unconfirmed',
-    is_open => 'is_active',
-};
+use constant MAPPED_FIELDS =>
+  {has_unconfirmed => 'allows_unconfirmed', is_open => 'is_active',};
 
 use constant MAPPED_RETURNS => {
-    allows_unconfirmed => 'has_unconfirmed',
-    defaultmilestone => 'default_milestone',
-    isactive => 'is_open',
+  allows_unconfirmed => 'has_unconfirmed',
+  defaultmilestone   => 'default_milestone',
+  isactive           => 'is_open',
 };
 
-use constant FIELD_MAP => {
-    has_unconfirmed => 'allows_unconfirmed',
-    is_open         => 'isactive',
-};
+use constant FIELD_MAP =>
+  {has_unconfirmed => 'allows_unconfirmed', is_open => 'isactive',};
 
 ##################################################
 # Add aliases here for method name compatibility #
@@ -57,300 +54,277 @@ use constant FIELD_MAP => {
 
 # Get the ids of the products the user can search
 sub get_selectable_products {
-    Bugzilla->switch_to_shadow_db();
-    return {ids => [map {$_->id} @{Bugzilla->user->get_selectable_products}]};
+  Bugzilla->switch_to_shadow_db();
+  return {ids => [map { $_->id } @{Bugzilla->user->get_selectable_products}]};
 }
 
 # Get the ids of the products the user can enter bugs against
 sub get_enterable_products {
-    Bugzilla->switch_to_shadow_db();
-    return {ids => [map {$_->id} @{Bugzilla->user->get_enterable_products}]};
+  Bugzilla->switch_to_shadow_db();
+  return {ids => [map { $_->id } @{Bugzilla->user->get_enterable_products}]};
 }
 
 # Get the union of the products the user can search and enter bugs against.
 sub get_accessible_products {
-    Bugzilla->switch_to_shadow_db();
-    return {ids => [map {$_->id} @{Bugzilla->user->get_accessible_products}]};
+  Bugzilla->switch_to_shadow_db();
+  return {ids => [map { $_->id } @{Bugzilla->user->get_accessible_products}]};
 }
 
 # Get a list of actual products, based on list of ids or names
 sub get {
-    my ($self, $params) = validate(@_, 'ids', 'names', 'type');
-    my $user = Bugzilla->user;
-
-    defined $params->{ids} || defined $params->{names} || defined $params->{type}
-        || ThrowCodeError("params_required", { function => "Product.get",
-                                               params => ['ids', 'names', 'type'] });
-    Bugzilla->switch_to_shadow_db();
-
-    my $products = [];
-    if (defined $params->{type}) {
-        my %product_hash;
-        foreach my $type (@{ $params->{type} }) {
-            my $result = [];
-            if ($type eq 'accessible') {
-                $result = $user->get_accessible_products();
-            }
-            elsif ($type eq 'enterable') {
-                $result = $user->get_enterable_products();
-            }
-            elsif ($type eq 'selectable') {
-                $result = $user->get_selectable_products();
-            }
-            else {
-                ThrowUserError('get_products_invalid_type',
-                               { type => $type });
-            }
-            map { $product_hash{$_->id} = $_ } @$result;
-        }
-        $products = [ values %product_hash ];
-    }
-    else {
-        $products = $user->get_accessible_products;
+  my ($self, $params) = validate(@_, 'ids', 'names', 'type');
+  my $user = Bugzilla->user;
+
+       defined $params->{ids}
+    || defined $params->{names}
+    || defined $params->{type}
+    || ThrowCodeError("params_required",
+    {function => "Product.get", params => ['ids', 'names', 'type']});
+  Bugzilla->switch_to_shadow_db();
+
+  my $products = [];
+  if (defined $params->{type}) {
+    my %product_hash;
+    foreach my $type (@{$params->{type}}) {
+      my $result = [];
+      if ($type eq 'accessible') {
+        $result = $user->get_accessible_products();
+      }
+      elsif ($type eq 'enterable') {
+        $result = $user->get_enterable_products();
+      }
+      elsif ($type eq 'selectable') {
+        $result = $user->get_selectable_products();
+      }
+      else {
+        ThrowUserError('get_products_invalid_type', {type => $type});
+      }
+      map { $product_hash{$_->id} = $_ } @$result;
     }
+    $products = [values %product_hash];
+  }
+  else {
+    $products = $user->get_accessible_products;
+  }
 
-    my @requested_products;
+  my @requested_products;
 
-    if (defined $params->{ids}) {
-        # Create a hash with the ids the user wants
-        my %ids = map { $_ => 1 } @{$params->{ids}};
+  if (defined $params->{ids}) {
 
-        # Return the intersection of this, by grepping the ids from $products.
-        push(@requested_products,
-            grep { $ids{$_->id} } @$products);
-    }
+    # Create a hash with the ids the user wants
+    my %ids = map { $_ => 1 } @{$params->{ids}};
 
-    if (defined $params->{names}) {
-        # Create a hash with the names the user wants
-        my %names = map { lc($_) => 1 } @{$params->{names}};
-
-        # Return the intersection of this, by grepping the names
-        # from $products, union'ed with products found by ID to
-        # avoid duplicates
-        foreach my $product (grep { $names{lc $_->name} }
-                                  @$products) {
-            next if grep { $_->id == $product->id }
-                         @requested_products;
-            push @requested_products, $product;
-        }
-    }
+    # Return the intersection of this, by grepping the ids from $products.
+    push(@requested_products, grep { $ids{$_->id} } @$products);
+  }
+
+  if (defined $params->{names}) {
 
-    # If we just requested a specific type of products without
-    # specifying ids or names, then return the entire list.
-    if (!defined $params->{ids} && !defined $params->{names}) {
-        @requested_products = @$products;
+    # Create a hash with the names the user wants
+    my %names = map { lc($_) => 1 } @{$params->{names}};
+
+    # Return the intersection of this, by grepping the names
+    # from $products, union'ed with products found by ID to
+    # avoid duplicates
+    foreach my $product (grep { $names{lc $_->name} } @$products) {
+      next if grep { $_->id == $product->id } @requested_products;
+      push @requested_products, $product;
     }
+  }
+
+  # If we just requested a specific type of products without
+  # specifying ids or names, then return the entire list.
+  if (!defined $params->{ids} && !defined $params->{names}) {
+    @requested_products = @$products;
+  }
 
-    # Now create a result entry for each.
-    my @products = map { $self->_product_to_hash($params, $_) }
-                       @requested_products;
-    return { products => \@products };
+  # Now create a result entry for each.
+  my @products = map { $self->_product_to_hash($params, $_) } @requested_products;
+  return {products => \@products};
 }
 
 sub create {
-    my ($self, $params) = @_;
-
-    Bugzilla->login(LOGIN_REQUIRED);
-    Bugzilla->user->in_group('editcomponents')
-        || ThrowUserError("auth_failure", { group  => "editcomponents",
-                                            action => "add",
-                                            object => "products"});
-    # Create product
-    my $args = {
-        name             => $params->{name},
-        description      => $params->{description},
-        version          => $params->{version},
-        defaultmilestone => $params->{default_milestone},
-        # create_series has no default value.
-        create_series    => defined $params->{create_series} ?
-                              $params->{create_series} : 1
-    };
-    foreach my $field (qw(has_unconfirmed is_open classification)) {
-        if (defined $params->{$field}) {
-            my $name = FIELD_MAP->{$field} || $field;
-            $args->{$name} = $params->{$field};
-        }
+  my ($self, $params) = @_;
+
+  Bugzilla->login(LOGIN_REQUIRED);
+  Bugzilla->user->in_group('editcomponents')
+    || ThrowUserError("auth_failure",
+    {group => "editcomponents", action => "add", object => "products"});
+
+  # Create product
+  my $args = {
+    name             => $params->{name},
+    description      => $params->{description},
+    version          => $params->{version},
+    defaultmilestone => $params->{default_milestone},
+
+    # create_series has no default value.
+    create_series => defined $params->{create_series}
+    ? $params->{create_series}
+    : 1
+  };
+  foreach my $field (qw(has_unconfirmed is_open classification)) {
+    if (defined $params->{$field}) {
+      my $name = FIELD_MAP->{$field} || $field;
+      $args->{$name} = $params->{$field};
     }
-    my $product = Bugzilla::Product->create($args);
-    return { id => $self->type('int', $product->id) };
+  }
+  my $product = Bugzilla::Product->create($args);
+  return {id => $self->type('int', $product->id)};
 }
 
 sub update {
-    my ($self, $params) = @_;
-
-    my $dbh = Bugzilla->dbh;
-
-    Bugzilla->login(LOGIN_REQUIRED);
-    Bugzilla->user->in_group('editcomponents')
-        || ThrowUserError("auth_failure", { group  => "editcomponents",
-                                            action => "edit",
-                                            object => "products" });
-
-    defined($params->{names}) || defined($params->{ids})
-        || ThrowCodeError('params_required',
-               { function => 'Product.update', params => ['ids', 'names'] });
-
-    my $product_objects = params_to_objects($params, 'Bugzilla::Product');
-
-    my $values = translate($params, MAPPED_FIELDS);
-
-    # We delete names and ids to keep only new values to set.
-    delete $values->{names};
-    delete $values->{ids};
-
-    $dbh->bz_start_transaction();
-    foreach my $product (@$product_objects) {
-        $product->set_all($values);
+  my ($self, $params) = @_;
+
+  my $dbh = Bugzilla->dbh;
+
+  Bugzilla->login(LOGIN_REQUIRED);
+  Bugzilla->user->in_group('editcomponents')
+    || ThrowUserError("auth_failure",
+    {group => "editcomponents", action => "edit", object => "products"});
+
+  defined($params->{names})
+    || defined($params->{ids})
+    || ThrowCodeError('params_required',
+    {function => 'Product.update', params => ['ids', 'names']});
+
+  my $product_objects = params_to_objects($params, 'Bugzilla::Product');
+
+  my $values = translate($params, MAPPED_FIELDS);
+
+  # We delete names and ids to keep only new values to set.
+  delete $values->{names};
+  delete $values->{ids};
+
+  $dbh->bz_start_transaction();
+  foreach my $product (@$product_objects) {
+    $product->set_all($values);
+  }
+
+  my %changes;
+  foreach my $product (@$product_objects) {
+    my $returned_changes = $product->update();
+    $changes{$product->id} = translate($returned_changes, MAPPED_RETURNS);
+  }
+  $dbh->bz_commit_transaction();
+
+  my @result;
+  foreach my $product (@$product_objects) {
+    my %hash = (id => $product->id, changes => {},);
+
+    foreach my $field (keys %{$changes{$product->id}}) {
+      my $change = $changes{$product->id}->{$field};
+      $hash{changes}{$field} = {
+        removed => $self->type('string', $change->[0]),
+        added   => $self->type('string', $change->[1])
+      };
     }
 
-    my %changes;
-    foreach my $product (@$product_objects) {
-        my $returned_changes = $product->update();
-        $changes{$product->id} = translate($returned_changes, MAPPED_RETURNS);
-    }
-    $dbh->bz_commit_transaction();
-
-    my @result;
-    foreach my $product (@$product_objects) {
-        my %hash = (
-            id      => $product->id,
-            changes => {},
-        );
-
-        foreach my $field (keys %{ $changes{$product->id} }) {
-            my $change = $changes{$product->id}->{$field};
-            $hash{changes}{$field} = {
-                removed => $self->type('string', $change->[0]),
-                added   => $self->type('string', $change->[1])
-            };
-        }
-
-        push(@result, \%hash);
-    }
+    push(@result, \%hash);
+  }
 
-    return { products => \@result };
+  return {products => \@result};
 }
 
 sub _product_to_hash {
-    my ($self, $params, $product) = @_;
-
-    my $field_data = {
-        id          => $self->type('int', $product->id),
-        name        => $self->type('string', $product->name),
-        description => $self->type('string', $product->description),
-        is_active   => $self->type('boolean', $product->is_active),
-        default_milestone => $self->type('string', $product->default_milestone),
-        has_unconfirmed   => $self->type('boolean', $product->allows_unconfirmed),
-        classification => $self->type('string', $product->classification->name),
-    };
-    if (filter_wants($params, 'components')) {
-        $field_data->{components} = [map {
-            $self->_component_to_hash($_, $params)
-        } @{$product->components}];
-    }
-    if (filter_wants($params, 'versions')) {
-        $field_data->{versions} = [map {
-            $self->_version_to_hash($_, $params)
-        } @{$product->versions}];
-    }
-    if (filter_wants($params, 'milestones')) {
-        $field_data->{milestones} = [map {
-            $self->_milestone_to_hash($_, $params)
-        } @{$product->milestones}];
-    }
-    return filter($params, $field_data);
+  my ($self, $params, $product) = @_;
+
+  my $field_data = {
+    id                => $self->type('int',     $product->id),
+    name              => $self->type('string',  $product->name),
+    description       => $self->type('string',  $product->description),
+    is_active         => $self->type('boolean', $product->is_active),
+    default_milestone => $self->type('string',  $product->default_milestone),
+    has_unconfirmed   => $self->type('boolean', $product->allows_unconfirmed),
+    classification    => $self->type('string',  $product->classification->name),
+  };
+  if (filter_wants($params, 'components')) {
+    $field_data->{components}
+      = [map { $self->_component_to_hash($_, $params) } @{$product->components}];
+  }
+  if (filter_wants($params, 'versions')) {
+    $field_data->{versions}
+      = [map { $self->_version_to_hash($_, $params) } @{$product->versions}];
+  }
+  if (filter_wants($params, 'milestones')) {
+    $field_data->{milestones}
+      = [map { $self->_milestone_to_hash($_, $params) } @{$product->milestones}];
+  }
+  return filter($params, $field_data);
 }
 
 sub _component_to_hash {
-    my ($self, $component, $params) = @_;
-    my $field_data = filter $params, {
-        id =>
-            $self->type('int', $component->id),
-        name =>
-            $self->type('string', $component->name),
-        description =>
-            $self->type('string' , $component->description),
-        default_assigned_to =>
-            $self->type('email', $component->default_assignee->login),
-        default_qa_contact =>
-            $self->type('email', $component->default_qa_contact ?
-                                 $component->default_qa_contact->login : ""),
-        sort_key =>  # sort_key is returned to match Bug.fields
-            0,
-        is_active =>
-            $self->type('boolean', $component->is_active),
-    }, undef, 'components';
-
-    if (filter_wants($params, 'flag_types', undef, 'components')) {
-        $field_data->{flag_types} = {
-            bug =>
-                [map {
-                    $self->_flag_type_to_hash($_)
-                } @{$component->flag_types->{'bug'}}],
-            attachment =>
-                [map {
-                    $self->_flag_type_to_hash($_)
-                } @{$component->flag_types->{'attachment'}}],
-        };
-    }
+  my ($self, $component, $params) = @_;
+  my $field_data = filter $params, {
+    id          => $self->type('int',    $component->id),
+    name        => $self->type('string', $component->name),
+    description => $self->type('string', $component->description),
+    default_assigned_to =>
+      $self->type('email', $component->default_assignee->login),
+    default_qa_contact => $self->type(
+      'email',
+      $component->default_qa_contact ? $component->default_qa_contact->login : ""
+    ),
+    sort_key =>    # sort_key is returned to match Bug.fields
+      0,
+    is_active => $self->type('boolean', $component->is_active),
+    },
+    undef, 'components';
+
+  if (filter_wants($params, 'flag_types', undef, 'components')) {
+    $field_data->{flag_types} = {
+      bug =>
+        [map { $self->_flag_type_to_hash($_) } @{$component->flag_types->{'bug'}}],
+      attachment => [
+        map { $self->_flag_type_to_hash($_) } @{$component->flag_types->{'attachment'}}
+      ],
+    };
+  }
 
-    return $field_data;
+  return $field_data;
 }
 
 sub _flag_type_to_hash {
-    my ($self, $flag_type, $params) = @_;
-    return filter $params, {
-        id =>
-            $self->type('int', $flag_type->id),
-        name =>
-            $self->type('string', $flag_type->name),
-        description =>
-            $self->type('string', $flag_type->description),
-        cc_list =>
-            $self->type('string', $flag_type->cc_list),
-        sort_key =>
-            $self->type('int', $flag_type->sortkey),
-        is_active =>
-            $self->type('boolean', $flag_type->is_active),
-        is_requestable =>
-            $self->type('boolean', $flag_type->is_requestable),
-        is_requesteeble =>
-            $self->type('boolean', $flag_type->is_requesteeble),
-        is_multiplicable =>
-            $self->type('boolean', $flag_type->is_multiplicable),
-        grant_group =>
-            $self->type('int', $flag_type->grant_group_id),
-        request_group =>
-            $self->type('int', $flag_type->request_group_id),
-    }, undef, 'flag_types';
+  my ($self, $flag_type, $params) = @_;
+  return filter $params,
+    {
+    id               => $self->type('int',     $flag_type->id),
+    name             => $self->type('string',  $flag_type->name),
+    description      => $self->type('string',  $flag_type->description),
+    cc_list          => $self->type('string',  $flag_type->cc_list),
+    sort_key         => $self->type('int',     $flag_type->sortkey),
+    is_active        => $self->type('boolean', $flag_type->is_active),
+    is_requestable   => $self->type('boolean', $flag_type->is_requestable),
+    is_requesteeble  => $self->type('boolean', $flag_type->is_requesteeble),
+    is_multiplicable => $self->type('boolean', $flag_type->is_multiplicable),
+    grant_group      => $self->type('int',     $flag_type->grant_group_id),
+    request_group    => $self->type('int',     $flag_type->request_group_id),
+    },
+    undef, 'flag_types';
 }
 
 sub _version_to_hash {
-    my ($self, $version, $params) = @_;
-    return filter $params, {
-        id =>
-            $self->type('int', $version->id),
-        name =>
-            $self->type('string', $version->name),
-        sort_key =>  # sort_key is returened to match Bug.fields
-            0,
-        is_active =>
-            $self->type('boolean', $version->is_active),
-    }, undef, 'versions';
+  my ($self, $version, $params) = @_;
+  return filter $params, {
+    id   => $self->type('int',    $version->id),
+    name => $self->type('string', $version->name),
+    sort_key =>    # sort_key is returened to match Bug.fields
+      0,
+    is_active => $self->type('boolean', $version->is_active),
+    },
+    undef, 'versions';
 }
 
 sub _milestone_to_hash {
-    my ($self, $milestone, $params) = @_;
-    return filter $params, {
-        id =>
-            $self->type('int', $milestone->id),
-        name =>
-            $self->type('string', $milestone->name),
-        sort_key =>
-            $self->type('int', $milestone->sortkey),
-        is_active =>
-            $self->type('boolean', $milestone->is_active),
-    }, undef, 'milestones';
+  my ($self, $milestone, $params) = @_;
+  return filter $params,
+    {
+    id        => $self->type('int',     $milestone->id),
+    name      => $self->type('string',  $milestone->name),
+    sort_key  => $self->type('int',     $milestone->sortkey),
+    is_active => $self->type('boolean', $milestone->is_active),
+    },
+    undef, 'milestones';
 }
 
 1;
diff --git a/Bugzilla/WebService/Server.pm b/Bugzilla/WebService/Server.pm
index 7950c7a3b..f2844cdcb 100644
--- a/Bugzilla/WebService/Server.pm
+++ b/Bugzilla/WebService/Server.pm
@@ -20,72 +20,77 @@ use Digest::MD5 qw(md5_base64);
 use Storable qw(freeze);
 
 sub handle_login {
-    my ($self, $class, $method, $full_method) = @_;
-    # Throw error if the supplied class does not exist or the method is private
-    ThrowCodeError('unknown_method', {method => $full_method}) if (!$class or $method =~ /^_/);
-
-    eval "require $class";
-    ThrowCodeError('unknown_method', {method => $full_method}) if $@;
-    return if ($class->login_exempt($method) 
-               and !defined Bugzilla->input_params->{Bugzilla_login});
-    Bugzilla->login();
-
-    Bugzilla::Hook::process(
-        'webservice_before_call',
-        { 'method'  => $method, full_method => $full_method });
+  my ($self, $class, $method, $full_method) = @_;
+
+  # Throw error if the supplied class does not exist or the method is private
+  ThrowCodeError('unknown_method', {method => $full_method})
+    if (!$class or $method =~ /^_/);
+
+  eval "require $class";
+  ThrowCodeError('unknown_method', {method => $full_method}) if $@;
+  return
+    if ($class->login_exempt($method)
+    and !defined Bugzilla->input_params->{Bugzilla_login});
+  Bugzilla->login();
+
+  Bugzilla::Hook::process('webservice_before_call',
+    {'method' => $method, full_method => $full_method});
 }
 
 sub datetime_format_inbound {
-    my ($self, $time) = @_;
-
-    my $converted = datetime_from($time, Bugzilla->local_timezone);
-    if (!defined $converted) {
-        ThrowUserError('illegal_date', { date => $time });
-    }
-    $time = $converted->ymd() . ' ' . $converted->hms();
-    return $time
+  my ($self, $time) = @_;
+
+  my $converted = datetime_from($time, Bugzilla->local_timezone);
+  if (!defined $converted) {
+    ThrowUserError('illegal_date', {date => $time});
+  }
+  $time = $converted->ymd() . ' ' . $converted->hms();
+  return $time;
 }
 
 sub datetime_format_outbound {
-    my ($self, $date) = @_;
-
-    return undef if (!defined $date or $date eq '');
-
-    my $time = $date;
-    if (blessed($date)) {
-        # We expect this to mean we were sent a datetime object
-        $time->set_time_zone('UTC');
-    } else {
-        # We always send our time in UTC, for consistency.
-        # passed in value is likely a string, create a datetime object
-        $time = datetime_from($date, 'UTC');
-    }
-    return $time->iso8601();
+  my ($self, $date) = @_;
+
+  return undef if (!defined $date or $date eq '');
+
+  my $time = $date;
+  if (blessed($date)) {
+
+    # We expect this to mean we were sent a datetime object
+    $time->set_time_zone('UTC');
+  }
+  else {
+    # We always send our time in UTC, for consistency.
+    # passed in value is likely a string, create a datetime object
+    $time = datetime_from($date, 'UTC');
+  }
+  return $time->iso8601();
 }
 
 # ETag support
 sub bz_etag {
-    my ($self, $data) = @_;
-    my $cache = Bugzilla->request_cache;
-    if (defined $data) {
-        # Serialize the data if passed a reference
-        local $Storable::canonical = 1;
-        $data = freeze($data) if ref $data;
-
-        # Wide characters cause md5_base64() to die.
-        utf8::encode($data) if utf8::is_utf8($data);
-
-        # Append content_type to the end of the data
-        # string as we want the etag to be unique to
-        # the content_type. We do not need this for
-        # XMLRPC as text/xml is always returned.
-        if (blessed($self) && $self->can('content_type')) {
-            $data .= $self->content_type if $self->content_type;
-        }
-
-        $cache->{'bz_etag'} = md5_base64($data);
+  my ($self, $data) = @_;
+  my $cache = Bugzilla->request_cache;
+  if (defined $data) {
+
+    # Serialize the data if passed a reference
+    local $Storable::canonical = 1;
+    $data = freeze($data) if ref $data;
+
+    # Wide characters cause md5_base64() to die.
+    utf8::encode($data) if utf8::is_utf8($data);
+
+    # Append content_type to the end of the data
+    # string as we want the etag to be unique to
+    # the content_type. We do not need this for
+    # XMLRPC as text/xml is always returned.
+    if (blessed($self) && $self->can('content_type')) {
+      $data .= $self->content_type if $self->content_type;
     }
-    return $cache->{'bz_etag'};
+
+    $cache->{'bz_etag'} = md5_base64($data);
+  }
+  return $cache->{'bz_etag'};
 }
 
 1;
diff --git a/Bugzilla/WebService/Server/JSONRPC.pm b/Bugzilla/WebService/Server/JSONRPC.pm
index 70b8fd96c..66640beb7 100644
--- a/Bugzilla/WebService/Server/JSONRPC.pm
+++ b/Bugzilla/WebService/Server/JSONRPC.pm
@@ -12,16 +12,17 @@ use strict;
 use warnings;
 
 use Bugzilla::WebService::Server;
-BEGIN {
-    our @ISA = qw(Bugzilla::WebService::Server);
 
-    if (eval { require JSON::RPC::Server::CGI }) {
-        unshift(@ISA, 'JSON::RPC::Server::CGI');
-    }
-    else {
-        require JSON::RPC::Legacy::Server::CGI;
-        unshift(@ISA, 'JSON::RPC::Legacy::Server::CGI');
-    }
+BEGIN {
+  our @ISA = qw(Bugzilla::WebService::Server);
+
+  if (eval { require JSON::RPC::Server::CGI }) {
+    unshift(@ISA, 'JSON::RPC::Server::CGI');
+  }
+  else {
+    require JSON::RPC::Legacy::Server::CGI;
+    unshift(@ISA, 'JSON::RPC::Legacy::Server::CGI');
+  }
 }
 
 use Bugzilla::Error;
@@ -38,79 +39,83 @@ use List::MoreUtils qw(none);
 #####################################
 
 sub new {
-    my $class = shift;
-    my $self = $class->SUPER::new(@_);
-    Bugzilla->_json_server($self);
-    $self->dispatch(WS_DISPATCH);
-    $self->return_die_message(1);
-    return $self;
+  my $class = shift;
+  my $self  = $class->SUPER::new(@_);
+  Bugzilla->_json_server($self);
+  $self->dispatch(WS_DISPATCH);
+  $self->return_die_message(1);
+  return $self;
 }
 
 sub create_json_coder {
-    my $self = shift;
-    my $json = $self->SUPER::create_json_coder(@_);
-    $json->allow_blessed(1);
-    $json->convert_blessed(1);
-    # This may seem a little backwards, but what this really means is
-    # "don't convert our utf8 into byte strings, just leave it as a
-    # utf8 string."
-    $json->utf8(0) if Bugzilla->params->{'utf8'};
-    return $json;
+  my $self = shift;
+  my $json = $self->SUPER::create_json_coder(@_);
+  $json->allow_blessed(1);
+  $json->convert_blessed(1);
+
+  # This may seem a little backwards, but what this really means is
+  # "don't convert our utf8 into byte strings, just leave it as a
+  # utf8 string."
+  $json->utf8(0) if Bugzilla->params->{'utf8'};
+  return $json;
 }
 
 # Override the JSON::RPC method to return our CGI object instead of theirs.
 sub cgi { return Bugzilla->cgi; }
 
 sub response_header {
-    my $self = shift;
-    # The HTTP body needs to be bytes (not a utf8 string) for recent
-    # versions of HTTP::Message, but JSON::RPC::Server doesn't handle this
-    # properly. $_[1] is the HTTP body content we're going to be sending.
-    if (utf8::is_utf8($_[1])) {
-        utf8::encode($_[1]);
-        # Since we're going to just be sending raw bytes, we need to
-        # set STDOUT to not expect utf8.
-        disable_utf8();
-    }
-    return $self->SUPER::response_header(@_);
+  my $self = shift;
+
+  # The HTTP body needs to be bytes (not a utf8 string) for recent
+  # versions of HTTP::Message, but JSON::RPC::Server doesn't handle this
+  # properly. $_[1] is the HTTP body content we're going to be sending.
+  if (utf8::is_utf8($_[1])) {
+    utf8::encode($_[1]);
+
+    # Since we're going to just be sending raw bytes, we need to
+    # set STDOUT to not expect utf8.
+    disable_utf8();
+  }
+  return $self->SUPER::response_header(@_);
 }
 
 sub response {
-    my ($self, $response) = @_;
-    my $cgi = $self->cgi;
-
-    # Implement JSONP.
-    if (my $callback = $self->_bz_callback) {
-        my $content = $response->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.
-    # This fixes various problems, including sending Bugzilla's cookies
-    # properly.
-    my $headers = $response->headers;
-    my @header_args;
-    foreach my $name ($headers->header_field_names) {
-        my @values = $headers->header($name);
-        $name =~ s/-/_/g;
-        foreach my $value (@values) {
-            push(@header_args, "-$name", $value);
-        }
-    }
-
-    # ETag support
-    my $etag = $self->bz_etag;
-    if ($etag && $cgi->check_etag($etag)) {
-        push(@header_args, "-ETag", $etag);
-        print $cgi->header(-status => '304 Not Modified', @header_args);
-    }
-    else {
-        push(@header_args, "-ETag", $etag) if $etag;
-        print $cgi->header(-status => $response->code, @header_args);
-        print $response->content;
-    }
+  my ($self, $response) = @_;
+  my $cgi = $self->cgi;
+
+  # Implement JSONP.
+  if (my $callback = $self->_bz_callback) {
+    my $content = $response->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.
+  # This fixes various problems, including sending Bugzilla's cookies
+  # properly.
+  my $headers = $response->headers;
+  my @header_args;
+  foreach my $name ($headers->header_field_names) {
+    my @values = $headers->header($name);
+    $name =~ s/-/_/g;
+    foreach my $value (@values) {
+      push(@header_args, "-$name", $value);
+    }
+  }
+
+  # ETag support
+  my $etag = $self->bz_etag;
+  if ($etag && $cgi->check_etag($etag)) {
+    push(@header_args, "-ETag", $etag);
+    print $cgi->header(-status => '304 Not Modified', @header_args);
+  }
+  else {
+    push(@header_args, "-ETag", $etag) if $etag;
+    print $cgi->header(-status => $response->code, @header_args);
+    print $response->content;
+  }
 }
 
 # The JSON-RPC 1.1 GET specification is not so great--you can't specify
@@ -122,70 +127,69 @@ sub response {
 # Base64 encoded, because that is ridiculous and obnoxious for JavaScript
 # clients.
 sub retrieve_json_from_get {
-    my $self = shift;
-    my $cgi = $self->cgi;
-
-    my %input;
-
-    # Both version and id must be set before any errors are thrown.
-    if ($cgi->param('version')) {
-        $self->version(scalar $cgi->param('version'));
-        $input{version} = $cgi->param('version');
-    }
-    else {
-        $self->version('1.0');
-    }
-
-    # The JSON-RPC 2.0 spec says that any request that omits an id doesn't
-    # want a response. However, in an HTTP GET situation, it's stupid to
-    # expect all clients to specify some id parameter just to get a response,
-    # so we don't require it.
-    my $id;
-    if (defined $cgi->param('id')) {
-        $id = $cgi->param('id');
-    }
-    # However, JSON::RPC does require that an id exist in most cases, in
-    # order to throw proper errors. We use the installation's urlbase as
-    # the id, in this case.
-    else {
-        $id = correct_urlbase();
-    }
-    # Setting _bz_request_id here is required in case we throw errors early,
-    # before _handle.
-    $self->{_bz_request_id} = $input{id} = $id;
-
-    # _bz_callback can throw an error, so we have to set it here, after we're
-    # ready to throw errors.
-    $self->_bz_callback(scalar $cgi->param('callback'));
-
-    if (!$cgi->param('method')) {
-        ThrowUserError('json_rpc_get_method_required');
-    }
-    $input{method} = $cgi->param('method');
-
-    my $params;
-    if (defined $cgi->param('params')) {
-        local $@;
-        $params = eval { 
-            $self->json->decode(scalar $cgi->param('params')) 
-        };
-        if ($@) {
-            ThrowUserError('json_rpc_invalid_params',
-                           { params => scalar $cgi->param('params'),
-                             err_msg  => $@ });
-        }
-    }
-    elsif (!$self->version or $self->version ne '1.1') {
-        $params = [];
-    }
-    else {
-        $params = {};
-    }
-
-    $input{params} = $params;
-
-    my $json = $self->json->encode(\%input);
-    return $json;
+  my $self = shift;
+  my $cgi  = $self->cgi;
+
+  my %input;
+
+  # Both version and id must be set before any errors are thrown.
+  if ($cgi->param('version')) {
+    $self->version(scalar $cgi->param('version'));
+    $input{version} = $cgi->param('version');
+  }
+  else {
+    $self->version('1.0');
+  }
+
+  # The JSON-RPC 2.0 spec says that any request that omits an id doesn't
+  # want a response. However, in an HTTP GET situation, it's stupid to
+  # expect all clients to specify some id parameter just to get a response,
+  # so we don't require it.
+  my $id;
+  if (defined $cgi->param('id')) {
+    $id = $cgi->param('id');
+  }
+
+  # However, JSON::RPC does require that an id exist in most cases, in
+  # order to throw proper errors. We use the installation's urlbase as
+  # the id, in this case.
+  else {
+    $id = correct_urlbase();
+  }
+
+  # Setting _bz_request_id here is required in case we throw errors early,
+  # before _handle.
+  $self->{_bz_request_id} = $input{id} = $id;
+
+  # _bz_callback can throw an error, so we have to set it here, after we're
+  # ready to throw errors.
+  $self->_bz_callback(scalar $cgi->param('callback'));
+
+  if (!$cgi->param('method')) {
+    ThrowUserError('json_rpc_get_method_required');
+  }
+  $input{method} = $cgi->param('method');
+
+  my $params;
+  if (defined $cgi->param('params')) {
+    local $@;
+    $params = eval { $self->json->decode(scalar $cgi->param('params')) };
+    if ($@) {
+      ThrowUserError('json_rpc_invalid_params',
+        {params => scalar $cgi->param('params'), err_msg => $@});
+    }
+  }
+  elsif (!$self->version or $self->version ne '1.1') {
+    $params = [];
+  }
+  else {
+    $params = {};
+  }
+
+  $input{params} = $params;
+
+  my $json = $self->json->encode(\%input);
+  return $json;
 }
 
 #######################################
@@ -193,72 +197,76 @@ sub retrieve_json_from_get {
 #######################################
 
 sub type {
-    my ($self, $type, $value) = @_;
-    
-    # This is the only type that does something special with undef.
-    if ($type eq 'boolean') {
-        return $value ? JSON::true : JSON::false;
-    }
-    
-    return JSON::null if !defined $value;
-
-    my $retval = $value;
-
-    if ($type eq 'int') {
-        $retval = int($value);
-    }
-    if ($type eq 'double') {
-        $retval = 0.0 + $value;
-    }
-    elsif ($type eq 'string') {
-        # Forces string context, so that JSON will make it a string.
-        $retval = "$value";
-    }
-    elsif ($type eq 'dateTime') {
-        # ISO-8601 "YYYYMMDDTHH:MM:SS" with a literal T
-        $retval = $self->datetime_format_outbound($value);
-    }
-    elsif ($type eq 'base64') {
-        utf8::encode($value) if utf8::is_utf8($value);
-        $retval = encode_base64($value, '');
-    }
-    elsif ($type eq 'email' && Bugzilla->params->{'webservice_email_filter'}) {
-        $retval = email_filter($value);
-    }
-
-    return $retval;
+  my ($self, $type, $value) = @_;
+
+  # This is the only type that does something special with undef.
+  if ($type eq 'boolean') {
+    return $value ? JSON::true : JSON::false;
+  }
+
+  return JSON::null if !defined $value;
+
+  my $retval = $value;
+
+  if ($type eq 'int') {
+    $retval = int($value);
+  }
+  if ($type eq 'double') {
+    $retval = 0.0 + $value;
+  }
+  elsif ($type eq 'string') {
+
+    # Forces string context, so that JSON will make it a string.
+    $retval = "$value";
+  }
+  elsif ($type eq 'dateTime') {
+
+    # ISO-8601 "YYYYMMDDTHH:MM:SS" with a literal T
+    $retval = $self->datetime_format_outbound($value);
+  }
+  elsif ($type eq 'base64') {
+    utf8::encode($value) if utf8::is_utf8($value);
+    $retval = encode_base64($value, '');
+  }
+  elsif ($type eq 'email' && Bugzilla->params->{'webservice_email_filter'}) {
+    $retval = email_filter($value);
+  }
+
+  return $retval;
 }
 
 sub datetime_format_outbound {
-    my $self = shift;
-    # YUI expects ISO8601 in UTC time; including TZ specifier
-    return $self->SUPER::datetime_format_outbound(@_) . 'Z';
+  my $self = shift;
+
+  # YUI expects ISO8601 in UTC time; including TZ specifier
+  return $self->SUPER::datetime_format_outbound(@_) . 'Z';
 }
 
 sub handle_login {
-    my $self = shift;
-
-    # If we're being called using GET, we don't allow cookie-based or Env
-    # login, because GET requests can be done cross-domain, and we don't
-    # want private data showing up on another site unless the user
-    # explicitly gives that site their username and password. (This is
-    # particularly important for JSONP, which would allow a remote site
-    # to use private data without the user's knowledge, unless we had this
-    # protection in place.)
-    if ($self->request->method ne 'POST') {
-        # XXX There's no particularly good way for us to get a parameter
-        # to Bugzilla->login at this point, so we pass this information
-        # around using request_cache, which is a bit of a hack. The
-        # implementation of it is in Bugzilla::Auth::Login::Stack.
-        Bugzilla->request_cache->{auth_no_automatic_login} = 1;
-    }
-
-    my $path = $self->path_info;
-    my $class = $self->{dispatch_path}->{$path};
-    my $full_method = $self->_bz_method_name;
-    $full_method =~ /^\S+\.(\S+)/;
-    my $method = $1;
-    $self->SUPER::handle_login($class, $method, $full_method);
+  my $self = shift;
+
+  # If we're being called using GET, we don't allow cookie-based or Env
+  # login, because GET requests can be done cross-domain, and we don't
+  # want private data showing up on another site unless the user
+  # explicitly gives that site their username and password. (This is
+  # particularly important for JSONP, which would allow a remote site
+  # to use private data without the user's knowledge, unless we had this
+  # protection in place.)
+  if ($self->request->method ne 'POST') {
+
+    # XXX There's no particularly good way for us to get a parameter
+    # to Bugzilla->login at this point, so we pass this information
+    # around using request_cache, which is a bit of a hack. The
+    # implementation of it is in Bugzilla::Auth::Login::Stack.
+    Bugzilla->request_cache->{auth_no_automatic_login} = 1;
+  }
+
+  my $path        = $self->path_info;
+  my $class       = $self->{dispatch_path}->{$path};
+  my $full_method = $self->_bz_method_name;
+  $full_method =~ /^\S+\.(\S+)/;
+  my $method = $1;
+  $self->SUPER::handle_login($class, $method, $full_method);
 }
 
 ######################################
@@ -267,165 +275,165 @@ sub handle_login {
 
 # Store the ID of the current call, because Bugzilla::Error will need it.
 sub _handle {
-    my $self = shift;
-    my ($obj) = @_;
-    $self->{_bz_request_id} = $obj->{id};
+  my $self = shift;
+  my ($obj) = @_;
+  $self->{_bz_request_id} = $obj->{id};
 
-    my $result = $self->SUPER::_handle(@_);
+  my $result = $self->SUPER::_handle(@_);
 
-    # Set the ETag if not already set in the webservice methods.
-    my $etag = $self->bz_etag;
-    if (!$etag && ref $result) {
-        my $data = $self->json->decode($result)->{'result'};
-        $self->bz_etag($data);
-    }
+  # Set the ETag if not already set in the webservice methods.
+  my $etag = $self->bz_etag;
+  if (!$etag && ref $result) {
+    my $data = $self->json->decode($result)->{'result'};
+    $self->bz_etag($data);
+  }
 
-    return $result;
+  return $result;
 }
 
 # Make all error messages returned by JSON::RPC go into the 100000
 # range, and bring down all our errors into the normal range.
 sub _error {
-    my ($self, $id, $code) = (shift, shift, shift);
-    # All JSON::RPC errors are less than 1000.
-    if ($code < 1000) {
-        $code += 100000;
-    }
-    # Bugzilla::Error adds 100,000 to all *our* errors, so
-    # we know they came from us.
-    elsif ($code > 100000) {
-        $code -= 100000;
-    }
-
-    # We can't just set $_[1] because it's not always settable,
-    # in JSON::RPC::Server.
-    unshift(@_, $id, $code);
-    my $json = $self->SUPER::_error(@_);
-
-    # We want to always send the JSON-RPC 1.1 error format, although
-    # If we're not in JSON-RPC 1.1, we don't need the silly "name" parameter.
-    if (!$self->version or $self->version ne '1.1') {
-        my $object = $self->json->decode($json);
-        my $message = $object->{error};
-        # Just assure that future versions of JSON::RPC don't change the
-        # JSON-RPC 1.0 error format.
-        if (!ref $message) {
-            $object->{error} = {
-                code    => $code,
-                message => $message,
-            };
-            $json = $self->json->encode($object);
-        }
-    }
-    return $json;
+  my ($self, $id, $code) = (shift, shift, shift);
+
+  # All JSON::RPC errors are less than 1000.
+  if ($code < 1000) {
+    $code += 100000;
+  }
+
+  # Bugzilla::Error adds 100,000 to all *our* errors, so
+  # we know they came from us.
+  elsif ($code > 100000) {
+    $code -= 100000;
+  }
+
+  # We can't just set $_[1] because it's not always settable,
+  # in JSON::RPC::Server.
+  unshift(@_, $id, $code);
+  my $json = $self->SUPER::_error(@_);
+
+  # We want to always send the JSON-RPC 1.1 error format, although
+  # If we're not in JSON-RPC 1.1, we don't need the silly "name" parameter.
+  if (!$self->version or $self->version ne '1.1') {
+    my $object  = $self->json->decode($json);
+    my $message = $object->{error};
+
+    # Just assure that future versions of JSON::RPC don't change the
+    # JSON-RPC 1.0 error format.
+    if (!ref $message) {
+      $object->{error} = {code => $code, message => $message,};
+      $json = $self->json->encode($object);
+    }
+  }
+  return $json;
 }
 
 # This handles dispatching our calls to the appropriate class based on
 # the name of the method.
 sub _find_procedure {
-    my $self = shift;
+  my $self = shift;
 
-    my $method = shift;
-    $self->{_bz_method_name} = $method;
+  my $method = shift;
+  $self->{_bz_method_name} = $method;
 
-    # This tricks SUPER::_find_procedure into finding the right class.
-    $method =~ /^(\S+)\.(\S+)$/;
-    $self->path_info($1);
-    unshift(@_, $2);
+  # This tricks SUPER::_find_procedure into finding the right class.
+  $method =~ /^(\S+)\.(\S+)$/;
+  $self->path_info($1);
+  unshift(@_, $2);
 
-    return $self->SUPER::_find_procedure(@_);
+  return $self->SUPER::_find_procedure(@_);
 }
 
 # This is a hacky way to do something right before methods are called.
 # This is the last thing that JSON::RPC::Server::_handle calls right before
 # the method is actually called.
 sub _argument_type_check {
-    my $self = shift;
-    my $params = $self->SUPER::_argument_type_check(@_);
-
-    # JSON-RPC 1.0 requires all parameters to be passed as an array, so
-    # we just pull out the first item and assume it's an object.
-    my $params_is_array;
-    if (ref $params eq 'ARRAY') {
-        $params = $params->[0];
-        $params_is_array = 1;
-    }
-
-    taint_data($params);
-
-    # Now, convert dateTime fields on input.
-    $self->_bz_method_name =~ /^(\S+)\.(\S+)$/;
-    my ($class, $method) = ($1, $2);
-    my $pkg = $self->{dispatch_path}->{$class};
-    my @date_fields = @{ $pkg->DATE_FIELDS->{$method} || [] };
-    foreach my $field (@date_fields) {
-        if (defined $params->{$field}) {
-            my $value = $params->{$field};
-            if (ref $value eq 'ARRAY') {
-                $params->{$field} = 
-                    [ map { $self->datetime_format_inbound($_) } @$value ];
-            }
-            else {
-                $params->{$field} = $self->datetime_format_inbound($value);
-            }
-        }
-    }
-    my @base64_fields = @{ $pkg->BASE64_FIELDS->{$method} || [] };
-    foreach my $field (@base64_fields) {
-        if (defined $params->{$field}) {
-            $params->{$field} = decode_base64($params->{$field});
-        }
-    }
-
-    # 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') {
-        # CSRF is possible via XMLHttpRequest when the Content-Type header
-        # is not application/json (for example: text/plain or
-        # application/x-www-form-urlencoded).
-        # application/json is the single official MIME type, per RFC 4627.
-        my $content_type = $self->cgi->content_type;
-        # The charset can be appended to the content type, so we use a regexp.
-        if ($content_type !~ m{^application/json(-rpc)?(;.*)?$}i) {
-            ThrowUserError('json_rpc_illegal_content_type',
-                            { content_type => $content_type });
-        }
-    }
-    else {
-        # When being called using GET, we don't allow calling
-        # methods that can change data. This protects us against cross-site
-        # request forgeries.
-        if (!grep($_ eq $method, $pkg->READ_ONLY)) {
-            ThrowUserError('json_rpc_post_only', 
-                           { method => $self->_bz_method_name });
-        }
-    }
-
-    # 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();
-
-    # Bugzilla::WebService packages call internal methods like
-    # $self->_some_private_method. So we have to inherit from 
-    # that class as well as this Server class.
-    my $new_class = ref($self) . '::' . $pkg;
-    my $isa_string = 'our @ISA = qw(' . ref($self) . " $pkg)";
-    eval "package $new_class;$isa_string;";
-    bless $self, $new_class;
-
-    if ($params_is_array) {
-        $params = [$params];
-    }
-
-    return $params;
+  my $self   = shift;
+  my $params = $self->SUPER::_argument_type_check(@_);
+
+  # JSON-RPC 1.0 requires all parameters to be passed as an array, so
+  # we just pull out the first item and assume it's an object.
+  my $params_is_array;
+  if (ref $params eq 'ARRAY') {
+    $params          = $params->[0];
+    $params_is_array = 1;
+  }
+
+  taint_data($params);
+
+  # Now, convert dateTime fields on input.
+  $self->_bz_method_name =~ /^(\S+)\.(\S+)$/;
+  my ($class, $method) = ($1, $2);
+  my $pkg = $self->{dispatch_path}->{$class};
+  my @date_fields = @{$pkg->DATE_FIELDS->{$method} || []};
+  foreach my $field (@date_fields) {
+    if (defined $params->{$field}) {
+      my $value = $params->{$field};
+      if (ref $value eq 'ARRAY') {
+        $params->{$field} = [map { $self->datetime_format_inbound($_) } @$value];
+      }
+      else {
+        $params->{$field} = $self->datetime_format_inbound($value);
+      }
+    }
+  }
+  my @base64_fields = @{$pkg->BASE64_FIELDS->{$method} || []};
+  foreach my $field (@base64_fields) {
+    if (defined $params->{$field}) {
+      $params->{$field} = decode_base64($params->{$field});
+    }
+  }
+
+  # 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') {
+
+    # CSRF is possible via XMLHttpRequest when the Content-Type header
+    # is not application/json (for example: text/plain or
+    # application/x-www-form-urlencoded).
+    # application/json is the single official MIME type, per RFC 4627.
+    my $content_type = $self->cgi->content_type;
+
+    # The charset can be appended to the content type, so we use a regexp.
+    if ($content_type !~ m{^application/json(-rpc)?(;.*)?$}i) {
+      ThrowUserError('json_rpc_illegal_content_type',
+        {content_type => $content_type});
+    }
+  }
+  else {
+    # When being called using GET, we don't allow calling
+    # methods that can change data. This protects us against cross-site
+    # request forgeries.
+    if (!grep($_ eq $method, $pkg->READ_ONLY)) {
+      ThrowUserError('json_rpc_post_only', {method => $self->_bz_method_name});
+    }
+  }
+
+  # 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();
+
+  # Bugzilla::WebService packages call internal methods like
+  # $self->_some_private_method. So we have to inherit from
+  # that class as well as this Server class.
+  my $new_class  = ref($self) . '::' . $pkg;
+  my $isa_string = 'our @ISA = qw(' . ref($self) . " $pkg)";
+  eval "package $new_class;$isa_string;";
+  bless $self, $new_class;
+
+  if ($params_is_array) {
+    $params = [$params];
+  }
+
+  return $params;
 }
 
 ##########################
@@ -434,22 +442,24 @@ sub _argument_type_check {
 
 # _bz_method_name is stored by _find_procedure for later use.
 sub _bz_method_name {
-    return $_[0]->{_bz_method_name}; 
+  return $_[0]->{_bz_method_name};
 }
 
 sub _bz_callback {
-    my ($self, $value) = @_;
-    if (defined $value) {
-        $value = trim($value);
-        # We don't use \w because we don't want to allow Unicode here.
-        if ($value !~ /^[A-Za-z0-9_\.\[\]]+$/) {
-            ThrowUserError('json_rpc_invalid_callback', { callback => $value });
-        }
-        $self->{_bz_callback} = $value;
-        # JSONP needs to be parsed by a JS parser, not by a JSON parser.
-        $self->content_type('text/javascript');
+  my ($self, $value) = @_;
+  if (defined $value) {
+    $value = trim($value);
+
+    # We don't use \w because we don't want to allow Unicode here.
+    if ($value !~ /^[A-Za-z0-9_\.\[\]]+$/) {
+      ThrowUserError('json_rpc_invalid_callback', {callback => $value});
     }
-    return $self->{_bz_callback};
+    $self->{_bz_callback} = $value;
+
+    # JSONP needs to be parsed by a JS parser, not by a JSON parser.
+    $self->content_type('text/javascript');
+  }
+  return $self->{_bz_callback};
 }
 
 1;
diff --git a/Bugzilla/WebService/Server/REST.pm b/Bugzilla/WebService/Server/REST.pm
index 8450a7a28..8108e1d4f 100644
--- a/Bugzilla/WebService/Server/REST.pm
+++ b/Bugzilla/WebService/Server/REST.pm
@@ -40,134 +40,134 @@ use MIME::Base64 qw(decode_base64);
 ###########################
 
 sub handle {
-    my ($self)  = @_;
-
-    # Determine how the data should be represented. We do this early so
-    # errors will also be returned with the proper content type.
-    # If no accept header was sent or the content types specified were not
-    # matched, we default to the first type in the whitelist.
-    $self->content_type($self->_best_content_type(REST_CONTENT_TYPE_WHITELIST()));
-
-    # Using current path information, decide which class/method to
-    # use to serve the request. Throw error if no resource was found
-    # unless we were looking for OPTIONS
-    if (!$self->_find_resource($self->cgi->path_info)) {
-        if ($self->request->method eq 'OPTIONS'
-            && $self->bz_rest_options)
-        {
-            my $response = $self->response_header(STATUS_OK, "");
-            my $options_string = join(', ', @{ $self->bz_rest_options });
-            $response->header('Allow' => $options_string,
-                              'Access-Control-Allow-Methods' => $options_string);
-            return $self->response($response);
-        }
-
-        ThrowUserError("rest_invalid_resource",
-                       { path   => $self->cgi->path_info,
-                         method => $self->request->method });
+  my ($self) = @_;
+
+  # Determine how the data should be represented. We do this early so
+  # errors will also be returned with the proper content type.
+  # If no accept header was sent or the content types specified were not
+  # matched, we default to the first type in the whitelist.
+  $self->content_type($self->_best_content_type(REST_CONTENT_TYPE_WHITELIST()));
+
+  # Using current path information, decide which class/method to
+  # use to serve the request. Throw error if no resource was found
+  # unless we were looking for OPTIONS
+  if (!$self->_find_resource($self->cgi->path_info)) {
+    if ($self->request->method eq 'OPTIONS' && $self->bz_rest_options) {
+      my $response = $self->response_header(STATUS_OK, "");
+      my $options_string = join(', ', @{$self->bz_rest_options});
+      $response->header(
+        'Allow'                        => $options_string,
+        'Access-Control-Allow-Methods' => $options_string
+      );
+      return $self->response($response);
     }
 
-    # Dispatch to the proper module
-    my $class  = $self->bz_class_name;
-    my ($path) = $class =~ /::([^:]+)$/;
-    $self->path_info($path);
-    delete $self->{dispatch_path};
-    $self->dispatch({ $path => $class });
+    ThrowUserError("rest_invalid_resource",
+      {path => $self->cgi->path_info, method => $self->request->method});
+  }
 
-    my $params = $self->_retrieve_json_params;
+  # Dispatch to the proper module
+  my $class = $self->bz_class_name;
+  my ($path) = $class =~ /::([^:]+)$/;
+  $self->path_info($path);
+  delete $self->{dispatch_path};
+  $self->dispatch({$path => $class});
 
-    fix_credentials($params);
+  my $params = $self->_retrieve_json_params;
 
-    # Fix includes/excludes for each call
-    rest_include_exclude($params);
+  fix_credentials($params);
 
-    # Set callback name if exists
-    $self->_bz_callback($params->{'callback'}) if $params->{'callback'};
+  # Fix includes/excludes for each call
+  rest_include_exclude($params);
 
-    Bugzilla->input_params($params);
+  # Set callback name if exists
+  $self->_bz_callback($params->{'callback'}) if $params->{'callback'};
 
-    # Set the JSON version to 1.1 and the id to the current urlbase
-    # also set up the correct handler method
-    my $obj = {
-        version => '1.1',
-        id      => correct_urlbase(),
-        method  => $self->bz_method_name,
-        params  => $params
-    };
+  Bugzilla->input_params($params);
 
-    # Execute the handler
-    my $result = $self->_handle($obj);
+  # Set the JSON version to 1.1 and the id to the current urlbase
+  # also set up the correct handler method
+  my $obj = {
+    version => '1.1',
+    id      => correct_urlbase(),
+    method  => $self->bz_method_name,
+    params  => $params
+  };
 
-    if (!$self->error_response_header) {
-        return $self->response(
-            $self->response_header($self->bz_success_code || STATUS_OK, $result));
-    }
+  # Execute the handler
+  my $result = $self->_handle($obj);
+
+  if (!$self->error_response_header) {
+    return $self->response(
+      $self->response_header($self->bz_success_code || STATUS_OK, $result));
+  }
 
-    $self->response($self->error_response_header);
+  $self->response($self->error_response_header);
 }
 
 sub response {
-    my ($self, $response) = @_;
-
-    # If we have thrown an error, the 'error' key will exist
-    # otherwise we use 'result'. JSONRPC returns other data
-    # along with the result/error such as version and id which
-    # we will strip off for REST calls.
-    my $content = $response->content;
-    my $json_data = {};
-    if ($content) {
-        $json_data = $self->json->decode($content);
-    }
-
-    my $result = {};
-    if (exists $json_data->{error}) {
-        $result = $json_data->{error};
-        $result->{error} = $self->type('boolean', 1);
-        $result->{documentation} = REST_DOC;
-        delete $result->{'name'}; # Remove JSONRPCError
-    }
-    elsif (exists $json_data->{result}) {
-        $result = $json_data->{result};
-    }
-
-    # The result needs to be a valid JSON data structure
-    # and not a undefined or scalar value.
-    if (!ref $result
-        || blessed($result)
-        || (ref $result ne 'HASH' && ref $result ne 'ARRAY'))
-    {
-        $result = { result => $result };
-    }
-
-    Bugzilla::Hook::process('webservice_rest_response',
-        { rpc => $self, result => \$result, response => $response });
-
-    # Access Control
-    $response->header("Access-Control-Allow-Origin", "*");
-    $response->header("Access-Control-Allow-Headers", "origin, content-type, accept, x-requested-with");
-
-    # ETag support
-    my $etag = $self->bz_etag;
-    $self->bz_etag($result) if !$etag;
-
-    # If accessing through web browser, then display in readable format
-    if ($self->content_type eq 'text/html') {
-        $result = $self->json->pretty->canonical->allow_nonref->encode($result);
-
-        my $template = Bugzilla->template;
-        $content = "";
-        $template->process("rest.html.tmpl", { result => $result }, \$content)
-            || ThrowTemplateError($template->error());
-
-        $response->content_type('text/html');
-    }
-    else {
-        $content = $self->json->encode($result);
-    }
-
-    $response->content($content);
-
-    $self->SUPER::response($response);
+  my ($self, $response) = @_;
+
+  # If we have thrown an error, the 'error' key will exist
+  # otherwise we use 'result'. JSONRPC returns other data
+  # along with the result/error such as version and id which
+  # we will strip off for REST calls.
+  my $content   = $response->content;
+  my $json_data = {};
+  if ($content) {
+    $json_data = $self->json->decode($content);
+  }
+
+  my $result = {};
+  if (exists $json_data->{error}) {
+    $result = $json_data->{error};
+    $result->{error} = $self->type('boolean', 1);
+    $result->{documentation} = REST_DOC;
+    delete $result->{'name'};    # Remove JSONRPCError
+  }
+  elsif (exists $json_data->{result}) {
+    $result = $json_data->{result};
+  }
+
+  # The result needs to be a valid JSON data structure
+  # and not a undefined or scalar value.
+  if ( !ref $result
+    || blessed($result)
+    || (ref $result ne 'HASH' && ref $result ne 'ARRAY'))
+  {
+    $result = {result => $result};
+  }
+
+  Bugzilla::Hook::process('webservice_rest_response',
+    {rpc => $self, result => \$result, response => $response});
+
+  # Access Control
+  $response->header("Access-Control-Allow-Origin", "*");
+  $response->header("Access-Control-Allow-Headers",
+    "origin, content-type, accept, x-requested-with");
+
+  # ETag support
+  my $etag = $self->bz_etag;
+  $self->bz_etag($result) if !$etag;
+
+  # If accessing through web browser, then display in readable format
+  if ($self->content_type eq 'text/html') {
+    $result = $self->json->pretty->canonical->allow_nonref->encode($result);
+
+    my $template = Bugzilla->template;
+    $content = "";
+    $template->process("rest.html.tmpl", {result => $result}, \$content)
+      || ThrowTemplateError($template->error());
+
+    $response->content_type('text/html');
+  }
+  else {
+    $content = $self->json->encode($result);
+  }
+
+  $response->content($content);
+
+  $self->SUPER::response($response);
 }
 
 #######################################
@@ -175,36 +175,40 @@ sub response {
 #######################################
 
 sub handle_login {
-    my $self = shift;
-
-    # If we're being called using GET, we don't allow cookie-based or Env
-    # login, because GET requests can be done cross-domain, and we don't
-    # want private data showing up on another site unless the user
-    # explicitly gives that site their username and password. (This is
-    # particularly important for JSONP, which would allow a remote site
-    # to use private data without the user's knowledge, unless we had this
-    # protection in place.) We do allow this for GET /login as we need to
-    # for Bugzilla::Auth::Persist::Cookie to create a login cookie that we
-    # can also use for Bugzilla_token support. This is OK as it requires
-    # a login and password to be supplied and will fail if they are not
-    # valid for the user.
-    if (!grep($_ eq $self->request->method, ('POST', 'PUT'))
-        && !($self->bz_class_name eq 'Bugzilla::WebService::User'
-            && $self->bz_method_name eq 'login'))
-    {
-        # XXX There's no particularly good way for us to get a parameter
-        # to Bugzilla->login at this point, so we pass this information
-        # around using request_cache, which is a bit of a hack. The
-        # implementation of it is in Bugzilla::Auth::Login::Stack.
-        Bugzilla->request_cache->{'auth_no_automatic_login'} = 1;
-    }
-
-    my $class = $self->bz_class_name;
-    my $method = $self->bz_method_name;
-    my $full_method = $class . "." . $method;
-
-    # Bypass JSONRPC::handle_login
-    Bugzilla::WebService::Server->handle_login($class, $method, $full_method);
+  my $self = shift;
+
+  # If we're being called using GET, we don't allow cookie-based or Env
+  # login, because GET requests can be done cross-domain, and we don't
+  # want private data showing up on another site unless the user
+  # explicitly gives that site their username and password. (This is
+  # particularly important for JSONP, which would allow a remote site
+  # to use private data without the user's knowledge, unless we had this
+  # protection in place.) We do allow this for GET /login as we need to
+  # for Bugzilla::Auth::Persist::Cookie to create a login cookie that we
+  # can also use for Bugzilla_token support. This is OK as it requires
+  # a login and password to be supplied and will fail if they are not
+  # valid for the user.
+  if (
+    !grep($_ eq $self->request->method, ('POST', 'PUT'))
+    && !(
+         $self->bz_class_name eq 'Bugzilla::WebService::User'
+      && $self->bz_method_name eq 'login'
+    )
+    )
+  {
+    # XXX There's no particularly good way for us to get a parameter
+    # to Bugzilla->login at this point, so we pass this information
+    # around using request_cache, which is a bit of a hack. The
+    # implementation of it is in Bugzilla::Auth::Login::Stack.
+    Bugzilla->request_cache->{'auth_no_automatic_login'} = 1;
+  }
+
+  my $class       = $self->bz_class_name;
+  my $method      = $self->bz_method_name;
+  my $full_method = $class . "." . $method;
+
+  # Bypass JSONRPC::handle_login
+  Bugzilla::WebService::Server->handle_login($class, $method, $full_method);
 }
 
 ############################
@@ -214,79 +218,78 @@ sub handle_login {
 # We do not want to run Bugzilla::WebService::Server::JSONRPC->_find_prodedure
 # as it determines the method name differently.
 sub _find_procedure {
-    my $self = shift;
-    if ($self->isa('JSON::RPC::Server::CGI')) {
-        return JSON::RPC::Server::_find_procedure($self, @_);
-    }
-    else {
-        return JSON::RPC::Legacy::Server::_find_procedure($self, @_);
-    }
+  my $self = shift;
+  if ($self->isa('JSON::RPC::Server::CGI')) {
+    return JSON::RPC::Server::_find_procedure($self, @_);
+  }
+  else {
+    return JSON::RPC::Legacy::Server::_find_procedure($self, @_);
+  }
 }
 
 sub _argument_type_check {
-    my $self = shift;
-    my $params;
-
-    if ($self->isa('JSON::RPC::Server::CGI')) {
-        $params = JSON::RPC::Server::_argument_type_check($self, @_);
+  my $self = shift;
+  my $params;
+
+  if ($self->isa('JSON::RPC::Server::CGI')) {
+    $params = JSON::RPC::Server::_argument_type_check($self, @_);
+  }
+  else {
+    $params = JSON::RPC::Legacy::Server::_argument_type_check($self, @_);
+  }
+
+  # JSON-RPC 1.0 requires all parameters to be passed as an array, so
+  # we just pull out the first item and assume it's an object.
+  my $params_is_array;
+  if (ref $params eq 'ARRAY') {
+    $params          = $params->[0];
+    $params_is_array = 1;
+  }
+
+  taint_data($params);
+
+  # Now, convert dateTime fields on input.
+  my $method      = $self->bz_method_name;
+  my $pkg         = $self->{dispatch_path}->{$self->path_info};
+  my @date_fields = @{$pkg->DATE_FIELDS->{$method} || []};
+  foreach my $field (@date_fields) {
+    if (defined $params->{$field}) {
+      my $value = $params->{$field};
+      if (ref $value eq 'ARRAY') {
+        $params->{$field} = [map { $self->datetime_format_inbound($_) } @$value];
+      }
+      else {
+        $params->{$field} = $self->datetime_format_inbound($value);
+      }
     }
-    else {
-        $params = JSON::RPC::Legacy::Server::_argument_type_check($self, @_);
+  }
+  my @base64_fields = @{$pkg->BASE64_FIELDS->{$method} || []};
+  foreach my $field (@base64_fields) {
+    if (defined $params->{$field}) {
+      $params->{$field} = decode_base64($params->{$field});
     }
+  }
 
-    # JSON-RPC 1.0 requires all parameters to be passed as an array, so
-    # we just pull out the first item and assume it's an object.
-    my $params_is_array;
-    if (ref $params eq 'ARRAY') {
-        $params = $params->[0];
-        $params_is_array = 1;
-    }
+  # This is the best time to do login checks.
+  $self->handle_login();
 
-    taint_data($params);
-
-    # Now, convert dateTime fields on input.
-    my $method = $self->bz_method_name;
-    my $pkg = $self->{dispatch_path}->{$self->path_info};
-    my @date_fields = @{ $pkg->DATE_FIELDS->{$method} || [] };
-    foreach my $field (@date_fields) {
-        if (defined $params->{$field}) {
-            my $value = $params->{$field};
-            if (ref $value eq 'ARRAY') {
-                $params->{$field} =
-                    [ map { $self->datetime_format_inbound($_) } @$value ];
-            }
-            else {
-                $params->{$field} = $self->datetime_format_inbound($value);
-            }
-        }
-    }
-    my @base64_fields = @{ $pkg->BASE64_FIELDS->{$method} || [] };
-    foreach my $field (@base64_fields) {
-        if (defined $params->{$field}) {
-            $params->{$field} = decode_base64($params->{$field});
-        }
-    }
+  # Bugzilla::WebService packages call internal methods like
+  # $self->_some_private_method. So we have to inherit from
+  # that class as well as this Server class.
+  my $new_class  = ref($self) . '::' . $pkg;
+  my $isa_string = 'our @ISA = qw(' . ref($self) . " $pkg)";
+  eval "package $new_class;$isa_string;";
+  bless $self, $new_class;
 
-    # This is the best time to do login checks.
-    $self->handle_login();
+  # Allow extensions to modify the params post login
+  Bugzilla::Hook::process('webservice_rest_request',
+    {rpc => $self, params => $params});
 
-    # Bugzilla::WebService packages call internal methods like
-    # $self->_some_private_method. So we have to inherit from
-    # that class as well as this Server class.
-    my $new_class = ref($self) . '::' . $pkg;
-    my $isa_string = 'our @ISA = qw(' . ref($self) . " $pkg)";
-    eval "package $new_class;$isa_string;";
-    bless $self, $new_class;
+  if ($params_is_array) {
+    $params = [$params];
+  }
 
-    # Allow extensions to modify the params post login
-    Bugzilla::Hook::process('webservice_rest_request',
-                            { rpc => $self, params => $params });
-
-    if ($params_is_array) {
-        $params = [$params];
-    }
-
-    return $params;
+  return $params;
 }
 
 ###################
@@ -294,46 +297,46 @@ sub _argument_type_check {
 ###################
 
 sub bz_method_name {
-    my ($self, $method) = @_;
-    $self->{_bz_method_name} = $method if $method;
-    return $self->{_bz_method_name};
+  my ($self, $method) = @_;
+  $self->{_bz_method_name} = $method if $method;
+  return $self->{_bz_method_name};
 }
 
 sub bz_class_name {
-    my ($self, $class) = @_;
-    $self->{_bz_class_name} = $class if $class;
-    return $self->{_bz_class_name};
+  my ($self, $class) = @_;
+  $self->{_bz_class_name} = $class if $class;
+  return $self->{_bz_class_name};
 }
 
 sub bz_success_code {
-    my ($self, $value) = @_;
-    $self->{_bz_success_code} = $value if $value;
-    return $self->{_bz_success_code};
+  my ($self, $value) = @_;
+  $self->{_bz_success_code} = $value if $value;
+  return $self->{_bz_success_code};
 }
 
 sub bz_rest_params {
-    my ($self, $params) = @_;
-    $self->{_bz_rest_params} = $params if $params;
-    return $self->{_bz_rest_params};
+  my ($self, $params) = @_;
+  $self->{_bz_rest_params} = $params if $params;
+  return $self->{_bz_rest_params};
 }
 
 sub bz_rest_options {
-    my ($self, $options) = @_;
-    $self->{_bz_rest_options} = $options if $options;
-    return $self->{_bz_rest_options};
+  my ($self, $options) = @_;
+  $self->{_bz_rest_options} = $options if $options;
+  return $self->{_bz_rest_options};
 }
 
 sub rest_include_exclude {
-    my ($params) = @_;
+  my ($params) = @_;
 
-    if ($params->{'include_fields'} && !ref $params->{'include_fields'}) {
-        $params->{'include_fields'} = [ split(/[\s+,]/, $params->{'include_fields'}) ];
-    }
-    if ($params->{'exclude_fields'} && !ref $params->{'exclude_fields'}) {
-        $params->{'exclude_fields'} = [ split(/[\s+,]/, $params->{'exclude_fields'}) ];
-    }
+  if ($params->{'include_fields'} && !ref $params->{'include_fields'}) {
+    $params->{'include_fields'} = [split(/[\s+,]/, $params->{'include_fields'})];
+  }
+  if ($params->{'exclude_fields'} && !ref $params->{'exclude_fields'}) {
+    $params->{'exclude_fields'} = [split(/[\s+,]/, $params->{'exclude_fields'})];
+  }
 
-    return $params;
+  return $params;
 }
 
 ##########################
@@ -341,184 +344,191 @@ sub rest_include_exclude {
 ##########################
 
 sub _retrieve_json_params {
-    my $self = shift;
-
-    # Make a copy of the current input_params rather than edit directly
-    my $params = {};
-    %{$params} = %{ Bugzilla->input_params };
-
-    # First add any parameters we were able to pull out of the path
-    # based on the resource regexp and combine with the normal URL
-    # parameters.
-    if (my $rest_params = $self->bz_rest_params) {
-        foreach my $param (keys %$rest_params) {
-            # If the param does not already exist or if the
-            # rest param is a single value, add it to the
-            # global params.
-            if (!exists $params->{$param} || !ref $rest_params->{$param}) {
-                $params->{$param} = $rest_params->{$param};
-            }
-            # If rest_param is a list then add any extra values to the list
-            elsif (ref $rest_params->{$param}) {
-                my @extra_values = ref $params->{$param}
-                                   ? @{ $params->{$param} }
-                                   : ($params->{$param});
-                $params->{$param}
-                    = [ uniq (@{ $rest_params->{$param} }, @extra_values) ];
-            }
-        }
+  my $self = shift;
+
+  # Make a copy of the current input_params rather than edit directly
+  my $params = {};
+  %{$params} = %{Bugzilla->input_params};
+
+  # First add any parameters we were able to pull out of the path
+  # based on the resource regexp and combine with the normal URL
+  # parameters.
+  if (my $rest_params = $self->bz_rest_params) {
+    foreach my $param (keys %$rest_params) {
+
+      # If the param does not already exist or if the
+      # rest param is a single value, add it to the
+      # global params.
+      if (!exists $params->{$param} || !ref $rest_params->{$param}) {
+        $params->{$param} = $rest_params->{$param};
+      }
+
+      # If rest_param is a list then add any extra values to the list
+      elsif (ref $rest_params->{$param}) {
+        my @extra_values
+          = ref $params->{$param} ? @{$params->{$param}} : ($params->{$param});
+        $params->{$param} = [uniq(@{$rest_params->{$param}}, @extra_values)];
+      }
+    }
+  }
+
+  # Any parameters passed in in the body of a non-GET request will override
+  # any parameters pull from the url path. Otherwise non-unique keys are
+  # combined.
+  if ($self->request->method ne 'GET') {
+    my $extra_params = {};
+
+    # We do this manually because CGI.pm doesn't understand JSON strings.
+    my $json = delete $params->{'POSTDATA'} || delete $params->{'PUTDATA'};
+    if ($json) {
+      eval { $extra_params = $self->json->decode($json); };
+      if ($@) {
+        ThrowUserError('json_rpc_invalid_params', {err_msg => $@});
+      }
     }
 
-    # Any parameters passed in in the body of a non-GET request will override
-    # any parameters pull from the url path. Otherwise non-unique keys are
-    # combined.
-    if ($self->request->method ne 'GET') {
-        my $extra_params = {};
-        # We do this manually because CGI.pm doesn't understand JSON strings.
-        my $json = delete $params->{'POSTDATA'} || delete $params->{'PUTDATA'};
-        if ($json) {
-            eval { $extra_params = $self->json->decode($json); };
-            if ($@) {
-                ThrowUserError('json_rpc_invalid_params', { err_msg  => $@ });
-            }
-        }
-
-        # Allow parameters in the query string if request was non-GET.
-        # Note: parameters in query string body override any matching
-        # parameters in the request body.
-        foreach my $param ($self->cgi->url_param()) {
-            $extra_params->{$param} = $self->cgi->url_param($param);
-        }
-
-        %{$params} = (%{$params}, %{$extra_params}) if %{$extra_params};
+    # Allow parameters in the query string if request was non-GET.
+    # Note: parameters in query string body override any matching
+    # parameters in the request body.
+    foreach my $param ($self->cgi->url_param()) {
+      $extra_params->{$param} = $self->cgi->url_param($param);
     }
 
-    return $params;
+    %{$params} = (%{$params}, %{$extra_params}) if %{$extra_params};
+  }
+
+  return $params;
 }
 
 sub _find_resource {
-    my ($self, $path) = @_;
-
-    # Load in the WebService module from the dispatch map and then call
-    # $module->rest_resources to get the resources array ref.
-    my $resources = {};
-    foreach my $module (values %{ $self->{dispatch_path} }) {
-        eval("require $module") || die $@;
-        next if !$module->can('rest_resources');
-        $resources->{$module} = $module->rest_resources;
-    }
-
-    Bugzilla::Hook::process('webservice_rest_resources',
-                            { rpc => $self, resources => $resources });
-
-    # Use the resources hash from each module loaded earlier to determine
-    # which handler to use based on a regex match of the CGI path.
-    # Also any matches found in the regex will be passed in later to the
-    # handler for possible use.
-    my $request_method = $self->request->method;
-
-    my (@matches, $handler_found, $handler_method, $handler_class);
-    foreach my $class (keys %{ $resources }) {
-        # The resource data for each module needs to be
-        # an array ref with an even number of elements
-        # to work correctly.
-        next if (ref $resources->{$class} ne 'ARRAY'
-                 || scalar @{ $resources->{$class} } % 2 != 0);
-
-        while (my $regex = shift @{ $resources->{$class} }) {
-            my $options_data = shift @{ $resources->{$class} };
-            next if ref $options_data ne 'HASH';
-
-            if (@matches = ($path =~ $regex)) {
-                # If a specific path is accompanied by a OPTIONS request
-                # method, the user is asking for a list of possible request
-                # methods for a specific path.
-                $self->bz_rest_options([ keys %{ $options_data } ]);
-
-                if ($options_data->{$request_method}) {
-                    my $resource_data = $options_data->{$request_method};
-                    $self->bz_class_name($class);
-
-                    # The method key/value can be a simple scalar method name
-                    # or a anonymous subroutine so we execute it here.
-                    my $method = ref $resource_data->{method} eq 'CODE'
-                                 ? $resource_data->{method}->($self)
-                                 : $resource_data->{method};
-                    $self->bz_method_name($method);
-
-                    # Pull out any parameters parsed from the URL path
-                    # and store them for use by the method.
-                    if ($resource_data->{params}) {
-                        $self->bz_rest_params($resource_data->{params}->(@matches));
-                    }
-
-                    # If a special success code is needed for this particular
-                    # method, then store it for later when generating response.
-                    if ($resource_data->{success_code}) {
-                        $self->bz_success_code($resource_data->{success_code});
-                    }
-                    $handler_found = 1;
-                }
-            }
-            last if $handler_found;
+  my ($self, $path) = @_;
+
+  # Load in the WebService module from the dispatch map and then call
+  # $module->rest_resources to get the resources array ref.
+  my $resources = {};
+  foreach my $module (values %{$self->{dispatch_path}}) {
+    eval("require $module") || die $@;
+    next if !$module->can('rest_resources');
+    $resources->{$module} = $module->rest_resources;
+  }
+
+  Bugzilla::Hook::process('webservice_rest_resources',
+    {rpc => $self, resources => $resources});
+
+  # Use the resources hash from each module loaded earlier to determine
+  # which handler to use based on a regex match of the CGI path.
+  # Also any matches found in the regex will be passed in later to the
+  # handler for possible use.
+  my $request_method = $self->request->method;
+
+  my (@matches, $handler_found, $handler_method, $handler_class);
+  foreach my $class (keys %{$resources}) {
+
+    # The resource data for each module needs to be
+    # an array ref with an even number of elements
+    # to work correctly.
+    next
+      if (ref $resources->{$class} ne 'ARRAY'
+      || scalar @{$resources->{$class}} % 2 != 0);
+
+    while (my $regex = shift @{$resources->{$class}}) {
+      my $options_data = shift @{$resources->{$class}};
+      next if ref $options_data ne 'HASH';
+
+      if (@matches = ($path =~ $regex)) {
+
+        # If a specific path is accompanied by a OPTIONS request
+        # method, the user is asking for a list of possible request
+        # methods for a specific path.
+        $self->bz_rest_options([keys %{$options_data}]);
+
+        if ($options_data->{$request_method}) {
+          my $resource_data = $options_data->{$request_method};
+          $self->bz_class_name($class);
+
+          # The method key/value can be a simple scalar method name
+          # or a anonymous subroutine so we execute it here.
+          my $method
+            = ref $resource_data->{method} eq 'CODE'
+            ? $resource_data->{method}->($self)
+            : $resource_data->{method};
+          $self->bz_method_name($method);
+
+          # Pull out any parameters parsed from the URL path
+          # and store them for use by the method.
+          if ($resource_data->{params}) {
+            $self->bz_rest_params($resource_data->{params}->(@matches));
+          }
+
+          # If a special success code is needed for this particular
+          # method, then store it for later when generating response.
+          if ($resource_data->{success_code}) {
+            $self->bz_success_code($resource_data->{success_code});
+          }
+          $handler_found = 1;
         }
-        last if $handler_found;
+      }
+      last if $handler_found;
     }
+    last if $handler_found;
+  }
 
-    return $handler_found;
+  return $handler_found;
 }
 
 sub _best_content_type {
-    my ($self, @types) = @_;
-    return ($self->_simple_content_negotiation(@types))[0] || '*/*';
+  my ($self, @types) = @_;
+  return ($self->_simple_content_negotiation(@types))[0] || '*/*';
 }
 
 sub _simple_content_negotiation {
-    my ($self, @types) = @_;
-    my @accept_types = $self->_get_content_prefs();
-    # Return the types as-is if no accept header sent, since sorting will be a no-op.
-    if (!@accept_types) {
-        return @types;
-    }
-    my $score = sub { $self->_score_type(shift, @accept_types) };
-    return sort {$score->($b) <=> $score->($a)} @types;
+  my ($self, @types) = @_;
+  my @accept_types = $self->_get_content_prefs();
+
+ # Return the types as-is if no accept header sent, since sorting will be a no-op.
+  if (!@accept_types) {
+    return @types;
+  }
+  my $score = sub { $self->_score_type(shift, @accept_types) };
+  return sort { $score->($b) <=> $score->($a) } @types;
 }
 
 sub _score_type {
-    my ($self, $type, @accept_types) = @_;
-    my $score = scalar(@accept_types);
-    for my $accept_type (@accept_types) {
-        return $score if $type eq $accept_type;
-        $score--;
-    }
-    return 0;
+  my ($self, $type, @accept_types) = @_;
+  my $score = scalar(@accept_types);
+  for my $accept_type (@accept_types) {
+    return $score if $type eq $accept_type;
+    $score--;
+  }
+  return 0;
 }
 
 sub _get_content_prefs {
-    my $self = shift;
-    my $default_weight = 1;
-    my @prefs;
-
-    # Parse the Accept header, and save type name, score, and position.
-    my @accept_types = split /,/, $self->cgi->http('accept') || '';
-    my $order = 0;
-    for my $accept_type (@accept_types) {
-        my ($weight) = ($accept_type =~ /q=(\d\.\d+|\d+)/);
-        my ($name) = ($accept_type =~ m#(\S+/[^;]+)#);
-        next unless $name;
-        push @prefs, { name => $name, order => $order++};
-        if (defined $weight) {
-            $prefs[-1]->{score} = $weight;
-        } else {
-            $prefs[-1]->{score} = $default_weight;
-            $default_weight -= 0.001;
-        }
+  my $self           = shift;
+  my $default_weight = 1;
+  my @prefs;
+
+  # Parse the Accept header, and save type name, score, and position.
+  my @accept_types = split /,/, $self->cgi->http('accept') || '';
+  my $order = 0;
+  for my $accept_type (@accept_types) {
+    my ($weight) = ($accept_type =~ /q=(\d\.\d+|\d+)/);
+    my ($name)   = ($accept_type =~ m#(\S+/[^;]+)#);
+    next unless $name;
+    push @prefs, {name => $name, order => $order++};
+    if (defined $weight) {
+      $prefs[-1]->{score} = $weight;
+    }
+    else {
+      $prefs[-1]->{score} = $default_weight;
+      $default_weight -= 0.001;
     }
+  }
 
-    # Sort the types by score, subscore by order, and pull out just the name
-    @prefs = map {$_->{name}} sort {$b->{score} <=> $a->{score} ||
-                                    $a->{order} <=> $b->{order}} @prefs;
-    return @prefs;
+  # Sort the types by score, subscore by order, and pull out just the name
+  @prefs = map { $_->{name} }
+    sort { $b->{score} <=> $a->{score} || $a->{order} <=> $b->{order} } @prefs;
+  return @prefs;
 }
 
 1;
diff --git a/Bugzilla/WebService/Server/REST/Resources/Bug.pm b/Bugzilla/WebService/Server/REST/Resources/Bug.pm
index 3fa8b65cf..5cc25f432 100644
--- a/Bugzilla/WebService/Server/REST/Resources/Bug.pm
+++ b/Bugzilla/WebService/Server/REST/Resources/Bug.pm
@@ -15,150 +15,150 @@ use Bugzilla::WebService::Constants;
 use Bugzilla::WebService::Bug;
 
 BEGIN {
-    *Bugzilla::WebService::Bug::rest_resources = \&_rest_resources;
-};
+  *Bugzilla::WebService::Bug::rest_resources = \&_rest_resources;
+}
 
 sub _rest_resources {
-    my $rest_resources = [
-        qr{^/bug$}, {
-            GET  => {
-                method => 'search',
-            },
-            POST => {
-                method => 'create',
-                status_code => STATUS_CREATED
-            }
-        },
-        qr{^/bug/$}, {
-            GET => {
-                method => 'get'
-            }
-        },
-        qr{^/bug/([^/]+)$}, {
-            GET => {
-                method => 'get',
-                params => sub {
-                    return { ids => [ $_[0] ] };
-                }
-            },
-            PUT => {
-                method => 'update',
-                params => sub {
-                    return { ids => [ $_[0] ] };
-                }
-            }
-        },
-        qr{^/bug/([^/]+)/comment$}, {
-            GET  => {
-                method => 'comments',
-                params => sub {
-                    return { ids => [ $_[0] ] };
-                }
-            },
-            POST => {
-                method => 'add_comment',
-                params => sub {
-                    return { id => $_[0] };
-                },
-                success_code => STATUS_CREATED
-            }
-        },
-        qr{^/bug/comment/([^/]+)$}, {
-            GET => {
-                method => 'comments',
-                params => sub {
-                    return { comment_ids => [ $_[0] ] };
-                }
-            }
-        },
-        qr{^/bug/comment/tags/([^/]+)$}, {
-            GET => {
-                method => 'search_comment_tags',
-                params => sub {
-                    return { query => $_[0] };
-                },
-            },
-        },
-        qr{^/bug/comment/([^/]+)/tags$}, {
-            PUT => {
-                method => 'update_comment_tags',
-                params => sub {
-                    return { comment_id => $_[0] };
-                },
-            },
-        },
-        qr{^/bug/([^/]+)/history$}, {
-            GET => {
-                method => 'history',
-                params => sub {
-                    return { ids => [ $_[0] ] };
-                },
-            }
-        },
-        qr{^/bug/([^/]+)/attachment$}, {
-            GET  => {
-                method => 'attachments',
-                params => sub {
-                    return { ids => [ $_[0] ] };
-                }
-            },
-            POST => {
-                method => 'add_attachment',
-                params => sub {
-                    return { ids => [ $_[0] ] };
-                },
-                success_code => STATUS_CREATED
-            }
-        },
-        qr{^/bug/attachment/([^/]+)$}, {
-            GET => {
-                method => 'attachments',
-                params => sub {
-                    return { attachment_ids => [ $_[0] ] };
-                }
-            },
-            PUT => {
-                method => 'update_attachment',
-                params => sub {
-                    return { ids => [ $_[0] ] };
-                }
-            }
+  my $rest_resources = [
+    qr{^/bug$},
+    {
+      GET  => {method => 'search',},
+      POST => {method => 'create', status_code => STATUS_CREATED}
+    },
+    qr{^/bug/$},
+    {GET => {method => 'get'}},
+    qr{^/bug/([^/]+)$},
+    {
+      GET => {
+        method => 'get',
+        params => sub {
+          return {ids => [$_[0]]};
+        }
+      },
+      PUT => {
+        method => 'update',
+        params => sub {
+          return {ids => [$_[0]]};
+        }
+      }
+    },
+    qr{^/bug/([^/]+)/comment$},
+    {
+      GET => {
+        method => 'comments',
+        params => sub {
+          return {ids => [$_[0]]};
+        }
+      },
+      POST => {
+        method => 'add_comment',
+        params => sub {
+          return {id => $_[0]};
         },
-        qr{^/field/bug$}, {
-            GET => {
-                method => 'fields',
-            }
+        success_code => STATUS_CREATED
+      }
+    },
+    qr{^/bug/comment/([^/]+)$},
+    {
+      GET => {
+        method => 'comments',
+        params => sub {
+          return {comment_ids => [$_[0]]};
+        }
+      }
+    },
+    qr{^/bug/comment/tags/([^/]+)$},
+    {
+      GET => {
+        method => 'search_comment_tags',
+        params => sub {
+          return {query => $_[0]};
         },
-        qr{^/field/bug/([^/]+)$}, {
-            GET => {
-                method => 'fields',
-                params => sub {
-                    my $value = $_[0];
-                    my $param = 'names';
-                    $param = 'ids' if $value =~ /^\d+$/;
-                    return { $param => [ $_[0] ] };
-                }
-            }
+      },
+    },
+    qr{^/bug/comment/([^/]+)/tags$},
+    {
+      PUT => {
+        method => 'update_comment_tags',
+        params => sub {
+          return {comment_id => $_[0]};
         },
-        qr{^/field/bug/([^/]+)/values$}, {
-            GET => {
-                method => 'legal_values',
-                params => sub {
-                    return { field => $_[0] };
-                }
-            }
+      },
+    },
+    qr{^/bug/([^/]+)/history$},
+    {
+      GET => {
+        method => 'history',
+        params => sub {
+          return {ids => [$_[0]]};
         },
-        qr{^/field/bug/([^/]+)/([^/]+)/values$}, {
-            GET => {
-                method => 'legal_values',
-                params => sub {
-                    return { field      => $_[0],
-                             product_id => $_[1] };
-                }
-            }
+      }
+    },
+    qr{^/bug/([^/]+)/attachment$},
+    {
+      GET => {
+        method => 'attachments',
+        params => sub {
+          return {ids => [$_[0]]};
+        }
+      },
+      POST => {
+        method => 'add_attachment',
+        params => sub {
+          return {ids => [$_[0]]};
         },
-    ];
-    return $rest_resources;
+        success_code => STATUS_CREATED
+      }
+    },
+    qr{^/bug/attachment/([^/]+)$},
+    {
+      GET => {
+        method => 'attachments',
+        params => sub {
+          return {attachment_ids => [$_[0]]};
+        }
+      },
+      PUT => {
+        method => 'update_attachment',
+        params => sub {
+          return {ids => [$_[0]]};
+        }
+      }
+    },
+    qr{^/field/bug$},
+    {GET => {method => 'fields',}},
+    qr{^/field/bug/([^/]+)$},
+    {
+      GET => {
+        method => 'fields',
+        params => sub {
+          my $value = $_[0];
+          my $param = 'names';
+          $param = 'ids' if $value =~ /^\d+$/;
+          return {$param => [$_[0]]};
+        }
+      }
+    },
+    qr{^/field/bug/([^/]+)/values$},
+    {
+      GET => {
+        method => 'legal_values',
+        params => sub {
+          return {field => $_[0]};
+        }
+      }
+    },
+    qr{^/field/bug/([^/]+)/([^/]+)/values$},
+    {
+      GET => {
+        method => 'legal_values',
+        params => sub {
+          return {field => $_[0], product_id => $_[1]};
+        }
+      }
+    },
+  ];
+  return $rest_resources;
 }
 
 1;
diff --git a/Bugzilla/WebService/Server/REST/Resources/BugUserLastVisit.pm b/Bugzilla/WebService/Server/REST/Resources/BugUserLastVisit.pm
index 8502d6b3b..806c3f9c7 100644
--- a/Bugzilla/WebService/Server/REST/Resources/BugUserLastVisit.pm
+++ b/Bugzilla/WebService/Server/REST/Resources/BugUserLastVisit.pm
@@ -12,27 +12,28 @@ use strict;
 use warnings;
 
 BEGIN {
-    *Bugzilla::WebService::BugUserLastVisit::rest_resources = \&_rest_resources;
+  *Bugzilla::WebService::BugUserLastVisit::rest_resources = \&_rest_resources;
 }
 
 sub _rest_resources {
-    return [
-        # bug-id
-        qr{^/bug_user_last_visit/(\d+)$}, {
-            GET => {
-                method => 'get',
-                params => sub {
-                    return { ids => [$_[0]] };
-                },
-            },
-            POST => {
-                method => 'update',
-                params => sub {
-                    return { ids => [$_[0]] };
-                },
-            },
+  return [
+    # bug-id
+    qr{^/bug_user_last_visit/(\d+)$},
+    {
+      GET => {
+        method => 'get',
+        params => sub {
+          return {ids => [$_[0]]};
         },
-    ];
+      },
+      POST => {
+        method => 'update',
+        params => sub {
+          return {ids => [$_[0]]};
+        },
+      },
+    },
+  ];
 }
 
 1;
diff --git a/Bugzilla/WebService/Server/REST/Resources/Bugzilla.pm b/Bugzilla/WebService/Server/REST/Resources/Bugzilla.pm
index a8f3f9330..072cfe2f6 100644
--- a/Bugzilla/WebService/Server/REST/Resources/Bugzilla.pm
+++ b/Bugzilla/WebService/Server/REST/Resources/Bugzilla.pm
@@ -15,43 +15,19 @@ use Bugzilla::WebService::Constants;
 use Bugzilla::WebService::Bugzilla;
 
 BEGIN {
-    *Bugzilla::WebService::Bugzilla::rest_resources = \&_rest_resources;
-};
+  *Bugzilla::WebService::Bugzilla::rest_resources = \&_rest_resources;
+}
 
 sub _rest_resources {
-    my $rest_resources = [
-        qr{^/version$}, {
-            GET  => {
-                method => 'version'
-            }
-        },
-        qr{^/extensions$}, {
-            GET => {
-                method => 'extensions'
-            }
-        },
-        qr{^/timezone$}, {
-            GET => {
-                method => 'timezone'
-            }
-        },
-        qr{^/time$}, {
-            GET => {
-                method => 'time'
-            }
-        },
-        qr{^/last_audit_time$}, {
-            GET => {
-                method => 'last_audit_time'
-            }
-        },
-        qr{^/parameters$}, {
-            GET => {
-                method => 'parameters'
-            }
-        }
-    ];
-    return $rest_resources;
+  my $rest_resources = [
+    qr{^/version$},         {GET => {method => 'version'}},
+    qr{^/extensions$},      {GET => {method => 'extensions'}},
+    qr{^/timezone$},        {GET => {method => 'timezone'}},
+    qr{^/time$},            {GET => {method => 'time'}},
+    qr{^/last_audit_time$}, {GET => {method => 'last_audit_time'}},
+    qr{^/parameters$},      {GET => {method => 'parameters'}}
+  ];
+  return $rest_resources;
 }
 
 1;
diff --git a/Bugzilla/WebService/Server/REST/Resources/Classification.pm b/Bugzilla/WebService/Server/REST/Resources/Classification.pm
index 3f8d32a03..ed65aea5c 100644
--- a/Bugzilla/WebService/Server/REST/Resources/Classification.pm
+++ b/Bugzilla/WebService/Server/REST/Resources/Classification.pm
@@ -15,22 +15,23 @@ use Bugzilla::WebService::Constants;
 use Bugzilla::WebService::Classification;
 
 BEGIN {
-    *Bugzilla::WebService::Classification::rest_resources = \&_rest_resources;
-};
+  *Bugzilla::WebService::Classification::rest_resources = \&_rest_resources;
+}
 
 sub _rest_resources {
-    my $rest_resources = [
-        qr{^/classification/([^/]+)$}, {
-            GET => {
-                method => 'get',
-                params => sub {
-                    my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names';
-                    return { $param => [ $_[0] ] };
-                }
-            }
+  my $rest_resources = [
+    qr{^/classification/([^/]+)$},
+    {
+      GET => {
+        method => 'get',
+        params => sub {
+          my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names';
+          return {$param => [$_[0]]};
         }
-    ];
-    return $rest_resources;
+      }
+    }
+  ];
+  return $rest_resources;
 }
 
 1;
diff --git a/Bugzilla/WebService/Server/REST/Resources/Component.pm b/Bugzilla/WebService/Server/REST/Resources/Component.pm
index 198c09332..8870a0f04 100644
--- a/Bugzilla/WebService/Server/REST/Resources/Component.pm
+++ b/Bugzilla/WebService/Server/REST/Resources/Component.pm
@@ -17,19 +17,15 @@ use Bugzilla::WebService::Component;
 use Bugzilla::Error;
 
 BEGIN {
-    *Bugzilla::WebService::Component::rest_resources = \&_rest_resources;
-};
+  *Bugzilla::WebService::Component::rest_resources = \&_rest_resources;
+}
 
 sub _rest_resources {
-    my $rest_resources = [
-        qr{^/component$}, {
-            POST => {
-                method => 'create',
-                success_code => STATUS_CREATED
-            }
-        },
-    ];
-    return $rest_resources;
+  my $rest_resources = [
+    qr{^/component$},
+    {POST => {method => 'create', success_code => STATUS_CREATED}},
+  ];
+  return $rest_resources;
 }
 
 1;
diff --git a/Bugzilla/WebService/Server/REST/Resources/FlagType.pm b/Bugzilla/WebService/Server/REST/Resources/FlagType.pm
index 21dad0f73..438c8fb30 100644
--- a/Bugzilla/WebService/Server/REST/Resources/FlagType.pm
+++ b/Bugzilla/WebService/Server/REST/Resources/FlagType.pm
@@ -17,43 +17,40 @@ use Bugzilla::WebService::FlagType;
 use Bugzilla::Error;
 
 BEGIN {
-    *Bugzilla::WebService::FlagType::rest_resources = \&_rest_resources;
-};
+  *Bugzilla::WebService::FlagType::rest_resources = \&_rest_resources;
+}
 
 sub _rest_resources {
-    my $rest_resources = [
-        qr{^/flag_type$}, {
-            POST => {
-                method => 'create',
-                success_code => STATUS_CREATED
-            }
-        },
-        qr{^/flag_type/([^/]+)/([^/]+)$}, {
-            GET => {
-                method => 'get',
-                params => sub {
-                    return { product   => $_[0],
-                             component => $_[1] };
-                }
-            }
-        },
-        qr{^/flag_type/([^/]+)$}, {
-            GET => {
-                method => 'get',
-                params => sub {
-                    return { product => $_[0] };
-                }
-            },
-            PUT => {
-                method => 'update',
-                params => sub {
-                    my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names';
-                    return { $param => [ $_[0] ] };
-                }
-            }
-        },
-    ];
-    return $rest_resources;
+  my $rest_resources = [
+    qr{^/flag_type$},
+    {POST => {method => 'create', success_code => STATUS_CREATED}},
+    qr{^/flag_type/([^/]+)/([^/]+)$},
+    {
+      GET => {
+        method => 'get',
+        params => sub {
+          return {product => $_[0], component => $_[1]};
+        }
+      }
+    },
+    qr{^/flag_type/([^/]+)$},
+    {
+      GET => {
+        method => 'get',
+        params => sub {
+          return {product => $_[0]};
+        }
+      },
+      PUT => {
+        method => 'update',
+        params => sub {
+          my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names';
+          return {$param => [$_[0]]};
+        }
+      }
+    },
+  ];
+  return $rest_resources;
 }
 
 1;
diff --git a/Bugzilla/WebService/Server/REST/Resources/Group.pm b/Bugzilla/WebService/Server/REST/Resources/Group.pm
index b052e384b..7f607b7d1 100644
--- a/Bugzilla/WebService/Server/REST/Resources/Group.pm
+++ b/Bugzilla/WebService/Server/REST/Resources/Group.pm
@@ -15,31 +15,28 @@ use Bugzilla::WebService::Constants;
 use Bugzilla::WebService::Group;
 
 BEGIN {
-    *Bugzilla::WebService::Group::rest_resources = \&_rest_resources;
-};
+  *Bugzilla::WebService::Group::rest_resources = \&_rest_resources;
+}
 
 sub _rest_resources {
-    my $rest_resources = [
-        qr{^/group$}, {
-            GET  => {
-                method => 'get'
-            },
-            POST => {
-                method => 'create',
-                success_code => STATUS_CREATED
-            }
-        },
-        qr{^/group/([^/]+)$}, {
-            PUT => {
-                method => 'update',
-                params => sub {
-                    my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names';
-                    return { $param => [ $_[0] ] };
-                }
-            }
+  my $rest_resources = [
+    qr{^/group$},
+    {
+      GET  => {method => 'get'},
+      POST => {method => 'create', success_code => STATUS_CREATED}
+    },
+    qr{^/group/([^/]+)$},
+    {
+      PUT => {
+        method => 'update',
+        params => sub {
+          my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names';
+          return {$param => [$_[0]]};
         }
-    ];
-    return $rest_resources;
+      }
+    }
+  ];
+  return $rest_resources;
 }
 
 1;
diff --git a/Bugzilla/WebService/Server/REST/Resources/Product.pm b/Bugzilla/WebService/Server/REST/Resources/Product.pm
index 607b94b53..eabe19681 100644
--- a/Bugzilla/WebService/Server/REST/Resources/Product.pm
+++ b/Bugzilla/WebService/Server/REST/Resources/Product.pm
@@ -17,53 +17,41 @@ use Bugzilla::WebService::Product;
 use Bugzilla::Error;
 
 BEGIN {
-    *Bugzilla::WebService::Product::rest_resources = \&_rest_resources;
-};
+  *Bugzilla::WebService::Product::rest_resources = \&_rest_resources;
+}
 
 sub _rest_resources {
-    my $rest_resources = [
-        qr{^/product_accessible$}, {
-            GET => {
-                method => 'get_accessible_products'
-            }
-        },
-        qr{^/product_enterable$}, {
-            GET => {
-                method => 'get_enterable_products'
-            }
-        },
-        qr{^/product_selectable$}, {
-            GET => {
-                method => 'get_selectable_products'
-            }
-        },
-        qr{^/product$}, {
-            GET  => {
-                method => 'get'
-            },
-            POST => {
-                method => 'create',
-                success_code => STATUS_CREATED
-            }
-        },
-        qr{^/product/([^/]+)$}, {
-            GET => {
-                method => 'get',
-                params => sub {
-                    my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names';
-                    return { $param => [ $_[0] ] };
-                }
-            },
-            PUT => {
-                method => 'update',
-                params => sub {
-                    my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names';
-                    return { $param => [ $_[0] ] };
-                }
-            }
-        },
-    ];
-    return $rest_resources;
+  my $rest_resources = [
+    qr{^/product_accessible$},
+    {GET => {method => 'get_accessible_products'}},
+    qr{^/product_enterable$},
+    {GET => {method => 'get_enterable_products'}},
+    qr{^/product_selectable$},
+    {GET => {method => 'get_selectable_products'}},
+    qr{^/product$},
+    {
+      GET  => {method => 'get'},
+      POST => {method => 'create', success_code => STATUS_CREATED}
+    },
+    qr{^/product/([^/]+)$},
+    {
+      GET => {
+        method => 'get',
+        params => sub {
+          my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names';
+          return {$param => [$_[0]]};
+        }
+      },
+      PUT => {
+        method => 'update',
+        params => sub {
+          my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names';
+          return {$param => [$_[0]]};
+        }
+      }
+    },
+  ];
+  return $rest_resources;
 }
 
 1;
diff --git a/Bugzilla/WebService/Server/REST/Resources/User.pm b/Bugzilla/WebService/Server/REST/Resources/User.pm
index a83109e73..4555b4dbc 100644
--- a/Bugzilla/WebService/Server/REST/Resources/User.pm
+++ b/Bugzilla/WebService/Server/REST/Resources/User.pm
@@ -15,53 +15,41 @@ use Bugzilla::WebService::Constants;
 use Bugzilla::WebService::User;
 
 BEGIN {
-    *Bugzilla::WebService::User::rest_resources = \&_rest_resources;
-};
+  *Bugzilla::WebService::User::rest_resources = \&_rest_resources;
+}
 
 sub _rest_resources {
-    my $rest_resources = [
-        qr{^/login$}, {
-            GET => {
-                method => 'login'
-            }
-        },
-        qr{^/logout$}, {
-            GET => {
-                method => 'logout'
-            }
-        },
-        qr{^/valid_login$}, {
-            GET => {
-                method => 'valid_login'
-            }
-        },
-        qr{^/user$}, {
-            GET  => {
-                method => 'get'
-            },
-            POST => {
-                method => 'create',
-                success_code => STATUS_CREATED
-            }
-        },
-        qr{^/user/([^/]+)$}, {
-            GET => {
-                method => 'get',
-                params => sub {
-                    my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names';
-                    return { $param => [ $_[0] ] };
-                }
-            },
-            PUT => {
-                method => 'update',
-                params => sub {
-                    my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names';
-                    return { $param => [ $_[0] ] };
-                }
-            }
+  my $rest_resources = [
+    qr{^/login$},
+    {GET => {method => 'login'}},
+    qr{^/logout$},
+    {GET => {method => 'logout'}},
+    qr{^/valid_login$},
+    {GET => {method => 'valid_login'}},
+    qr{^/user$},
+    {
+      GET  => {method => 'get'},
+      POST => {method => 'create', success_code => STATUS_CREATED}
+    },
+    qr{^/user/([^/]+)$},
+    {
+      GET => {
+        method => 'get',
+        params => sub {
+          my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names';
+          return {$param => [$_[0]]};
+        }
+      },
+      PUT => {
+        method => 'update',
+        params => sub {
+          my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names';
+          return {$param => [$_[0]]};
         }
-    ];
-    return $rest_resources;
+      }
+    }
+  ];
+  return $rest_resources;
 }
 
 1;
diff --git a/Bugzilla/WebService/Server/XMLRPC.pm b/Bugzilla/WebService/Server/XMLRPC.pm
index 8deb253ad..b0eae8e19 100644
--- a/Bugzilla/WebService/Server/XMLRPC.pm
+++ b/Bugzilla/WebService/Server/XMLRPC.pm
@@ -14,9 +14,10 @@ use warnings;
 use XMLRPC::Transport::HTTP;
 use Bugzilla::WebService::Server;
 if ($ENV{MOD_PERL}) {
-    our @ISA = qw(XMLRPC::Transport::HTTP::Apache Bugzilla::WebService::Server);
-} else {
-    our @ISA = qw(XMLRPC::Transport::HTTP::CGI Bugzilla::WebService::Server);
+  our @ISA = qw(XMLRPC::Transport::HTTP::Apache Bugzilla::WebService::Server);
+}
+else {
+  our @ISA = qw(XMLRPC::Transport::HTTP::CGI Bugzilla::WebService::Server);
 }
 
 use Bugzilla::WebService::Constants;
@@ -26,97 +27,99 @@ use Bugzilla::Util;
 use List::MoreUtils qw(none);
 
 BEGIN {
-    # Allow WebService methods to call XMLRPC::Lite's type method directly
-    *Bugzilla::WebService::type = sub {
-        my ($self, $type, $value) = @_;
-        if ($type eq 'dateTime') {
-            # This is the XML-RPC implementation,  see the README in Bugzilla/WebService/.
-            # Our "base" implementation is in Bugzilla::WebService::Server.
-            $value = Bugzilla::WebService::Server->datetime_format_outbound($value);
-            $value =~ s/-//g;
-        }
-        elsif ($type eq 'email') {
-            $type = 'string';
-            if (Bugzilla->params->{'webservice_email_filter'}) {
-                $value = email_filter($value);
-            }
-        }
-        return XMLRPC::Data->type($type)->value($value);
-    };
-
-    # Add support for ETags into XMLRPC WebServices
-    *Bugzilla::WebService::bz_etag = sub {
-        return Bugzilla::WebService::Server->bz_etag($_[1]);
-    };
+  # Allow WebService methods to call XMLRPC::Lite's type method directly
+  *Bugzilla::WebService::type = sub {
+    my ($self, $type, $value) = @_;
+    if ($type eq 'dateTime') {
+
+      # This is the XML-RPC implementation,  see the README in Bugzilla/WebService/.
+      # Our "base" implementation is in Bugzilla::WebService::Server.
+      $value = Bugzilla::WebService::Server->datetime_format_outbound($value);
+      $value =~ s/-//g;
+    }
+    elsif ($type eq 'email') {
+      $type = 'string';
+      if (Bugzilla->params->{'webservice_email_filter'}) {
+        $value = email_filter($value);
+      }
+    }
+    return XMLRPC::Data->type($type)->value($value);
+  };
+
+  # Add support for ETags into XMLRPC WebServices
+  *Bugzilla::WebService::bz_etag = sub {
+    return Bugzilla::WebService::Server->bz_etag($_[1]);
+  };
 }
 
 sub initialize {
-    my $self = shift;
-    my %retval = $self->SUPER::initialize(@_);
-    $retval{'serializer'}   = Bugzilla::XMLRPC::Serializer->new;
-    $retval{'deserializer'} = Bugzilla::XMLRPC::Deserializer->new;
-    $retval{'dispatch_with'} = WS_DISPATCH;
-    return %retval;
+  my $self   = shift;
+  my %retval = $self->SUPER::initialize(@_);
+  $retval{'serializer'}    = Bugzilla::XMLRPC::Serializer->new;
+  $retval{'deserializer'}  = Bugzilla::XMLRPC::Deserializer->new;
+  $retval{'dispatch_with'} = WS_DISPATCH;
+  return %retval;
 }
 
 sub make_response {
-    my $self = shift;
-    my $cgi = Bugzilla->cgi;
-
-    # Fix various problems with IIS.
-    if ($ENV{'SERVER_SOFTWARE'} =~ /IIS/) {
-        $ENV{CONTENT_LENGTH} = 0;
-        binmode(STDOUT, ':bytes');
-    }
-
-    $self->SUPER::make_response(@_);
-
-    # XMLRPC::Transport::HTTP::CGI doesn't know about Bugzilla carrying around
-    # its cookies in Bugzilla::CGI, so we need to copy them over.
-    foreach my $cookie (@{$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]+/, $cgi->header)) {
-        my ($name, $value) = $header =~ /^([^:]+): (.*)/;
-        if (!$self->response->headers->header($name)) {
-           $self->response->headers->header($name => $value);
-        }
-    }
-
-    # ETag support
-    my $etag = $self->bz_etag;
-    if (!$etag) {
-        my $data = $self->response->as_string;
-        $etag = $self->bz_etag($data);
-    }
-
-    if ($etag && $cgi->check_etag($etag)) {
-        $self->response->headers->push_header('ETag', $etag);
-        $self->response->headers->push_header('status', '304 Not Modified');
-    }
-    elsif ($etag) {
-        $self->response->headers->push_header('ETag', $etag);
+  my $self = shift;
+  my $cgi  = Bugzilla->cgi;
+
+  # Fix various problems with IIS.
+  if ($ENV{'SERVER_SOFTWARE'} =~ /IIS/) {
+    $ENV{CONTENT_LENGTH} = 0;
+    binmode(STDOUT, ':bytes');
+  }
+
+  $self->SUPER::make_response(@_);
+
+  # XMLRPC::Transport::HTTP::CGI doesn't know about Bugzilla carrying around
+  # its cookies in Bugzilla::CGI, so we need to copy them over.
+  foreach my $cookie (@{$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]+/, $cgi->header)) {
+    my ($name, $value) = $header =~ /^([^:]+): (.*)/;
+    if (!$self->response->headers->header($name)) {
+      $self->response->headers->header($name => $value);
     }
+  }
+
+  # ETag support
+  my $etag = $self->bz_etag;
+  if (!$etag) {
+    my $data = $self->response->as_string;
+    $etag = $self->bz_etag($data);
+  }
+
+  if ($etag && $cgi->check_etag($etag)) {
+    $self->response->headers->push_header('ETag',   $etag);
+    $self->response->headers->push_header('status', '304 Not Modified');
+  }
+  elsif ($etag) {
+    $self->response->headers->push_header('ETag', $etag);
+  }
 }
 
 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 });
-    }
-
-    $ENV{CONTENT_LENGTH} = 0 if $ENV{'SERVER_SOFTWARE'} =~ /IIS/;
-    $self->SUPER::handle_login($class, $method, $full_method);
-    return;
+  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});
+  }
+
+  $ENV{CONTENT_LENGTH} = 0 if $ENV{'SERVER_SOFTWARE'} =~ /IIS/;
+  $self->SUPER::handle_login($class, $method, $full_method);
+  return;
 }
 
 1;
@@ -140,100 +143,111 @@ 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;
+  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;
-
-    # Only allow certain content types to protect against CSRF attacks
-    my $content_type = lc($ENV{'CONTENT_TYPE'});
-    # Remove charset, etc, if provided
-    $content_type =~ s/^([^;]+);.*/$1/;
-    if (!grep($_ eq $content_type, XMLRPC_CONTENT_TYPE_WHITELIST)) {
-        ThrowUserError('xmlrpc_illegal_content_type',
-                       { content_type => $ENV{'CONTENT_TYPE'} });
-    }
+  my $self = shift;
 
-    my ($xml) = @_;
-    my $som = $self->SUPER::deserialize(@_);
-    if (tainted($xml)) {
-        $som->{_bz_do_taint} = 1;
-    }
-    bless $som, 'Bugzilla::XMLRPC::SOM';
-    my $params = $som->paramsin;
-    # This allows positional parameters for Testopia.
-    $params = {} if ref $params ne 'HASH';
+  # Only allow certain content types to protect against CSRF attacks
+  my $content_type = lc($ENV{'CONTENT_TYPE'});
+
+  # Remove charset, etc, if provided
+  $content_type =~ s/^([^;]+);.*/$1/;
+  if (!grep($_ eq $content_type, XMLRPC_CONTENT_TYPE_WHITELIST)) {
+    ThrowUserError('xmlrpc_illegal_content_type',
+      {content_type => $ENV{'CONTENT_TYPE'}});
+  }
 
-    # Update the params to allow for several convenience key/values
-    # use for authentication
-    fix_credentials($params);
+  my ($xml) = @_;
+  my $som = $self->SUPER::deserialize(@_);
+  if (tainted($xml)) {
+    $som->{_bz_do_taint} = 1;
+  }
+  bless $som, 'Bugzilla::XMLRPC::SOM';
+  my $params = $som->paramsin;
 
-    Bugzilla->input_params($params);
+  # This allows positional parameters for Testopia.
+  $params = {} if ref $params ne 'HASH';
 
-    return $som;
+  # Update the params to allow for several convenience key/values
+  # use for authentication
+  fix_credentials($params);
+
+  Bugzilla->input_params($params);
+
+  return $som;
 }
 
 # Some method arguments need to be converted in some way, when they are input.
 sub decode_value {
-    my $self = shift;
-    my ($type) = @{ $_[0] };
-    my $value = $self->SUPER::decode_value(@_);
-    
-    # We only validate/convert certain types here.
-    return $value if $type !~ /^(?:int|i4|boolean|double|dateTime\.iso8601)$/;
-    
-    # Though the XML-RPC standard doesn't allow an empty ,
-    # ,or ,  we do, and we just say
-    # "that's undef".
-    if (grep($type eq $_, qw(int double dateTime))) {
-        return undef if $value eq '';
-    }
-    
-    my $validator = $self->_validation_subs->{$type};
-    if (!$validator->($value)) {
-        ThrowUserError('xmlrpc_invalid_value',
-                       { type => $type, value => $value });
-    }
-    
-    # We convert dateTimes to a DB-friendly date format.
-    if ($type eq 'dateTime.iso8601') {
-        if ($value !~ /T.*[\-+Z]/i) {
-           # The caller did not specify a timezone, so we assume UTC.
-           # pass 'Z' specifier to datetime_from to force it
-           $value = $value . 'Z';
-        }
-        $value = Bugzilla::WebService::Server::XMLRPC->datetime_format_inbound($value);
+  my $self   = shift;
+  my ($type) = @{$_[0]};
+  my $value  = $self->SUPER::decode_value(@_);
+
+  # We only validate/convert certain types here.
+  return $value if $type !~ /^(?:int|i4|boolean|double|dateTime\.iso8601)$/;
+
+  # Though the XML-RPC standard doesn't allow an empty ,
+  # ,or ,  we do, and we just say
+  # "that's undef".
+  if (grep($type eq $_, qw(int double dateTime))) {
+    return undef if $value eq '';
+  }
+
+  my $validator = $self->_validation_subs->{$type};
+  if (!$validator->($value)) {
+    ThrowUserError('xmlrpc_invalid_value', {type => $type, value => $value});
+  }
+
+  # We convert dateTimes to a DB-friendly date format.
+  if ($type eq 'dateTime.iso8601') {
+    if ($value !~ /T.*[\-+Z]/i) {
+
+      # The caller did not specify a timezone, so we assume UTC.
+      # pass 'Z' specifier to datetime_from to force it
+      $value = $value . 'Z';
     }
+    $value = Bugzilla::WebService::Server::XMLRPC->datetime_format_inbound($value);
+  }
 
-    return $value;
+  return $value;
 }
 
 sub _validation_subs {
-    my $self = shift;
-    return $self->{_validation_subs} if $self->{_validation_subs};
-    # The only place that XMLRPC::Lite stores any sort of validation
-    # regex is in XMLRPC::Serializer. We want to re-use those regexes here.
-    my $lookup = Bugzilla::XMLRPC::Serializer->new->typelookup;
-    
-    # $lookup is a hash whose values are arrayrefs, and whose keys are the
-    # names of types. The second item of each arrayref is a subroutine
-    # that will do our validation for us.
-    my %validators = map { $_ => $lookup->{$_}->[1] } (keys %$lookup);
-    # Add a boolean validator
-    $validators{'boolean'} = sub {$_[0] =~ /^[01]$/};
-    # Some types have multiple names, or have a different name in
-    # XMLRPC::Serializer than their standard XML-RPC name.
-    $validators{'dateTime.iso8601'} = $validators{'dateTime'};
-    $validators{'i4'} = $validators{'int'};
-    
-    $self->{_validation_subs} = \%validators;
-    return \%validators;
+  my $self = shift;
+  return $self->{_validation_subs} if $self->{_validation_subs};
+
+  # The only place that XMLRPC::Lite stores any sort of validation
+  # regex is in XMLRPC::Serializer. We want to re-use those regexes here.
+  my $lookup = Bugzilla::XMLRPC::Serializer->new->typelookup;
+
+  # $lookup is a hash whose values are arrayrefs, and whose keys are the
+  # names of types. The second item of each arrayref is a subroutine
+  # that will do our validation for us.
+  my %validators = map { $_ => $lookup->{$_}->[1] } (keys %$lookup);
+
+  # Add a boolean validator
+  $validators{'boolean'} = sub { $_[0] =~ /^[01]$/ };
+
+  # Some types have multiple names, or have a different name in
+  # XMLRPC::Serializer than their standard XML-RPC name.
+  $validators{'dateTime.iso8601'} = $validators{'dateTime'};
+  $validators{'i4'}               = $validators{'int'};
+
+  $self->{_validation_subs} = \%validators;
+  return \%validators;
 }
 
 1;
@@ -249,16 +263,16 @@ our @ISA = qw(XMLRPC::SOM);
 use Bugzilla::WebService::Util qw(taint_data);
 
 sub paramsin {
-    my $self = shift;
-    if (!$self->{bz_params_in}) {
-        my @params = $self->SUPER::paramsin(@_); 
-        if ($self->{_bz_do_taint}) {
-            taint_data(@params);
-        }
-        $self->{bz_params_in} = \@params;
+  my $self = shift;
+  if (!$self->{bz_params_in}) {
+    my @params = $self->SUPER::paramsin(@_);
+    if ($self->{_bz_do_taint}) {
+      taint_data(@params);
     }
-    my $params = $self->{bz_params_in};
-    return wantarray ? @$params : $params->[0];
+    $self->{bz_params_in} = \@params;
+  }
+  my $params = $self->{bz_params_in};
+  return wantarray ? @$params : $params->[0];
 }
 
 1;
@@ -272,43 +286,46 @@ use strict;
 use warnings;
 
 use Scalar::Util qw(blessed reftype);
+
 # We can't use "use parent" because XMLRPC::Serializer doesn't return
 # a true value.
 use XMLRPC::Lite;
 our @ISA = qw(XMLRPC::Serializer);
 
 sub new {
-    my $class = shift;
-    my $self = $class->SUPER::new(@_);
-    # This fixes UTF-8.
-    $self->{'_typelookup'}->{'base64'} =
-        [10, sub { !utf8::is_utf8($_[0]) && $_[0] =~ /[^\x09\x0a\x0d\x20-\x7f]/},
-        'as_base64'];
-    # This makes arrays work right even though we're a subclass.
-    # (See http://rt.cpan.org//Ticket/Display.html?id=34514)
-    $self->{'_encodingStyle'} = '';
-    return $self;
+  my $class = shift;
+  my $self  = $class->SUPER::new(@_);
+
+  # This fixes UTF-8.
+  $self->{'_typelookup'}->{'base64'} = [
+    10, sub { !utf8::is_utf8($_[0]) && $_[0] =~ /[^\x09\x0a\x0d\x20-\x7f]/ },
+    'as_base64'
+  ];
+
+  # This makes arrays work right even though we're a subclass.
+  # (See http://rt.cpan.org//Ticket/Display.html?id=34514)
+  $self->{'_encodingStyle'} = '';
+  return $self;
 }
 
 # Here the XMLRPC::Serializer is extended to use the XMLRPC nil extension.
 sub encode_object {
-    my $self = shift;
-    my @encoded = $self->SUPER::encode_object(@_);
+  my $self    = shift;
+  my @encoded = $self->SUPER::encode_object(@_);
 
-    return $encoded[0]->[0] eq 'nil'
-        ? ['value', {}, [@encoded]]
-        : @encoded;
+  return $encoded[0]->[0] eq 'nil' ? ['value', {}, [@encoded]] : @encoded;
 }
 
 # Removes undefined values so they do not produce invalid XMLRPC.
 sub envelope {
-    my $self = shift;
-    my ($type, $method, $data) = @_;
-    # If the type isn't a successful response we don't want to change the values.
-    if ($type eq 'response') {
-        _strip_undefs($data);
-    }
-    return $self->SUPER::envelope($type, $method, $data);
+  my $self = shift;
+  my ($type, $method, $data) = @_;
+
+  # If the type isn't a successful response we don't want to change the values.
+  if ($type eq 'response') {
+    _strip_undefs($data);
+  }
+  return $self->SUPER::envelope($type, $method, $data);
 }
 
 # In an XMLRPC response we have to handle hashes of arrays, hashes, scalars,
@@ -316,58 +333,58 @@ sub envelope {
 # The whole XMLRPC::Data object must be removed if its value key is undefined
 # so it cannot be recursed like the other hash type objects.
 sub _strip_undefs {
-    my ($initial) = @_;
-    my $type = reftype($initial) or return;
-
-    if ($type eq "HASH") {
-        while (my ($key, $value) = each(%$initial)) {
-            if ( !defined $value
-                 || (blessed $value && $value->isa('XMLRPC::Data') && !defined $value->value) )
-            {
-                # If the value is undefined remove it from the hash.
-                delete $initial->{$key};
-            }
-            else {
-                _strip_undefs($value);
-            }
-        }
+  my ($initial) = @_;
+  my $type = reftype($initial) or return;
+
+  if ($type eq "HASH") {
+    while (my ($key, $value) = each(%$initial)) {
+      if (!defined $value
+        || (blessed $value && $value->isa('XMLRPC::Data') && !defined $value->value))
+      {
+        # If the value is undefined remove it from the hash.
+        delete $initial->{$key};
+      }
+      else {
+        _strip_undefs($value);
+      }
     }
-    elsif ($type eq "ARRAY") {
-        for (my $count = 0; $count < scalar @{$initial}; $count++) {
-            my $value = $initial->[$count];
-            if ( !defined $value
-                 || (blessed $value && $value->isa('XMLRPC::Data') && !defined $value->value) )
-            {
-                # If the value is undefined remove it from the array.
-                splice(@$initial, $count, 1);
-                $count--;
-            }
-            else {
-                _strip_undefs($value);
-            }
-        }
+  }
+  elsif ($type eq "ARRAY") {
+    for (my $count = 0; $count < scalar @{$initial}; $count++) {
+      my $value = $initial->[$count];
+      if (!defined $value
+        || (blessed $value && $value->isa('XMLRPC::Data') && !defined $value->value))
+      {
+        # If the value is undefined remove it from the array.
+        splice(@$initial, $count, 1);
+        $count--;
+      }
+      else {
+        _strip_undefs($value);
+      }
     }
+  }
 }
 
 sub BEGIN {
-    no strict 'refs';
-    for my $type (qw(double i4 int dateTime)) {
-        my $method = 'as_' . $type;
-        *$method = sub {
-            my ($self, $value) = @_;
-            if (!defined($value)) {
-                return as_nil();
-            }
-            else {
-                my $super_method = "SUPER::$method"; 
-                return $self->$super_method($value);
-            }
-        }
-    }
+  no strict 'refs';
+  for my $type (qw(double i4 int dateTime)) {
+    my $method = 'as_' . $type;
+    *$method = sub {
+      my ($self, $value) = @_;
+      if (!defined($value)) {
+        return as_nil();
+      }
+      else {
+        my $super_method = "SUPER::$method";
+        return $self->$super_method($value);
+      }
+      }
+  }
 }
 
 sub as_nil {
-    return ['nil', {}];
+  return ['nil', {}];
 }
 
 1;
diff --git a/Bugzilla/WebService/User.pm b/Bugzilla/WebService/User.pm
index 0ae76d70f..591021831 100644
--- a/Bugzilla/WebService/User.pm
+++ b/Bugzilla/WebService/User.pm
@@ -18,40 +18,35 @@ use Bugzilla::Error;
 use Bugzilla::Group;
 use Bugzilla::User;
 use Bugzilla::Util qw(trim detaint_natural);
-use Bugzilla::WebService::Util qw(filter filter_wants validate translate params_to_objects);
+use Bugzilla::WebService::Util
+  qw(filter filter_wants validate translate params_to_objects);
 
 use List::Util qw(first min);
 
 # Don't need auth to login
-use constant LOGIN_EXEMPT => {
-    login => 1,
-    offer_account_by_email => 1,
-};
+use constant LOGIN_EXEMPT => {login => 1, offer_account_by_email => 1,};
 
 use constant READ_ONLY => qw(
-    get
+  get
 );
 
 use constant PUBLIC_METHODS => qw(
-    create
-    get
-    login
-    logout
-    offer_account_by_email
-    update
-    valid_login
+  create
+  get
+  login
+  logout
+  offer_account_by_email
+  update
+  valid_login
 );
 
-use constant MAPPED_FIELDS => {
-    email => 'login',
-    full_name => 'name',
-    login_denied_text => 'disabledtext',
-};
+use constant MAPPED_FIELDS =>
+  {email => 'login', full_name => 'name', login_denied_text => 'disabledtext',};
 
 use constant MAPPED_RETURNS => {
-    login_name => 'email',
-    realname => 'full_name',
-    disabledtext => 'login_denied_text',
+  login_name   => 'email',
+  realname     => 'full_name',
+  disabledtext => 'login_denied_text',
 };
 
 ##############
@@ -59,38 +54,38 @@ use constant MAPPED_RETURNS => {
 ##############
 
 sub login {
-    my ($self, $params) = @_;
+  my ($self, $params) = @_;
 
-    # Check to see if we are already logged in
-    my $user = Bugzilla->user;
-    if ($user->id) {
-        return $self->_login_to_hash($user);
-    }
+  # 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->{'Bugzilla_' . $param})
-            || ThrowCodeError('param_required', { param => $param });
-    }
+  # Username and password params are required
+  foreach my $param ("login", "password") {
+    (defined $params->{$param} || defined $params->{'Bugzilla_' . $param})
+      || ThrowCodeError('param_required', {param => $param});
+  }
 
-    $user = Bugzilla->login();
-    return $self->_login_to_hash($user);
+  $user = Bugzilla->login();
+  return $self->_login_to_hash($user);
 }
 
 sub logout {
-    my $self = shift;
-    Bugzilla->logout;
+  my $self = shift;
+  Bugzilla->logout;
 }
 
 sub valid_login {
-    my ($self, $params) = @_;
-    defined $params->{login}
-        || ThrowCodeError('param_required', { param => 'login' });
-    Bugzilla->login();
-    if (Bugzilla->user->id && Bugzilla->user->login eq $params->{login}) {
-        return $self->type('boolean', 1);
-    }
-    return $self->type('boolean', 0);
+  my ($self, $params) = @_;
+  defined $params->{login}
+    || ThrowCodeError('param_required', {param => 'login'});
+  Bugzilla->login();
+  if (Bugzilla->user->id && Bugzilla->user->login eq $params->{login}) {
+    return $self->type('boolean', 1);
+  }
+  return $self->type('boolean', 0);
 }
 
 #################
@@ -98,168 +93,171 @@ sub valid_login {
 #################
 
 sub offer_account_by_email {
-    my $self = shift;
-    my ($params) = @_;
-    my $email = trim($params->{email})
-        || ThrowCodeError('param_required', { param => 'email' });
-
-    Bugzilla->user->check_account_creation_enabled;
-    Bugzilla->user->check_and_send_account_creation_confirmation($email);
-    return undef;
+  my $self     = shift;
+  my ($params) = @_;
+  my $email    = trim($params->{email})
+    || ThrowCodeError('param_required', {param => 'email'});
+
+  Bugzilla->user->check_account_creation_enabled;
+  Bugzilla->user->check_and_send_account_creation_confirmation($email);
+  return undef;
 }
 
 sub create {
-    my $self = shift;
-    my ($params) = @_;
-
-    Bugzilla->user->in_group('editusers') 
-        || ThrowUserError("auth_failure", { group  => "editusers",
-                                            action => "add",
-                                            object => "users"});
-
-    my $email = trim($params->{email})
-        || ThrowCodeError('param_required', { param => 'email' });
-    my $realname = trim($params->{full_name});
-    my $password = trim($params->{password}) || '*';
-
-    my $user = Bugzilla::User->create({
-        login_name    => $email,
-        realname      => $realname,
-        cryptpassword => $password
+  my $self = shift;
+  my ($params) = @_;
+
+  Bugzilla->user->in_group('editusers')
+    || ThrowUserError("auth_failure",
+    {group => "editusers", action => "add", object => "users"});
+
+  my $email = trim($params->{email})
+    || ThrowCodeError('param_required', {param => 'email'});
+  my $realname = trim($params->{full_name});
+  my $password = trim($params->{password}) || '*';
+
+  my $user
+    = Bugzilla::User->create({
+    login_name => $email, realname => $realname, cryptpassword => $password
     });
 
-    return { id => $self->type('int', $user->id) };
+  return {id => $self->type('int', $user->id)};
 }
 
 
-# function to return user information by passing either user ids or 
+# function to return user information by passing either user ids or
 # login names or both together:
-# $call = $rpc->call( 'User.get', { ids => [1,2,3], 
+# $call = $rpc->call( 'User.get', { ids => [1,2,3],
 #         names => ['testusera@redhat.com', 'testuserb@redhat.com'] });
 sub get {
-    my ($self, $params) = validate(@_, 'names', 'ids', 'match', 'group_ids', 'groups');
-
-    Bugzilla->switch_to_shadow_db();
-
-    defined($params->{names}) || defined($params->{ids})
-        || defined($params->{match})
-        || ThrowCodeError('params_required', 
-               { function => 'User.get', params => ['ids', 'names', 'match'] });
-
-    my @user_objects;
-    @user_objects = map { Bugzilla::User->check($_) } @{ $params->{names} }
-                    if $params->{names};
-
-    # start filtering to remove duplicate user ids
-    my %unique_users = map { $_->id => $_ } @user_objects;
-    @user_objects = values %unique_users;
-      
-    my @users;
-
-    # If the user is not logged in: Return an error if they passed any user ids.
-    # Otherwise, return a limited amount of information based on login names.
-    if (!Bugzilla->user->id){
-        if ($params->{ids}){
-            ThrowUserError("user_access_by_id_denied");
-        }
-        if ($params->{match}) {
-            ThrowUserError('user_access_by_match_denied');
-        }
-        my $in_group = $self->_filter_users_by_group(
-            \@user_objects, $params);
-        @users = map { filter $params, {
-                     id        => $self->type('int', $_->id),
-                     real_name => $self->type('string', $_->name),
-                     name      => $self->type('email', $_->login),
-                 } } @$in_group;
-
-        return { users => \@users };
-    }
+  my ($self, $params)
+    = validate(@_, 'names', 'ids', 'match', 'group_ids', 'groups');
 
-    my $obj_by_ids;
-    $obj_by_ids = Bugzilla::User->new_from_list($params->{ids}) if $params->{ids};
-
-    # obj_by_ids are only visible to the user if they can see
-    # the otheruser, for non visible otheruser throw an error
-    foreach my $obj (@$obj_by_ids) {
-        if (Bugzilla->user->can_see_user($obj)){
-            if (!$unique_users{$obj->id}) {
-                push (@user_objects, $obj);
-                $unique_users{$obj->id} = $obj;
-            }
-        }
-        else {
-            ThrowUserError('auth_failure', {reason => "not_visible",
-                                            action => "access",
-                                            object => "user",
-                                            userid => $obj->id});
-        }
-    }
+  Bugzilla->switch_to_shadow_db();
 
-    # User Matching
-    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};
-    }
+       defined($params->{names})
+    || defined($params->{ids})
+    || defined($params->{match})
+    || ThrowCodeError('params_required',
+    {function => 'User.get', params => ['ids', 'names', 'match']});
 
-    my $exclude_disabled = $params->{'include_disabled'} ? 0 : 1;
-    foreach my $match_string (@{ $params->{'match'} || [] }) {
-        my $matched = Bugzilla::User::match($match_string, $limit, $exclude_disabled);
-        foreach my $user (@$matched) {
-            if (!$unique_users{$user->id}) {
-                push(@user_objects, $user);
-                $unique_users{$user->id} = $user;
-            }
-        }
-    }
+  my @user_objects;
+  @user_objects = map { Bugzilla::User->check($_) } @{$params->{names}}
+    if $params->{names};
+
+  # start filtering to remove duplicate user ids
+  my %unique_users = map { $_->id => $_ } @user_objects;
+  @user_objects = values %unique_users;
+
+  my @users;
 
+  # If the user is not logged in: Return an error if they passed any user ids.
+  # Otherwise, return a limited amount of information based on login names.
+  if (!Bugzilla->user->id) {
+    if ($params->{ids}) {
+      ThrowUserError("user_access_by_id_denied");
+    }
+    if ($params->{match}) {
+      ThrowUserError('user_access_by_match_denied');
+    }
     my $in_group = $self->_filter_users_by_group(\@user_objects, $params);
-    foreach my $user (@$in_group) {
-        my $user_info = filter $params, {
-            id        => $self->type('int', $user->id),
-            real_name => $self->type('string', $user->name),
-            name      => $self->type('email', $user->login),
-            email     => $self->type('email', $user->email),
-            can_login => $self->type('boolean', $user->is_enabled ? 1 : 0),
-        };
-
-        if (Bugzilla->user->in_group('editusers')) {
-            $user_info->{email_enabled}     = $self->type('boolean', $user->email_enabled);
-            $user_info->{login_denied_text} = $self->type('string', $user->disabledtext);
+    @users = map {
+      filter $params,
+        {
+        id        => $self->type('int',    $_->id),
+        real_name => $self->type('string', $_->name),
+        name      => $self->type('email',  $_->login),
         }
-
-        if (Bugzilla->user->id == $user->id) {
-            if (filter_wants($params, 'saved_searches')) {
-                $user_info->{saved_searches} = [
-                    map { $self->_query_to_hash($_) } @{ $user->queries }
-                ];
-            }
-            if (filter_wants($params, 'saved_reports')) {
-                $user_info->{saved_reports}  = [
-                    map { $self->_report_to_hash($_) } @{ $user->reports }
-                ];
-            }
+    } @$in_group;
+
+    return {users => \@users};
+  }
+
+  my $obj_by_ids;
+  $obj_by_ids = Bugzilla::User->new_from_list($params->{ids}) if $params->{ids};
+
+  # obj_by_ids are only visible to the user if they can see
+  # the otheruser, for non visible otheruser throw an error
+  foreach my $obj (@$obj_by_ids) {
+    if (Bugzilla->user->can_see_user($obj)) {
+      if (!$unique_users{$obj->id}) {
+        push(@user_objects, $obj);
+        $unique_users{$obj->id} = $obj;
+      }
+    }
+    else {
+      ThrowUserError(
+        'auth_failure',
+        {
+          reason => "not_visible",
+          action => "access",
+          object => "user",
+          userid => $obj->id
         }
+      );
+    }
+  }
+
+  # User Matching
+  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);
+    foreach my $user (@$matched) {
+      if (!$unique_users{$user->id}) {
+        push(@user_objects, $user);
+        $unique_users{$user->id} = $user;
+      }
+    }
+  }
+
+  my $in_group = $self->_filter_users_by_group(\@user_objects, $params);
+  foreach my $user (@$in_group) {
+    my $user_info = filter $params,
+      {
+      id        => $self->type('int',     $user->id),
+      real_name => $self->type('string',  $user->name),
+      name      => $self->type('email',   $user->login),
+      email     => $self->type('email',   $user->email),
+      can_login => $self->type('boolean', $user->is_enabled ? 1 : 0),
+      };
+
+    if (Bugzilla->user->in_group('editusers')) {
+      $user_info->{email_enabled}     = $self->type('boolean', $user->email_enabled);
+      $user_info->{login_denied_text} = $self->type('string',  $user->disabledtext);
+    }
 
-        if (filter_wants($params, 'groups')) {
-            if (Bugzilla->user->id == $user->id || Bugzilla->user->in_group('editusers')) {
-                $user_info->{groups} = [
-                    map { $self->_group_to_hash($_) } @{ $user->groups }
-                ];
-            }
-            else {
-                $user_info->{groups} = $self->_filter_bless_groups($user->groups);
-            }
-        }
+    if (Bugzilla->user->id == $user->id) {
+      if (filter_wants($params, 'saved_searches')) {
+        $user_info->{saved_searches}
+          = [map { $self->_query_to_hash($_) } @{$user->queries}];
+      }
+      if (filter_wants($params, 'saved_reports')) {
+        $user_info->{saved_reports}
+          = [map { $self->_report_to_hash($_) } @{$user->reports}];
+      }
+    }
 
-        push(@users, $user_info);
+    if (filter_wants($params, 'groups')) {
+      if (Bugzilla->user->id == $user->id || Bugzilla->user->in_group('editusers')) {
+        $user_info->{groups} = [map { $self->_group_to_hash($_) } @{$user->groups}];
+      }
+      else {
+        $user_info->{groups} = $self->_filter_bless_groups($user->groups);
+      }
     }
 
-    return { users => \@users };
+    push(@users, $user_info);
+  }
+
+  return {users => \@users};
 }
 
 ###############
@@ -267,156 +265,157 @@ sub get {
 ###############
 
 sub update {
-    my ($self, $params) = @_;
+  my ($self, $params) = @_;
 
-    my $dbh = Bugzilla->dbh;
+  my $dbh = Bugzilla->dbh;
 
-    my $user = Bugzilla->login(LOGIN_REQUIRED);
+  my $user = Bugzilla->login(LOGIN_REQUIRED);
 
-    # Reject access if there is no sense in continuing.
-    $user->in_group('editusers')
-        || ThrowUserError("auth_failure", {group  => "editusers",
-                                           action => "edit",
-                                           object => "users"});
+  # Reject access if there is no sense in continuing.
+  $user->in_group('editusers')
+    || ThrowUserError("auth_failure",
+    {group => "editusers", action => "edit", object => "users"});
 
-    defined($params->{names}) || defined($params->{ids})
-        || ThrowCodeError('params_required', 
-               { function => 'User.update', params => ['ids', 'names'] });
+  defined($params->{names})
+    || defined($params->{ids})
+    || ThrowCodeError('params_required',
+    {function => 'User.update', params => ['ids', 'names']});
 
-    my $user_objects = params_to_objects($params, 'Bugzilla::User');
+  my $user_objects = params_to_objects($params, 'Bugzilla::User');
 
-    my $values = translate($params, MAPPED_FIELDS);
+  my $values = translate($params, MAPPED_FIELDS);
 
-    # We delete names and ids to keep only new values to set.
-    delete $values->{names};
-    delete $values->{ids};
+  # We delete names and ids to keep only new values to set.
+  delete $values->{names};
+  delete $values->{ids};
 
-    $dbh->bz_start_transaction();
-    foreach my $user (@$user_objects){
-        $user->set_all($values);
-    }
+  $dbh->bz_start_transaction();
+  foreach my $user (@$user_objects) {
+    $user->set_all($values);
+  }
 
-    my %changes;
-    foreach my $user (@$user_objects){
-        my $returned_changes = $user->update();
-        $changes{$user->id} = translate($returned_changes, MAPPED_RETURNS);    
-    }
-    $dbh->bz_commit_transaction();
-
-    my @result;
-    foreach my $user (@$user_objects) {
-        my %hash = (
-            id      => $user->id,
-            changes => {},
-        );
-
-        foreach my $field (keys %{ $changes{$user->id} }) {
-            my $change = $changes{$user->id}->{$field};
-            # We normalize undef to an empty string, so that the API
-            # stays consistent for things that can become empty.
-            $change->[0] = '' if !defined $change->[0];
-            $change->[1] = '' if !defined $change->[1];
-            # We also flatten arrays (used by groups and blessed_groups)
-            $change->[0] = join(',', @{$change->[0]}) if ref $change->[0];
-            $change->[1] = join(',', @{$change->[1]}) if ref $change->[1];
-
-            $hash{changes}{$field} = {
-                removed => $self->type('string', $change->[0]),
-                added   => $self->type('string', $change->[1]) 
-            };
-        }
+  my %changes;
+  foreach my $user (@$user_objects) {
+    my $returned_changes = $user->update();
+    $changes{$user->id} = translate($returned_changes, MAPPED_RETURNS);
+  }
+  $dbh->bz_commit_transaction();
 
-        push(@result, \%hash);
-    }
+  my @result;
+  foreach my $user (@$user_objects) {
+    my %hash = (id => $user->id, changes => {},);
 
-    return { users => \@result };
-}
+    foreach my $field (keys %{$changes{$user->id}}) {
+      my $change = $changes{$user->id}->{$field};
 
-sub _filter_users_by_group {
-    my ($self, $users, $params) = @_;
-    my ($group_ids, $group_names) = @$params{qw(group_ids groups)};
-
-    # If no groups are specified, we return all users.
-    return $users if (!$group_ids and !$group_names);
+      # We normalize undef to an empty string, so that the API
+      # stays consistent for things that can become empty.
+      $change->[0] = '' if !defined $change->[0];
+      $change->[1] = '' if !defined $change->[1];
 
-    my $user = Bugzilla->user;
-    my (@groups, %groups);
+      # We also flatten arrays (used by groups and blessed_groups)
+      $change->[0] = join(',', @{$change->[0]}) if ref $change->[0];
+      $change->[1] = join(',', @{$change->[1]}) if ref $change->[1];
 
-    if ($group_ids) {
-        @groups = map { Bugzilla::Group->check({ id => $_ }) } @$group_ids;
-        $groups{$_->id} = $_ foreach @groups;
+      $hash{changes}{$field} = {
+        removed => $self->type('string', $change->[0]),
+        added   => $self->type('string', $change->[1])
+      };
     }
-    if ($group_names) {
-        foreach my $name (@$group_names) {
-            my $group = Bugzilla::Group->check({ name => $name, _error => 'invalid_group_name' });
-            $user->in_group($group) || ThrowUserError('invalid_group_name', { name => $name });
-            $groups{$group->id} = $group;
-        }
+
+    push(@result, \%hash);
+  }
+
+  return {users => \@result};
+}
+
+sub _filter_users_by_group {
+  my ($self, $users, $params) = @_;
+  my ($group_ids, $group_names) = @$params{qw(group_ids groups)};
+
+  # If no groups are specified, we return all users.
+  return $users if (!$group_ids and !$group_names);
+
+  my $user = Bugzilla->user;
+  my (@groups, %groups);
+
+  if ($group_ids) {
+    @groups = map { Bugzilla::Group->check({id => $_}) } @$group_ids;
+    $groups{$_->id} = $_ foreach @groups;
+  }
+  if ($group_names) {
+    foreach my $name (@$group_names) {
+      my $group
+        = Bugzilla::Group->check({name => $name, _error => 'invalid_group_name'});
+      $user->in_group($group)
+        || ThrowUserError('invalid_group_name', {name => $name});
+      $groups{$group->id} = $group;
     }
-    @groups = values %groups;
+  }
+  @groups = values %groups;
 
-    my @in_group = grep { $self->_user_in_any_group($_, \@groups) } @$users;
-    return \@in_group;
+  my @in_group = grep { $self->_user_in_any_group($_, \@groups) } @$users;
+  return \@in_group;
 }
 
 sub _user_in_any_group {
-    my ($self, $user, $groups) = @_;
-    foreach my $group (@$groups) {
-        return 1 if $user->in_group($group);
-    }
-    return 0;
+  my ($self, $user, $groups) = @_;
+  foreach my $group (@$groups) {
+    return 1 if $user->in_group($group);
+  }
+  return 0;
 }
 
 sub _filter_bless_groups {
-    my ($self, $groups) = @_;
-    my $user = Bugzilla->user;
+  my ($self, $groups) = @_;
+  my $user = Bugzilla->user;
 
-    my @filtered_groups;
-    foreach my $group (@$groups) {
-        next unless $user->can_bless($group->id);
-        push(@filtered_groups, $self->_group_to_hash($group));
-    }
+  my @filtered_groups;
+  foreach my $group (@$groups) {
+    next unless $user->can_bless($group->id);
+    push(@filtered_groups, $self->_group_to_hash($group));
+  }
 
-    return \@filtered_groups;
+  return \@filtered_groups;
 }
 
 sub _group_to_hash {
-    my ($self, $group) = @_;
-    my $item = {
-        id          => $self->type('int', $group->id), 
-        name        => $self->type('string', $group->name), 
-        description => $self->type('string', $group->description), 
-    };
-    return $item;
+  my ($self, $group) = @_;
+  my $item = {
+    id          => $self->type('int',    $group->id),
+    name        => $self->type('string', $group->name),
+    description => $self->type('string', $group->description),
+  };
+  return $item;
 }
 
 sub _query_to_hash {
-    my ($self, $query) = @_;
-    my $item = {
-        id    => $self->type('int', $query->id),
-        name  => $self->type('string', $query->name),
-        query => $self->type('string', $query->url),
-    };
-    return $item;
+  my ($self, $query) = @_;
+  my $item = {
+    id    => $self->type('int',    $query->id),
+    name  => $self->type('string', $query->name),
+    query => $self->type('string', $query->url),
+  };
+  return $item;
 }
 
 sub _report_to_hash {
-    my ($self, $report) = @_;
-    my $item = {
-        id    => $self->type('int', $report->id),
-        name  => $self->type('string', $report->name),
-        query => $self->type('string', $report->query),
-    };
-    return $item;
+  my ($self, $report) = @_;
+  my $item = {
+    id    => $self->type('int',    $report->id),
+    name  => $self->type('string', $report->name),
+    query => $self->type('string', $report->query),
+  };
+  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;
+  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;
diff --git a/Bugzilla/WebService/Util.pm b/Bugzilla/WebService/Util.pm
index a879c0e0d..3e70921b3 100644
--- a/Bugzilla/WebService/Util.pm
+++ b/Bugzilla/WebService/Util.pm
@@ -25,269 +25,277 @@ use parent qw(Exporter);
 require Test::Taint;
 
 our @EXPORT_OK = qw(
-    extract_flags
-    filter
-    filter_wants
-    taint_data
-    validate
-    translate
-    params_to_objects
-    fix_credentials
+  extract_flags
+  filter
+  filter_wants
+  taint_data
+  validate
+  translate
+  params_to_objects
+  fix_credentials
 );
 
 sub extract_flags {
-    my ($flags, $bug, $attachment) = @_;
-    my (@new_flags, @old_flags);
+  my ($flags, $bug, $attachment) = @_;
+  my (@new_flags, @old_flags);
 
-    my $flag_types    = $attachment ? $attachment->flag_types : $bug->flag_types;
-    my $current_flags = $attachment ? $attachment->flags : $bug->flags;
+  my $flag_types    = $attachment ? $attachment->flag_types : $bug->flag_types;
+  my $current_flags = $attachment ? $attachment->flags      : $bug->flags;
 
-    # Copy the user provided $flags as we may call extract_flags more than
-    # once when editing multiple bugs or attachments.
-    my $flags_copy = dclone($flags);
+  # Copy the user provided $flags as we may call extract_flags more than
+  # once when editing multiple bugs or attachments.
+  my $flags_copy = dclone($flags);
 
-    foreach my $flag (@$flags_copy) {
-        my $id      = $flag->{id};
-        my $type_id = $flag->{type_id};
+  foreach my $flag (@$flags_copy) {
+    my $id      = $flag->{id};
+    my $type_id = $flag->{type_id};
 
-        my $new  = delete $flag->{new};
-        my $name = delete $flag->{name};
+    my $new  = delete $flag->{new};
+    my $name = delete $flag->{name};
 
-        if ($id) {
-            my $flag_obj = grep($id == $_->id, @$current_flags);
-            $flag_obj || ThrowUserError('object_does_not_exist',
-                                        { class => 'Bugzilla::Flag', id => $id });
-        }
-        elsif ($type_id) {
-            my $type_obj = grep($type_id == $_->id, @$flag_types);
-            $type_obj || ThrowUserError('object_does_not_exist',
-                                        { class => 'Bugzilla::FlagType', id => $type_id });
-            if (!$new) {
-                my @flag_matches = grep($type_id == $_->type->id, @$current_flags);
-                @flag_matches > 1 && ThrowUserError('flag_not_unique',
-                                                     { value => $type_id });
-                if (!@flag_matches) {
-                    delete $flag->{id};
-                }
-                else {
-                    delete $flag->{type_id};
-                    $flag->{id} = $flag_matches[0]->id;
-                }
-            }
+    if ($id) {
+      my $flag_obj = grep($id == $_->id, @$current_flags);
+      $flag_obj
+        || ThrowUserError('object_does_not_exist',
+        {class => 'Bugzilla::Flag', id => $id});
+    }
+    elsif ($type_id) {
+      my $type_obj = grep($type_id == $_->id, @$flag_types);
+      $type_obj
+        || ThrowUserError('object_does_not_exist',
+        {class => 'Bugzilla::FlagType', id => $type_id});
+      if (!$new) {
+        my @flag_matches = grep($type_id == $_->type->id, @$current_flags);
+        @flag_matches > 1 && ThrowUserError('flag_not_unique', {value => $type_id});
+        if (!@flag_matches) {
+          delete $flag->{id};
         }
-        elsif ($name) {
-            my @type_matches = grep($name eq $_->name, @$flag_types);
-            @type_matches > 1 && ThrowUserError('flag_type_not_unique',
-                                                { value => $name });
-            @type_matches || ThrowUserError('object_does_not_exist',
-                                            { class => 'Bugzilla::FlagType', name => $name });
-            if ($new) {
-                delete $flag->{id};
-                $flag->{type_id} = $type_matches[0]->id;
-            }
-            else {
-                my @flag_matches = grep($name eq $_->type->name, @$current_flags);
-                @flag_matches > 1 && ThrowUserError('flag_not_unique', { value => $name });
-                if (@flag_matches) {
-                    $flag->{id} = $flag_matches[0]->id;
-                }
-                else {
-                    delete $flag->{id};
-                    $flag->{type_id} = $type_matches[0]->id;
-                }
-            }
+        else {
+          delete $flag->{type_id};
+          $flag->{id} = $flag_matches[0]->id;
         }
-
-        if ($flag->{id}) {
-            push(@old_flags, $flag);
+      }
+    }
+    elsif ($name) {
+      my @type_matches = grep($name eq $_->name, @$flag_types);
+      @type_matches > 1 && ThrowUserError('flag_type_not_unique', {value => $name});
+      @type_matches
+        || ThrowUserError('object_does_not_exist',
+        {class => 'Bugzilla::FlagType', name => $name});
+      if ($new) {
+        delete $flag->{id};
+        $flag->{type_id} = $type_matches[0]->id;
+      }
+      else {
+        my @flag_matches = grep($name eq $_->type->name, @$current_flags);
+        @flag_matches > 1 && ThrowUserError('flag_not_unique', {value => $name});
+        if (@flag_matches) {
+          $flag->{id} = $flag_matches[0]->id;
         }
         else {
-            push(@new_flags, $flag);
+          delete $flag->{id};
+          $flag->{type_id} = $type_matches[0]->id;
         }
+      }
     }
 
-    return (\@old_flags, \@new_flags);
+    if ($flag->{id}) {
+      push(@old_flags, $flag);
+    }
+    else {
+      push(@new_flags, $flag);
+    }
+  }
+
+  return (\@old_flags, \@new_flags);
 }
 
 sub filter($$;$$) {
-    my ($params, $hash, $types, $prefix) = @_;
-    my %newhash = %$hash;
+  my ($params, $hash, $types, $prefix) = @_;
+  my %newhash = %$hash;
 
-    foreach my $key (keys %$hash) {
-        delete $newhash{$key} if !filter_wants($params, $key, $types, $prefix);
-    }
+  foreach my $key (keys %$hash) {
+    delete $newhash{$key} if !filter_wants($params, $key, $types, $prefix);
+  }
 
-    return \%newhash;
+  return \%newhash;
 }
 
 sub filter_wants($$;$$) {
-    my ($params, $field, $types, $prefix) = @_;
-
-    # 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};
-    }
-
-    # Mimic old behavior if no types provided
-    my %field_types = map { $_ => 1 } (ref $types ? @$types : ($types || 'default'));
-
-    my %include = map { $_ => 1 } @{ $params->{'include_fields'} || [] };
-    my %exclude = map { $_ => 1 } @{ $params->{'exclude_fields'} || [] };
-
-    my %include_types;
-    my %exclude_types;
-
-    # Only return default fields if nothing is specified
-    $include_types{default} = 1 if !%include;
-
-    # Look for any field types requested
-    foreach my $key (keys %include) {
-        next if $key !~ /^_(.*)$/;
-        $include_types{$1} = 1;
-        delete $include{$key};
-    }
-    foreach my $key (keys %exclude) {
-        next if $key !~ /^_(.*)$/;
-        $exclude_types{$1} = 1;
-        delete $exclude{$key};
-    }
-
-    # Explicit inclusion/exclusion
-    return $cache->{$field} = 0 if $exclude{$field};
-    return $cache->{$field} = 1 if $include{$field};
-
-    # If the user has asked to include all or exclude all
-    return $cache->{$field} = 0 if $exclude_types{'all'};
-    return $cache->{$field} = 1 if $include_types{'all'};
-
-    # If the user has not asked for any fields specifically or if the user has asked
-    # for one or more of the field's types (and not excluded them)
-    foreach my $type (keys %field_types) {
-        return $cache->{$field} = 0 if $exclude_types{$type};
-        return $cache->{$field} = 1 if $include_types{$type};
-    }
-
-    my $wants = 0;
-    if ($prefix) {
-        # Include the field if the parent is include (and this one is not excluded)
-        $wants = 1 if $include{$prefix};
-    }
-    else {
-        # We want to include this if one of the sub keys is included
-        my $key = $field . '.';
-        my $len = length($key);
-        $wants = 1 if grep { substr($_, 0, $len) eq $key  } keys %include;
-    }
-
-    return $cache->{$field} = $wants;
+  my ($params, $field, $types, $prefix) = @_;
+
+  # 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};
+  }
+
+  # Mimic old behavior if no types provided
+  my %field_types
+    = map { $_ => 1 } (ref $types ? @$types : ($types || 'default'));
+
+  my %include = map { $_ => 1 } @{$params->{'include_fields'} || []};
+  my %exclude = map { $_ => 1 } @{$params->{'exclude_fields'} || []};
+
+  my %include_types;
+  my %exclude_types;
+
+  # Only return default fields if nothing is specified
+  $include_types{default} = 1 if !%include;
+
+  # Look for any field types requested
+  foreach my $key (keys %include) {
+    next if $key !~ /^_(.*)$/;
+    $include_types{$1} = 1;
+    delete $include{$key};
+  }
+  foreach my $key (keys %exclude) {
+    next if $key !~ /^_(.*)$/;
+    $exclude_types{$1} = 1;
+    delete $exclude{$key};
+  }
+
+  # Explicit inclusion/exclusion
+  return $cache->{$field} = 0 if $exclude{$field};
+  return $cache->{$field} = 1 if $include{$field};
+
+  # If the user has asked to include all or exclude all
+  return $cache->{$field} = 0 if $exclude_types{'all'};
+  return $cache->{$field} = 1 if $include_types{'all'};
+
+  # If the user has not asked for any fields specifically or if the user has asked
+  # for one or more of the field's types (and not excluded them)
+  foreach my $type (keys %field_types) {
+    return $cache->{$field} = 0 if $exclude_types{$type};
+    return $cache->{$field} = 1 if $include_types{$type};
+  }
+
+  my $wants = 0;
+  if ($prefix) {
+
+    # Include the field if the parent is include (and this one is not excluded)
+    $wants = 1 if $include{$prefix};
+  }
+  else {
+    # We want to include this if one of the sub keys is included
+    my $key = $field . '.';
+    my $len = length($key);
+    $wants = 1 if grep { substr($_, 0, $len) eq $key } keys %include;
+  }
+
+  return $cache->{$field} = $wants;
 }
 
 sub taint_data {
-    my @params = @_;
-    return if !@params;
-    # Though this is a private function, it hasn't changed since 2004 and
-    # should be safe to use, and prevents us from having to write it ourselves
-    # or require another module to do it.
-    Test::Taint::_deeply_traverse(\&_delete_bad_keys, \@params);
-    Test::Taint::taint_deeply(\@params);
+  my @params = @_;
+  return if !@params;
+
+  # Though this is a private function, it hasn't changed since 2004 and
+  # should be safe to use, and prevents us from having to write it ourselves
+  # or require another module to do it.
+  Test::Taint::_deeply_traverse(\&_delete_bad_keys, \@params);
+  Test::Taint::taint_deeply(\@params);
 }
 
 sub _delete_bad_keys {
-    foreach my $item (@_) {
-        next if ref $item ne 'HASH';
-        foreach my $key (keys %$item) {
-            # Making something a hash key always untaints it, in Perl.
-            # However, we need to validate our argument names in some way.
-            # We know that all hash keys passed in to the WebService will 
-            # match \w+, contain '.' or '-', so we delete any key that
-            # doesn't match that.
-            if ($key !~ /^[\w\.\-]+$/) {
-                delete $item->{$key};
-            }
-        }
+  foreach my $item (@_) {
+    next if ref $item ne 'HASH';
+    foreach my $key (keys %$item) {
+
+      # Making something a hash key always untaints it, in Perl.
+      # However, we need to validate our argument names in some way.
+      # We know that all hash keys passed in to the WebService will
+      # match \w+, contain '.' or '-', so we delete any key that
+      # doesn't match that.
+      if ($key !~ /^[\w\.\-]+$/) {
+        delete $item->{$key};
+      }
     }
-    return @_;
+  }
+  return @_;
 }
 
-sub validate  {
-    my ($self, $params, @keys) = @_;
-
-    # If $params is defined but not a reference, then we weren't
-    # sent any parameters at all, and we're getting @keys where
-    # $params should be.
-    return ($self, undef) if (defined $params and !ref $params);
-
-    my @id_params = qw(ids comment_ids);
-    # If @keys is not empty then we convert any named 
-    # parameters that have scalar values to arrayrefs
-    # that match.
-    foreach my $key (@keys) {
-        if (exists $params->{$key}) {
-            $params->{$key} = [ $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;
-            }
-        }
+sub validate {
+  my ($self, $params, @keys) = @_;
+
+  # If $params is defined but not a reference, then we weren't
+  # sent any parameters at all, and we're getting @keys where
+  # $params should be.
+  return ($self, undef) if (defined $params and !ref $params);
+
+  my @id_params = qw(ids comment_ids);
+
+  # If @keys is not empty then we convert any named
+  # parameters that have scalar values to arrayrefs
+  # that match.
+  foreach my $key (@keys) {
+    if (exists $params->{$key}) {
+      $params->{$key} = [$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;
+      }
     }
+  }
 
-    return ($self, $params);
+  return ($self, $params);
 }
 
 sub translate {
-    my ($params, $mapped) = @_;
-    my %changes;
-    while (my ($key,$value) = each (%$params)) {
-        my $new_field = $mapped->{$key} || $key;
-        $changes{$new_field} = $value;
-    }
-    return \%changes;
+  my ($params, $mapped) = @_;
+  my %changes;
+  while (my ($key, $value) = each(%$params)) {
+    my $new_field = $mapped->{$key} || $key;
+    $changes{$new_field} = $value;
+  }
+  return \%changes;
 }
 
 sub params_to_objects {
-    my ($params, $class) = @_;
-    my (@objects, @objects_by_ids);
+  my ($params, $class) = @_;
+  my (@objects, @objects_by_ids);
 
-    @objects = map { $class->check($_) } 
-        @{ $params->{names} } if $params->{names};
+  @objects = map { $class->check($_) } @{$params->{names}} if $params->{names};
 
-    @objects_by_ids = map { $class->check({ id => $_ }) } 
-        @{ $params->{ids} } if $params->{ids};
+  @objects_by_ids = map { $class->check({id => $_}) } @{$params->{ids}}
+    if $params->{ids};
 
-    push(@objects, @objects_by_ids);
-    my %seen;
-    @objects = grep { !$seen{$_->id}++ } @objects;
-    return \@objects;
+  push(@objects, @objects_by_ids);
+  my %seen;
+  @objects = grep { !$seen{$_->id}++ } @objects;
+  return \@objects;
 }
 
 sub fix_credentials {
-    my ($params) = @_;
-    # Allow user to pass in login=foo&password=bar as a convenience
-    # even if not calling GET /login. We also do not delete them as
-    # GET /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 api_key=12345678 as a convenience which becomes
-    # "Bugzilla_api_key" which is what the auth code looks for.
-    if (exists $params->{api_key}) {
-        $params->{Bugzilla_api_key} = delete $params->{api_key};
-    }
-    # 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'};
-    }
-
-    # Allow extensions to modify the credential data before login
-    Bugzilla::Hook::process('webservice_fix_credentials', { params => $params });
+  my ($params) = @_;
+
+  # Allow user to pass in login=foo&password=bar as a convenience
+  # even if not calling GET /login. We also do not delete them as
+  # GET /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 api_key=12345678 as a convenience which becomes
+  # "Bugzilla_api_key" which is what the auth code looks for.
+  if (exists $params->{api_key}) {
+    $params->{Bugzilla_api_key} = delete $params->{api_key};
+  }
+
+  # 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'};
+  }
+
+  # Allow extensions to modify the credential data before login
+  Bugzilla::Hook::process('webservice_fix_credentials', {params => $params});
 }
 
 __END__
diff --git a/Bugzilla/Whine.pm b/Bugzilla/Whine.pm
index eeaea6da4..081933cba 100644
--- a/Bugzilla/Whine.pm
+++ b/Bugzilla/Whine.pm
@@ -27,11 +27,11 @@ use Bugzilla::Whine::Query;
 use constant DB_TABLE => 'whine_events';
 
 use constant DB_COLUMNS => qw(
-    id
-    owner_userid
-    subject
-    body
-    mailifnobugs
+  id
+  owner_userid
+  subject
+  body
+  mailifnobugs
 );
 
 use constant LIST_ORDER => 'id';
@@ -39,15 +39,15 @@ use constant LIST_ORDER => 'id';
 ####################
 # Simple Accessors #
 ####################
-sub subject         { return $_[0]->{'subject'};      }
-sub body            { return $_[0]->{'body'};         }
+sub subject         { return $_[0]->{'subject'}; }
+sub body            { return $_[0]->{'body'}; }
 sub mail_if_no_bugs { return $_[0]->{'mailifnobugs'}; }
 
 sub user {
-    my ($self) = @_;
-    return $self->{user} if defined $self->{user};
-    $self->{user} = new Bugzilla::User($self->{'owner_userid'});
-    return $self->{user};
+  my ($self) = @_;
+  return $self->{user} if defined $self->{user};
+  $self->{user} = new Bugzilla::User($self->{'owner_userid'});
+  return $self->{user};
 }
 
 1;
diff --git a/Bugzilla/Whine/Query.pm b/Bugzilla/Whine/Query.pm
index b2a2c9e07..6648eab66 100644
--- a/Bugzilla/Whine/Query.pm
+++ b/Bugzilla/Whine/Query.pm
@@ -23,12 +23,12 @@ use Bugzilla::Search::Saved;
 use constant DB_TABLE => 'whine_queries';
 
 use constant DB_COLUMNS => qw(
-    id
-    eventid
-    query_name
-    sortkey
-    onemailperbug
-    title
+  id
+  eventid
+  query_name
+  sortkey
+  onemailperbug
+  title
 );
 
 use constant NAME_FIELD => 'id';
@@ -37,11 +37,11 @@ use constant LIST_ORDER => 'sortkey';
 ####################
 # Simple Accessors #
 ####################
-sub eventid           { return $_[0]->{'eventid'};       }
-sub sortkey           { return $_[0]->{'sortkey'};       }
+sub eventid           { return $_[0]->{'eventid'}; }
+sub sortkey           { return $_[0]->{'sortkey'}; }
 sub one_email_per_bug { return $_[0]->{'onemailperbug'}; }
-sub title             { return $_[0]->{'title'};         }
-sub name              { return $_[0]->{'query_name'};    }
+sub title             { return $_[0]->{'title'}; }
+sub name              { return $_[0]->{'query_name'}; }
 
 
 1;
diff --git a/Bugzilla/Whine/Schedule.pm b/Bugzilla/Whine/Schedule.pm
index 11f0bf16f..7517a3f26 100644
--- a/Bugzilla/Whine/Schedule.pm
+++ b/Bugzilla/Whine/Schedule.pm
@@ -22,22 +22,22 @@ use Bugzilla::Constants;
 use constant DB_TABLE => 'whine_schedules';
 
 use constant DB_COLUMNS => qw(
-    id
-    eventid
-    run_day
-    run_time
-    run_next
-    mailto
-    mailto_type
+  id
+  eventid
+  run_day
+  run_time
+  run_next
+  mailto
+  mailto_type
 );
 
 use constant UPDATE_COLUMNS => qw(
-    eventid 
-    run_day 
-    run_time 
-    run_next 
-    mailto 
-    mailto_type
+  eventid
+  run_day
+  run_time
+  run_next
+  mailto
+  mailto_type
 );
 use constant NAME_FIELD => 'id';
 use constant LIST_ORDER => 'id';
@@ -45,36 +45,38 @@ use constant LIST_ORDER => 'id';
 ####################
 # Simple Accessors #
 ####################
-sub eventid         { return $_[0]->{'eventid'};     }
-sub run_day         { return $_[0]->{'run_day'};     }
-sub run_time        { return $_[0]->{'run_time'};    }
+sub eventid         { return $_[0]->{'eventid'}; }
+sub run_day         { return $_[0]->{'run_day'}; }
+sub run_time        { return $_[0]->{'run_time'}; }
 sub mailto_is_group { return $_[0]->{'mailto_type'}; }
 
 sub mailto {
-    my $self = shift;
-
-    return $self->{mailto_object} if exists $self->{mailto_object};
-    my $id = $self->{'mailto'};
-
-    if ($self->mailto_is_group) {
-        $self->{mailto_object} = Bugzilla::Group->new($id);
-    } else {
-        $self->{mailto_object} = Bugzilla::User->new($id);
-    }
-    return $self->{mailto_object};
+  my $self = shift;
+
+  return $self->{mailto_object} if exists $self->{mailto_object};
+  my $id = $self->{'mailto'};
+
+  if ($self->mailto_is_group) {
+    $self->{mailto_object} = Bugzilla::Group->new($id);
+  }
+  else {
+    $self->{mailto_object} = Bugzilla::User->new($id);
+  }
+  return $self->{mailto_object};
 }
 
-sub mailto_users { 
-    my $self = shift;
-    return $self->{mailto_users} if exists $self->{mailto_users};
-    my $object = $self->mailto;
-
-    if ($self->mailto_is_group) {
-        $self->{mailto_users} = $object->members_non_inherited if $object->is_active;
-    } else {
-        $self->{mailto_users} = $object;
-    }
-    return $self->{mailto_users};
+sub mailto_users {
+  my $self = shift;
+  return $self->{mailto_users} if exists $self->{mailto_users};
+  my $object = $self->mailto;
+
+  if ($self->mailto_is_group) {
+    $self->{mailto_users} = $object->members_non_inherited if $object->is_active;
+  }
+  else {
+    $self->{mailto_users} = $object;
+  }
+  return $self->{mailto_users};
 }
 
 1;
diff --git a/admin.cgi b/admin.cgi
index 1dc9b2c1b..2ba0cdc7f 100755
--- a/admin.cgi
+++ b/admin.cgi
@@ -16,14 +16,15 @@ use Bugzilla;
 use Bugzilla::Constants;
 use Bugzilla::Error;
 
-my $cgi = Bugzilla->cgi;
+my $cgi      = Bugzilla->cgi;
 my $template = Bugzilla->template;
-my $user = Bugzilla->login(LOGIN_REQUIRED);
+my $user     = Bugzilla->login(LOGIN_REQUIRED);
 
 print $cgi->header();
 
 $user->can_administer
-  || ThrowUserError('auth_failure', {action => 'access', object => 'administrative_pages'});
+  || ThrowUserError('auth_failure',
+  {action => 'access', object => 'administrative_pages'});
 
 $template->process('admin/admin.html.tmpl')
   || ThrowTemplateError($template->error());
diff --git a/attachment.cgi b/attachment.cgi
index 4cd9229fb..d8244b080 100755
--- a/attachment.cgi
+++ b/attachment.cgi
@@ -16,8 +16,8 @@ use Bugzilla;
 use Bugzilla::BugMail;
 use Bugzilla::Constants;
 use Bugzilla::Error;
-use Bugzilla::Flag; 
-use Bugzilla::FlagType; 
+use Bugzilla::Flag;
+use Bugzilla::FlagType;
 use Bugzilla::User;
 use Bugzilla::Util;
 use Bugzilla::Bug;
@@ -26,15 +26,15 @@ use Bugzilla::Attachment::PatchReader;
 use Bugzilla::Token;
 
 use Encode qw(encode find_encoding);
-use Encode::MIME::Header; # Required to alter Encode::Encoding{'MIME-Q'}.
+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
 # variables in so many subroutines that it was easier to just
 # make them globals.
-local our $cgi = Bugzilla->cgi;
-local our $template = Bugzilla->template;
-local our $vars = {};
+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
@@ -49,57 +49,48 @@ my $format = $cgi->param('format') || '';
 # You must use the appropriate urlbase/sslbase param when doing anything
 # but viewing an attachment, or a raw diff.
 if ($action ne 'view'
-    && (($action !~ /^(?:interdiff|diff)$/) || $format ne 'raw'))
+  && (($action !~ /^(?:interdiff|diff)$/) || $format ne 'raw'))
 {
-    do_ssl_redirect_if_required();
-    if ($cgi->url_is_attachment_base) {
-        $cgi->redirect_to_urlbase;
-    }
-    Bugzilla->login();
+  do_ssl_redirect_if_required();
+  if ($cgi->url_is_attachment_base) {
+    $cgi->redirect_to_urlbase;
+  }
+  Bugzilla->login();
 }
 
 # When viewing an attachment, do not request credentials if we are on
 # the alternate host. Let view() decide when to call Bugzilla->login.
-if ($action eq "view")
-{
-    view();
+if ($action eq "view") {
+  view();
 }
-elsif ($action eq "interdiff")
-{
-    interdiff();
+elsif ($action eq "interdiff") {
+  interdiff();
 }
-elsif ($action eq "diff")
-{
-    diff();
+elsif ($action eq "diff") {
+  diff();
 }
-elsif ($action eq "viewall") 
-{ 
-    viewall(); 
+elsif ($action eq "viewall") {
+  viewall();
 }
-elsif ($action eq "enter") 
-{ 
-    Bugzilla->login(LOGIN_REQUIRED);
-    enter(); 
+elsif ($action eq "enter") {
+  Bugzilla->login(LOGIN_REQUIRED);
+  enter();
 }
-elsif ($action eq "insert")
-{
-    Bugzilla->login(LOGIN_REQUIRED);
-    insert();
+elsif ($action eq "insert") {
+  Bugzilla->login(LOGIN_REQUIRED);
+  insert();
 }
-elsif ($action eq "edit") 
-{ 
-    edit(); 
+elsif ($action eq "edit") {
+  edit();
 }
-elsif ($action eq "update") 
-{ 
-    Bugzilla->login(LOGIN_REQUIRED);
-    update();
+elsif ($action eq "update") {
+  Bugzilla->login(LOGIN_REQUIRED);
+  update();
 }
 elsif ($action eq "delete") {
-    delete_attachment();
+  delete_attachment();
 }
-else 
-{ 
+else {
   ThrowUserError('unknown_action', {action => $action});
 }
 
@@ -121,72 +112,73 @@ exit;
 # Returns an attachment object.
 
 sub validateID {
-    my($param, $dont_validate_access) = @_;
-    $param ||= 'id';
+  my ($param, $dont_validate_access) = @_;
+  $param ||= 'id';
 
-    # If we're not doing interdiffs, check if id wasn't specified and
-    # prompt them with a page that allows them to choose an attachment.
-    # Happens when calling plain attachment.cgi from the urlbar directly
-    if ($param eq 'id' && !$cgi->param('id')) {
-        print $cgi->header();
-        $template->process("attachment/choose.html.tmpl", $vars) ||
-            ThrowTemplateError($template->error());
-        exit;
-    }
-    
-    my $attach_id = $cgi->param($param);
-
-    # Validate the specified attachment id. detaint kills $attach_id if
-    # non-natural, so use the original value from $cgi in our exception
-    # message here.
-    detaint_natural($attach_id)
-        || ThrowUserError("invalid_attach_id",
-                          { attach_id => scalar $cgi->param($param) });
-  
-    # Make sure the attachment exists in the database.
-    my $attachment = new Bugzilla::Attachment({ id => $attach_id, cache => 1 })
-        || ThrowUserError("invalid_attach_id", { attach_id => $attach_id });
-
-    return $attachment if ($dont_validate_access || check_can_access($attachment));
+  # If we're not doing interdiffs, check if id wasn't specified and
+  # prompt them with a page that allows them to choose an attachment.
+  # Happens when calling plain attachment.cgi from the urlbar directly
+  if ($param eq 'id' && !$cgi->param('id')) {
+    print $cgi->header();
+    $template->process("attachment/choose.html.tmpl", $vars)
+      || ThrowTemplateError($template->error());
+    exit;
+  }
+
+  my $attach_id = $cgi->param($param);
+
+  # Validate the specified attachment id. detaint kills $attach_id if
+  # non-natural, so use the original value from $cgi in our exception
+  # message here.
+  detaint_natural($attach_id)
+    || ThrowUserError("invalid_attach_id",
+    {attach_id => scalar $cgi->param($param)});
+
+  # Make sure the attachment exists in the database.
+  my $attachment = new Bugzilla::Attachment({id => $attach_id, cache => 1})
+    || ThrowUserError("invalid_attach_id", {attach_id => $attach_id});
+
+  return $attachment if ($dont_validate_access || check_can_access($attachment));
 }
 
 # Make sure the current user has access to the specified attachment.
 sub check_can_access {
-    my $attachment = shift;
-    my $user = Bugzilla->user;
-
-    # Make sure the user is authorized to access this attachment's bug.
-    Bugzilla::Bug->check({ id => $attachment->bug_id, cache => 1 });
-    if ($attachment->isprivate && $user->id != $attachment->attacher->id 
-        && !$user->is_insider) 
-    {
-        ThrowUserError('auth_failure', {action => 'access',
-                                        object => 'attachment',
-                                        attach_id => $attachment->id});
-    }
-    return 1;
+  my $attachment = shift;
+  my $user       = Bugzilla->user;
+
+  # Make sure the user is authorized to access this attachment's bug.
+  Bugzilla::Bug->check({id => $attachment->bug_id, cache => 1});
+  if ( $attachment->isprivate
+    && $user->id != $attachment->attacher->id
+    && !$user->is_insider)
+  {
+    ThrowUserError('auth_failure',
+      {action => 'access', object => 'attachment', attach_id => $attachment->id});
+  }
+  return 1;
 }
 
 # Determines if the attachment is public -- that is, if users who are
 # not logged in have access to the attachment
 sub attachmentIsPublic {
-    my $attachment = shift;
+  my $attachment = shift;
 
-    return 0 if Bugzilla->params->{'requirelogin'};
-    return 0 if $attachment->isprivate;
+  return 0 if Bugzilla->params->{'requirelogin'};
+  return 0 if $attachment->isprivate;
 
-    my $anon_user = new Bugzilla::User;
-    return $anon_user->can_see_bug($attachment->bug_id);
+  my $anon_user = new Bugzilla::User;
+  return $anon_user->can_see_bug($attachment->bug_id);
 }
 
 # Validates format of a diff/interdiff. Takes a list as an parameter, which
 # defines the valid format values. Will throw an error if the format is not
 # in the list. Returns either the user selected or default format.
 sub validateFormat {
+
   # receives a list of legal formats; first item is a default
   my $format = $cgi->param('format') || $_[0];
   if (not grep($_ eq $format, @_)) {
-     ThrowUserError("invalid_format", { format  => $format, formats => \@_ });
+    ThrowUserError("invalid_format", {format => $format, formats => \@_});
   }
 
   return $format;
@@ -195,125 +187,139 @@ sub validateFormat {
 # Gets the attachment object(s) generated by validateID, while ensuring
 # attachbase and token authentication is used when required.
 sub get_attachment {
-    my @field_names = @_ ? @_ : qw(id);
-
-    my %attachments;
-
-    if (use_attachbase()) {
-        # Load each attachment, and ensure they are all from the same bug
-        my $bug_id = 0;
+  my @field_names = @_ ? @_ : qw(id);
+
+  my %attachments;
+
+  if (use_attachbase()) {
+
+    # Load each attachment, and ensure they are all from the same bug
+    my $bug_id = 0;
+    foreach my $field_name (@field_names) {
+      my $attachment = validateID($field_name, 1);
+      if (!$bug_id) {
+        $bug_id = $attachment->bug_id;
+      }
+      elsif ($attachment->bug_id != $bug_id) {
+        ThrowUserError('attachment_bug_id_mismatch');
+      }
+      $attachments{$field_name} = $attachment;
+    }
+    my @args = map { $_ . '=' . $attachments{$_}->id } @field_names;
+    my $cgi_params = $cgi->canonicalise_query(@field_names, 't', 'Bugzilla_login',
+      'Bugzilla_password');
+    push(@args, $cgi_params) if $cgi_params;
+    my $path = 'attachment.cgi?' . join('&', @args);
+
+    # Make sure the attachment is served from the correct server.
+    if ($cgi->url_is_attachment_base($bug_id)) {
+
+      # No need to validate the token for public attachments. We cannot request
+      # credentials as we are on the alternate host.
+      if (!all_attachments_are_public(\%attachments)) {
+        my $token = $cgi->param('t');
+        my ($userid, undef, $token_data) = Bugzilla::Token::GetTokenData($token);
+        my %token_data  = unpack_token_data($token_data);
+        my $valid_token = 1;
         foreach my $field_name (@field_names) {
-            my $attachment = validateID($field_name, 1);
-            if (!$bug_id) {
-                $bug_id = $attachment->bug_id;
-            } elsif ($attachment->bug_id != $bug_id) {
-                ThrowUserError('attachment_bug_id_mismatch');
-            }
-            $attachments{$field_name} = $attachment;
-        }
-        my @args = map { $_ . '=' . $attachments{$_}->id } @field_names;
-        my $cgi_params = $cgi->canonicalise_query(@field_names, 't',
-            'Bugzilla_login', 'Bugzilla_password');
-        push(@args, $cgi_params) if $cgi_params;
-        my $path = 'attachment.cgi?' . join('&', @args);
-
-        # Make sure the attachment is served from the correct server.
-        if ($cgi->url_is_attachment_base($bug_id)) {
-            # No need to validate the token for public attachments. We cannot request
-            # credentials as we are on the alternate host.
-            if (!all_attachments_are_public(\%attachments)) {
-                my $token = $cgi->param('t');
-                my ($userid, undef, $token_data) = Bugzilla::Token::GetTokenData($token);
-                my %token_data = unpack_token_data($token_data);
-                my $valid_token = 1;
-                foreach my $field_name (@field_names) {
-                    my $token_id = $token_data{$field_name};
-                    if (!$token_id
-                        || !detaint_natural($token_id)
-                        || $attachments{$field_name}->id != $token_id)
-                    {
-                        $valid_token = 0;
-                        last;
-                    }
-                }
-                unless ($userid && $valid_token) {
-                    # Not a valid token.
-                    print $cgi->redirect('-location' => correct_urlbase() . $path);
-                    exit;
-                }
-                # Change current user without creating cookies.
-                Bugzilla->set_user(new Bugzilla::User($userid));
-                # Tokens are single use only, delete it.
-                delete_token($token);
-            }
+          my $token_id = $token_data{$field_name};
+          if ( !$token_id
+            || !detaint_natural($token_id)
+            || $attachments{$field_name}->id != $token_id)
+          {
+            $valid_token = 0;
+            last;
+          }
         }
-        elsif ($cgi->url_is_attachment_base) {
-            # If we come here, this means that each bug has its own host
-            # for attachments, and that we are trying to view one attachment
-            # using another bug's host. That's not desired.
-            $cgi->redirect_to_urlbase;
-        }
-        else {
-            # We couldn't call Bugzilla->login earlier as we first had to
-            # make sure we were not going to request credentials on the
-            # alternate host.
-            Bugzilla->login();
-            my $attachbase = Bugzilla->params->{'attachment_base'};
-            # Replace %bugid% by the ID of the bug the attachment 
-            # belongs to, if present.
-            $attachbase =~ s/\%bugid\%/$bug_id/;
-            if (all_attachments_are_public(\%attachments)) {
-                # No need for a token; redirect to attachment base.
-                print $cgi->redirect(-location => $attachbase . $path);
-                exit;
-            } else {
-                # Make sure the user can view the attachment.
-                foreach my $field_name (@field_names) {
-                    check_can_access($attachments{$field_name});
-                }
-                # Create a token and redirect.
-                my $token = url_quote(issue_session_token(pack_token_data(\%attachments)));
-                print $cgi->redirect(-location => $attachbase . "$path&t=$token");
-                exit;
-            }
+        unless ($userid && $valid_token) {
+
+          # Not a valid token.
+          print $cgi->redirect('-location' => correct_urlbase() . $path);
+          exit;
         }
-    } else {
-        do_ssl_redirect_if_required();
-        # No alternate host is used. Request credentials if required.
-        Bugzilla->login();
+
+        # Change current user without creating cookies.
+        Bugzilla->set_user(new Bugzilla::User($userid));
+
+        # Tokens are single use only, delete it.
+        delete_token($token);
+      }
+    }
+    elsif ($cgi->url_is_attachment_base) {
+
+      # If we come here, this means that each bug has its own host
+      # for attachments, and that we are trying to view one attachment
+      # using another bug's host. That's not desired.
+      $cgi->redirect_to_urlbase;
+    }
+    else {
+      # We couldn't call Bugzilla->login earlier as we first had to
+      # make sure we were not going to request credentials on the
+      # alternate host.
+      Bugzilla->login();
+      my $attachbase = Bugzilla->params->{'attachment_base'};
+
+      # Replace %bugid% by the ID of the bug the attachment
+      # belongs to, if present.
+      $attachbase =~ s/\%bugid\%/$bug_id/;
+      if (all_attachments_are_public(\%attachments)) {
+
+        # No need for a token; redirect to attachment base.
+        print $cgi->redirect(-location => $attachbase . $path);
+        exit;
+      }
+      else {
+        # Make sure the user can view the attachment.
         foreach my $field_name (@field_names) {
-            $attachments{$field_name} = validateID($field_name);
+          check_can_access($attachments{$field_name});
         }
+
+        # Create a token and redirect.
+        my $token = url_quote(issue_session_token(pack_token_data(\%attachments)));
+        print $cgi->redirect(-location => $attachbase . "$path&t=$token");
+        exit;
+      }
+    }
+  }
+  else {
+    do_ssl_redirect_if_required();
+
+    # No alternate host is used. Request credentials if required.
+    Bugzilla->login();
+    foreach my $field_name (@field_names) {
+      $attachments{$field_name} = validateID($field_name);
     }
+  }
 
-    return wantarray
-        ? map { $attachments{$_} } @field_names
-        : $attachments{$field_names[0]};
+  return
+    wantarray
+    ? map { $attachments{$_} } @field_names
+    : $attachments{$field_names[0]};
 }
 
 sub all_attachments_are_public {
-    my $attachments = shift;
-    foreach my $field_name (keys %$attachments) {
-        if (!attachmentIsPublic($attachments->{$field_name})) {
-            return 0;
-        }
+  my $attachments = shift;
+  foreach my $field_name (keys %$attachments) {
+    if (!attachmentIsPublic($attachments->{$field_name})) {
+      return 0;
     }
-    return 1;
+  }
+  return 1;
 }
 
 sub pack_token_data {
-    my $attachments = shift;
-    return join(' ', map { $_ . '=' . $attachments->{$_}->id } keys %$attachments);
+  my $attachments = shift;
+  return join(' ', map { $_ . '=' . $attachments->{$_}->id } keys %$attachments);
 }
 
 sub unpack_token_data {
-    my @token_data = split(/ /, shift || '');
-    my %data;
-    foreach my $token (@token_data) {
-        my ($field_name, $attach_id) = split('=', $token);
-        $data{$field_name} = $attach_id;
-    }
-    return %data;
+  my @token_data = split(/ /, shift || '');
+  my %data;
+  foreach my $token (@token_data) {
+    my ($field_name, $attach_id) = split('=', $token);
+    $data{$field_name} = $attach_id;
+  }
+  return %data;
 }
 
 ################################################################################
@@ -322,262 +328,284 @@ sub unpack_token_data {
 
 # Display an attachment.
 sub view {
-    my $attachment = get_attachment();
+  my $attachment = get_attachment();
 
-    # At this point, Bugzilla->login has been called if it had to.
-    my $contenttype = $attachment->contenttype;
-    my $filename = $attachment->filename;
+  # At this point, Bugzilla->login has been called if it had to.
+  my $contenttype = $attachment->contenttype;
+  my $filename    = $attachment->filename;
 
-    # Bug 111522: allow overriding content-type manually in the posted form
-    # params.
-    if (defined $cgi->param('content_type')) {
-        $contenttype = $attachment->_check_content_type($cgi->param('content_type'));
-    }
+  # Bug 111522: allow overriding content-type manually in the posted form
+  # params.
+  if (defined $cgi->param('content_type')) {
+    $contenttype = $attachment->_check_content_type($cgi->param('content_type'));
+  }
 
-    # Return the appropriate HTTP response headers.
-    $attachment->datasize || ThrowUserError("attachment_removed");
-
-    $filename =~ s/^.*[\/\\]//;
-    # escape quotes and backslashes in the filename, per RFCs 2045/822
-    $filename =~ s/\\/\\\\/g; # escape backslashes
-    $filename =~ s/"/\\"/g; # escape quotes
-
-    # Avoid line wrapping done by Encode, which we don't need for HTTP
-    # headers. See discussion in bug 328628 for details.
-    local $Encode::Encoding{'MIME-Q'}->{'bpl'} = 10000;
-    $filename = encode('MIME-Q', $filename);
-
-    my $disposition = Bugzilla->params->{'allow_attachment_display'} ? 'inline' : 'attachment';
-
-    # Don't send a charset header with attachments--they might not be UTF-8.
-    # However, we do allow people to explicitly specify a charset if they
-    # want.
-    if ($contenttype !~ /\bcharset=/i) {
-        # In order to prevent Apache from adding a charset, we have to send a
-        # charset that's a single space.
-        $cgi->charset(' ');
-        if (Bugzilla->feature('detect_charset') && $contenttype =~ /^text\//) {
-            my $encoding = detect_encoding($attachment->data);
-            if ($encoding) {
-                $cgi->charset(find_encoding($encoding)->mime_name);
-            }
-        }
+  # Return the appropriate HTTP response headers.
+  $attachment->datasize || ThrowUserError("attachment_removed");
+
+  $filename =~ s/^.*[\/\\]//;
+
+  # escape quotes and backslashes in the filename, per RFCs 2045/822
+  $filename =~ s/\\/\\\\/g;    # escape backslashes
+  $filename =~ s/"/\\"/g;      # escape quotes
+
+  # Avoid line wrapping done by Encode, which we don't need for HTTP
+  # headers. See discussion in bug 328628 for details.
+  local $Encode::Encoding{'MIME-Q'}->{'bpl'} = 10000;
+  $filename = encode('MIME-Q', $filename);
+
+  my $disposition
+    = Bugzilla->params->{'allow_attachment_display'} ? 'inline' : 'attachment';
+
+  # Don't send a charset header with attachments--they might not be UTF-8.
+  # However, we do allow people to explicitly specify a charset if they
+  # want.
+  if ($contenttype !~ /\bcharset=/i) {
+
+    # In order to prevent Apache from adding a charset, we have to send a
+    # charset that's a single space.
+    $cgi->charset(' ');
+    if (Bugzilla->feature('detect_charset') && $contenttype =~ /^text\//) {
+      my $encoding = detect_encoding($attachment->data);
+      if ($encoding) {
+        $cgi->charset(find_encoding($encoding)->mime_name);
+      }
     }
-    print $cgi->header(-type=>"$contenttype; name=\"$filename\"",
-                       -content_disposition=> "$disposition; filename=\"$filename\"",
-                       -content_length => $attachment->datasize);
-    disable_utf8();
-    print $attachment->data;
+  }
+  print $cgi->header(
+    -type                => "$contenttype; name=\"$filename\"",
+    -content_disposition => "$disposition; filename=\"$filename\"",
+    -content_length      => $attachment->datasize
+  );
+  disable_utf8();
+  print $attachment->data;
 }
 
 sub interdiff {
-    # Retrieve and validate parameters
-    my $format = validateFormat('html', 'raw');
-    my($old_attachment, $new_attachment);
-    if ($format eq 'raw') {
-        ($old_attachment, $new_attachment) = get_attachment('oldid', 'newid');
-    } else {
-        $old_attachment = validateID('oldid');
-        $new_attachment = validateID('newid');
-    }
 
-    Bugzilla::Attachment::PatchReader::process_interdiff(
-        $old_attachment, $new_attachment, $format);
+  # Retrieve and validate parameters
+  my $format = validateFormat('html', 'raw');
+  my ($old_attachment, $new_attachment);
+  if ($format eq 'raw') {
+    ($old_attachment, $new_attachment) = get_attachment('oldid', 'newid');
+  }
+  else {
+    $old_attachment = validateID('oldid');
+    $new_attachment = validateID('newid');
+  }
+
+  Bugzilla::Attachment::PatchReader::process_interdiff($old_attachment,
+    $new_attachment, $format);
 }
 
 sub diff {
-    # Retrieve and validate parameters
-    my $format = validateFormat('html', 'raw');
-    my $attachment = $format eq 'raw' ? get_attachment() : validateID();
-
-    # If it is not a patch, view normally.
-    if (!$attachment->ispatch) {
-        view();
-        return;
-    }
 
-    Bugzilla::Attachment::PatchReader::process_diff($attachment, $format);
+  # Retrieve and validate parameters
+  my $format = validateFormat('html', 'raw');
+  my $attachment = $format eq 'raw' ? get_attachment() : validateID();
+
+  # If it is not a patch, view normally.
+  if (!$attachment->ispatch) {
+    view();
+    return;
+  }
+
+  Bugzilla::Attachment::PatchReader::process_diff($attachment, $format);
 }
 
 # Display all attachments for a given bug in a series of IFRAMEs within one
 # HTML page.
 sub viewall {
-    # Retrieve and validate parameters
-    my $bug = Bugzilla::Bug->check({ id => scalar $cgi->param('bugid'), cache => 1 });
 
-    my $attachments = Bugzilla::Attachment->get_attachments_by_bug($bug);
-    # Ignore deleted attachments.
-    @$attachments = grep { $_->datasize } @$attachments;
+  # Retrieve and validate parameters
+  my $bug = Bugzilla::Bug->check({id => scalar $cgi->param('bugid'), cache => 1});
 
-    if ($cgi->param('hide_obsolete')) {
-        @$attachments = grep { !$_->isobsolete } @$attachments;
-        $vars->{'hide_obsolete'} = 1;
-    }
+  my $attachments = Bugzilla::Attachment->get_attachments_by_bug($bug);
 
-    # Define the variables and functions that will be passed to the UI template.
-    $vars->{'bug'} = $bug;
-    $vars->{'attachments'} = $attachments;
+  # Ignore deleted attachments.
+  @$attachments = grep { $_->datasize } @$attachments;
 
-    print $cgi->header();
+  if ($cgi->param('hide_obsolete')) {
+    @$attachments = grep { !$_->isobsolete } @$attachments;
+    $vars->{'hide_obsolete'} = 1;
+  }
 
-    # Generate and return the UI (HTML page) from the appropriate template.
-    $template->process("attachment/show-multiple.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
+  # Define the variables and functions that will be passed to the UI template.
+  $vars->{'bug'}         = $bug;
+  $vars->{'attachments'} = $attachments;
+
+  print $cgi->header();
+
+  # Generate and return the UI (HTML page) from the appropriate template.
+  $template->process("attachment/show-multiple.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
 }
 
 # Display a form for entering a new attachment.
 sub enter {
-    # Retrieve and validate parameters
-    my $bug = Bugzilla::Bug->check(scalar $cgi->param('bugid'));
-    my $bugid = $bug->id;
-    Bugzilla::Attachment->_check_bug($bug);
-    my $dbh = Bugzilla->dbh;
-    my $user = Bugzilla->user;
-
-    # Retrieve the attachments the user can edit from the database and write
-    # them into an array of hashes where each hash represents one attachment.
-  
-    my ($can_edit, $not_private) = ('', '');
-    if (!$user->in_group('editbugs', $bug->product_id)) {
-        $can_edit = "AND submitter_id = " . $user->id;
-    }
-    if (!$user->is_insider) {
-        $not_private = "AND isprivate = 0";
-    }
-    my $attach_ids = $dbh->selectcol_arrayref(
-        "SELECT attach_id
+
+  # Retrieve and validate parameters
+  my $bug   = Bugzilla::Bug->check(scalar $cgi->param('bugid'));
+  my $bugid = $bug->id;
+  Bugzilla::Attachment->_check_bug($bug);
+  my $dbh  = Bugzilla->dbh;
+  my $user = Bugzilla->user;
+
+  # Retrieve the attachments the user can edit from the database and write
+  # them into an array of hashes where each hash represents one attachment.
+
+  my ($can_edit, $not_private) = ('', '');
+  if (!$user->in_group('editbugs', $bug->product_id)) {
+    $can_edit = "AND submitter_id = " . $user->id;
+  }
+  if (!$user->is_insider) {
+    $not_private = "AND isprivate = 0";
+  }
+  my $attach_ids = $dbh->selectcol_arrayref(
+    "SELECT attach_id
            FROM attachments
           WHERE bug_id = ?
                 AND isobsolete = 0
                 $can_edit $not_private
-       ORDER BY attach_id",
-         undef, $bugid);
-
-    # Define the variables and functions that will be passed to the UI template.
-    $vars->{'bug'} = $bug;
-    $vars->{'attachments'} = Bugzilla::Attachment->new_from_list($attach_ids);
-
-    my $flag_types = Bugzilla::FlagType::match({
-        'target_type'  => 'attachment',
-        'product_id'   => $bug->product_id,
-        'component_id' => $bug->component_id
-    });
-    $vars->{'flag_types'} = $flag_types;
-    $vars->{'any_flags_requesteeble'} =
-        grep { $_->is_requestable && $_->is_requesteeble } @$flag_types;
-    $vars->{'token'} = issue_session_token('create_attachment');
-
-    print $cgi->header();
-
-    # Generate and return the UI (HTML page) from the appropriate template.
-    $template->process("attachment/create.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
+       ORDER BY attach_id", undef, $bugid
+  );
+
+  # Define the variables and functions that will be passed to the UI template.
+  $vars->{'bug'}         = $bug;
+  $vars->{'attachments'} = Bugzilla::Attachment->new_from_list($attach_ids);
+
+  my $flag_types = Bugzilla::FlagType::match({
+    'target_type'  => 'attachment',
+    'product_id'   => $bug->product_id,
+    'component_id' => $bug->component_id
+  });
+  $vars->{'flag_types'} = $flag_types;
+  $vars->{'any_flags_requesteeble'}
+    = grep { $_->is_requestable && $_->is_requesteeble } @$flag_types;
+  $vars->{'token'} = issue_session_token('create_attachment');
+
+  print $cgi->header();
+
+  # Generate and return the UI (HTML page) from the appropriate template.
+  $template->process("attachment/create.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
 }
 
 # Insert a new attachment into the database.
 sub insert {
-    my $dbh = Bugzilla->dbh;
-    my $user = Bugzilla->user;
-
-    $dbh->bz_start_transaction;
+  my $dbh  = Bugzilla->dbh;
+  my $user = Bugzilla->user;
+
+  $dbh->bz_start_transaction;
+
+  # Retrieve and validate parameters
+  my $bug         = Bugzilla::Bug->check(scalar $cgi->param('bugid'));
+  my $bugid       = $bug->id;
+  my ($timestamp) = $dbh->selectrow_array("SELECT NOW()");
+
+  # Detect if the user already used the same form to submit an attachment
+  my $token = trim($cgi->param('token'));
+  check_token_data($token, 'create_attachment', 'index.cgi');
+
+  # Check attachments the user tries to mark as obsolete.
+  my @obsolete_attachments;
+  if ($cgi->param('obsolete')) {
+    my @obsolete = $cgi->param('obsolete');
+    @obsolete_attachments
+      = Bugzilla::Attachment->validate_obsolete($bug, \@obsolete);
+  }
 
-    # Retrieve and validate parameters
-    my $bug = Bugzilla::Bug->check(scalar $cgi->param('bugid'));
-    my $bugid = $bug->id;
-    my ($timestamp) = $dbh->selectrow_array("SELECT NOW()");
+  # Must be called before create() as it may alter $cgi->param('ispatch').
+  my $content_type = Bugzilla::Attachment::get_content_type();
+
+  # 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        => $attach_text || $data_fh,
+    description => scalar $cgi->param('description'),
+    filename    => $attach_text ? "file_$bugid.txt" : $data_fh,
+    ispatch     => scalar $cgi->param('ispatch'),
+    isprivate   => scalar $cgi->param('isprivate'),
+    mimetype    => $content_type,
+  });
+
+  # Delete the token used to create this attachment.
+  delete_token($token);
+
+  foreach my $obsolete_attachment (@obsolete_attachments) {
+    $obsolete_attachment->set_is_obsolete(1);
+    $obsolete_attachment->update($timestamp);
+  }
 
-    # Detect if the user already used the same form to submit an attachment
-    my $token = trim($cgi->param('token'));
-    check_token_data($token, 'create_attachment', 'index.cgi');
+  my ($flags, $new_flags)
+    = Bugzilla::Flag->extract_flags_from_cgi($bug, $attachment, $vars,
+    SKIP_REQUESTEE_ON_ERROR);
+  $attachment->set_flags($flags, $new_flags);
 
-    # Check attachments the user tries to mark as obsolete.
-    my @obsolete_attachments;
-    if ($cgi->param('obsolete')) {
-        my @obsolete = $cgi->param('obsolete');
-        @obsolete_attachments = Bugzilla::Attachment->validate_obsolete($bug, \@obsolete);
+  # Insert a comment about the new attachment into the database.
+  my $comment = $cgi->param('comment');
+  $comment = '' unless defined $comment;
+  $bug->add_comment(
+    $comment,
+    {
+      isprivate  => $attachment->isprivate,
+      type       => CMT_ATTACHMENT_CREATED,
+      extra_data => $attachment->id
     }
+  );
 
-    # Must be called before create() as it may alter $cgi->param('ispatch').
-    my $content_type = Bugzilla::Attachment::get_content_type();
-
-    # 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          => $attach_text || $data_fh,
-         description   => scalar $cgi->param('description'),
-         filename      => $attach_text ? "file_$bugid.txt" : $data_fh,
-         ispatch       => scalar $cgi->param('ispatch'),
-         isprivate     => scalar $cgi->param('isprivate'),
-         mimetype      => $content_type,
-         });
-
-    # Delete the token used to create this attachment.
-    delete_token($token);
+  # Assign the bug to the user, if they are allowed to take it
+  my $owner = "";
+  if ($cgi->param('takebug') && $user->in_group('editbugs', $bug->product_id)) {
 
-    foreach my $obsolete_attachment (@obsolete_attachments) {
-        $obsolete_attachment->set_is_obsolete(1);
-        $obsolete_attachment->update($timestamp);
+    # When taking a bug, we have to follow the workflow.
+    my $bug_status = $cgi->param('bug_status') || '';
+    ($bug_status) = grep { $_->name eq $bug_status } @{$bug->status->can_change_to};
+
+    if ( $bug_status
+      && $bug_status->is_open
+      && ($bug_status->name ne 'UNCONFIRMED' || $bug->product_obj->allows_unconfirmed)
+      )
+    {
+      $bug->set_bug_status($bug_status->name);
+      $bug->clear_resolution();
     }
 
-    my ($flags, $new_flags) = Bugzilla::Flag->extract_flags_from_cgi(
-                                  $bug, $attachment, $vars, SKIP_REQUESTEE_ON_ERROR);
-    $attachment->set_flags($flags, $new_flags);
+    # Make sure the person we are taking the bug from gets mail.
+    $owner = $bug->assigned_to->login;
+    $bug->set_assigned_to($user);
+  }
 
-    # Insert a comment about the new attachment into the database.
-    my $comment = $cgi->param('comment');
-    $comment = '' unless defined $comment;
-    $bug->add_comment($comment, { isprivate => $attachment->isprivate,
-                                  type => CMT_ATTACHMENT_CREATED,
-                                  extra_data => $attachment->id });
-
-    # Assign the bug to the user, if they are allowed to take it
-    my $owner = "";
-    if ($cgi->param('takebug') && $user->in_group('editbugs', $bug->product_id)) {
-        # When taking a bug, we have to follow the workflow.
-        my $bug_status = $cgi->param('bug_status') || '';
-        ($bug_status) = grep { $_->name eq $bug_status }
-                        @{ $bug->status->can_change_to };
-
-        if ($bug_status && $bug_status->is_open
-            && ($bug_status->name ne 'UNCONFIRMED'
-                || $bug->product_obj->allows_unconfirmed))
-        {
-            $bug->set_bug_status($bug_status->name);
-            $bug->clear_resolution();
-        }
-        # Make sure the person we are taking the bug from gets mail.
-        $owner = $bug->assigned_to->login;
-        $bug->set_assigned_to($user);
-    }
+  $bug->add_cc($user) if $cgi->param('addselfcc');
+  $bug->update($timestamp);
 
-    $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);
 
-    # We have to update the attachment after updating the bug, to ensure new
-    # comments are available.
-    $attachment->update($timestamp);
+  $dbh->bz_commit_transaction;
 
-    $dbh->bz_commit_transaction;
+  # Define the variables and functions that will be passed to the UI template.
+  $vars->{'attachment'} = $attachment;
 
-    # Define the variables and functions that will be passed to the UI template.
-    $vars->{'attachment'} = $attachment;
-    # We cannot reuse the $bug object as delta_ts has eventually been updated
-    # since the object was created.
-    $vars->{'bugs'} = [new Bugzilla::Bug($bugid)];
-    $vars->{'header_done'} = 1;
-    $vars->{'contenttypemethod'} = $cgi->param('contenttypemethod');
+  # We cannot reuse the $bug object as delta_ts has eventually been updated
+  # since the object was created.
+  $vars->{'bugs'}              = [new Bugzilla::Bug($bugid)];
+  $vars->{'header_done'}       = 1;
+  $vars->{'contenttypemethod'} = $cgi->param('contenttypemethod');
 
-    my $recipients = { 'changer' => $user, 'owner' => $owner };
-    $vars->{'sent_bugmail'} = Bugzilla::BugMail::Send($bugid, $recipients);
+  my $recipients = {'changer' => $user, 'owner' => $owner};
+  $vars->{'sent_bugmail'} = Bugzilla::BugMail::Send($bugid, $recipients);
 
-    print $cgi->header();
-    # Generate and return the UI (HTML page) from the appropriate template.
-    $template->process("attachment/created.html.tmpl", $vars)
-        || ThrowTemplateError($template->error());
+  print $cgi->header();
+
+  # Generate and return the UI (HTML page) from the appropriate template.
+  $template->process("attachment/created.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
 }
 
 # Displays a form for editing attachment properties.
@@ -585,227 +613,237 @@ sub insert {
 # is private and the user does not belong to the insider group.
 # Validations are done later when the user submits changes.
 sub edit {
-    my $attachment = validateID();
+  my $attachment = validateID();
 
-    my $bugattachments =
-        Bugzilla::Attachment->get_attachments_by_bug($attachment->bug);
+  my $bugattachments
+    = Bugzilla::Attachment->get_attachments_by_bug($attachment->bug);
 
-    my $any_flags_requesteeble = grep { $_->is_requestable && $_->is_requesteeble }
-                                 @{ $attachment->flag_types };
-    # Useful in case a flagtype is no longer requestable but a requestee
-    # has been set before we turned off that bit.
-    $any_flags_requesteeble ||= grep { $_->requestee_id } @{ $attachment->flags };
-    $vars->{'any_flags_requesteeble'} = $any_flags_requesteeble;
-    $vars->{'attachment'} = $attachment;
-    $vars->{'attachments'} = $bugattachments;
+  my $any_flags_requesteeble = grep { $_->is_requestable && $_->is_requesteeble }
+    @{$attachment->flag_types};
 
-    print $cgi->header();
+  # Useful in case a flagtype is no longer requestable but a requestee
+  # has been set before we turned off that bit.
+  $any_flags_requesteeble ||= grep { $_->requestee_id } @{$attachment->flags};
+  $vars->{'any_flags_requesteeble'} = $any_flags_requesteeble;
+  $vars->{'attachment'}             = $attachment;
+  $vars->{'attachments'}            = $bugattachments;
 
-    # Generate and return the UI (HTML page) from the appropriate template.
-    $template->process("attachment/edit.html.tmpl", $vars)
-        || ThrowTemplateError($template->error());
+  print $cgi->header();
+
+  # Generate and return the UI (HTML page) from the appropriate template.
+  $template->process("attachment/edit.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
 }
 
 # Updates an attachment record. Only users with "editbugs" privileges,
 # (or the original attachment's submitter) can edit the attachment.
 # Users cannot edit the content of the attachment itself.
 sub update {
-    my $user = Bugzilla->user;
-    my $dbh = Bugzilla->dbh;
-
-    # Start a transaction in preparation for updating the attachment.
-    $dbh->bz_start_transaction();
-
-    # Retrieve and validate parameters
-    my $attachment = validateID();
-    my $bug = $attachment->bug;
-    $attachment->_check_bug;
-    my $can_edit = $attachment->validate_can_edit;
-
-    if ($can_edit) {
-        $attachment->set_description(scalar $cgi->param('description'));
-        $attachment->set_is_patch(scalar $cgi->param('ispatch'));
-        $attachment->set_content_type(scalar $cgi->param('contenttypeentry'));
-        $attachment->set_is_obsolete(scalar $cgi->param('isobsolete'));
-        $attachment->set_is_private(scalar $cgi->param('isprivate'));
-        $attachment->set_filename(scalar $cgi->param('filename'));
-
-        # Now make sure the attachment has not been edited since we loaded the page.
-        my $delta_ts = $cgi->param('delta_ts');
-        my $modification_time = $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', $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();
-                # Warn the user about the mid-air collision and ask them what to do.
-                $template->process("attachment/midair.html.tmpl", $vars)
-                  || ThrowTemplateError($template->error());
-                exit;
-            }
-        }
-    }
+  my $user = Bugzilla->user;
+  my $dbh  = Bugzilla->dbh;
+
+  # Start a transaction in preparation for updating the attachment.
+  $dbh->bz_start_transaction();
+
+  # Retrieve and validate parameters
+  my $attachment = validateID();
+  my $bug        = $attachment->bug;
+  $attachment->_check_bug;
+  my $can_edit = $attachment->validate_can_edit;
+
+  if ($can_edit) {
+    $attachment->set_description(scalar $cgi->param('description'));
+    $attachment->set_is_patch(scalar $cgi->param('ispatch'));
+    $attachment->set_content_type(scalar $cgi->param('contenttypeentry'));
+    $attachment->set_is_obsolete(scalar $cgi->param('isobsolete'));
+    $attachment->set_is_private(scalar $cgi->param('isprivate'));
+    $attachment->set_filename(scalar $cgi->param('filename'));
+
+    # Now make sure the attachment has not been edited since we loaded the page.
+    my $delta_ts          = $cgi->param('delta_ts');
+    my $modification_time = $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', $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;
 
-    # We couldn't do this check earlier as we first had to validate attachment ID
-    # and display the mid-air collision page if modification_time changed.
-    my $token = $cgi->param('token');
-    check_hash_token($token, [$attachment->id, $attachment->modification_time]);
-
-    # If the user submitted a comment while editing the attachment,
-    # add the comment to the bug. Do this after having validated isprivate!
-    my $comment = $cgi->param('comment');
-    if (defined $comment && trim($comment) ne '') {
-        $bug->add_comment($comment, { isprivate => $attachment->isprivate,
-                                      type => CMT_ATTACHMENT_UPDATED,
-                                      extra_data => $attachment->id });
+        print $cgi->header();
+
+        # Warn the user about the mid-air collision and ask them what to do.
+        $template->process("attachment/midair.html.tmpl", $vars)
+          || ThrowTemplateError($template->error());
+        exit;
+      }
     }
+  }
 
-    $bug->add_cc($user) if $cgi->param('addselfcc');
+  # We couldn't do this check earlier as we first had to validate attachment ID
+  # and display the mid-air collision page if modification_time changed.
+  my $token = $cgi->param('token');
+  check_hash_token($token, [$attachment->id, $attachment->modification_time]);
+
+  # If the user submitted a comment while editing the attachment,
+  # add the comment to the bug. Do this after having validated isprivate!
+  my $comment = $cgi->param('comment');
+  if (defined $comment && trim($comment) ne '') {
+    $bug->add_comment(
+      $comment,
+      {
+        isprivate  => $attachment->isprivate,
+        type       => CMT_ATTACHMENT_UPDATED,
+        extra_data => $attachment->id
+      }
+    );
+  }
+
+  $bug->add_cc($user) if $cgi->param('addselfcc');
 
-    my ($flags, $new_flags) =
-      Bugzilla::Flag->extract_flags_from_cgi($bug, $attachment, $vars);
+  my ($flags, $new_flags)
+    = Bugzilla::Flag->extract_flags_from_cgi($bug, $attachment, $vars);
 
-    if ($can_edit) {
-        $attachment->set_flags($flags, $new_flags);
+  if ($can_edit) {
+    $attachment->set_flags($flags, $new_flags);
+  }
+
+  # Requestees can set flags targetted to them, even if they cannot
+  # edit the attachment. Flag setters can edit their own flags too.
+  elsif (scalar @$flags) {
+    my %flag_list = map { $_->{id} => $_ } @$flags;
+    my $flag_objs = Bugzilla::Flag->new_from_list([keys %flag_list]);
+
+    my @editable_flags;
+    foreach my $flag_obj (@$flag_objs) {
+      if ($flag_obj->setter_id == $user->id
+        || ($flag_obj->requestee_id && $flag_obj->requestee_id == $user->id))
+      {
+        push(@editable_flags, $flag_list{$flag_obj->id});
+      }
     }
-    # Requestees can set flags targetted to them, even if they cannot
-    # edit the attachment. Flag setters can edit their own flags too.
-    elsif (scalar @$flags) {
-        my %flag_list = map { $_->{id} => $_ } @$flags;
-        my $flag_objs = Bugzilla::Flag->new_from_list([keys %flag_list]);
-
-        my @editable_flags;
-        foreach my $flag_obj (@$flag_objs) {
-            if ($flag_obj->setter_id == $user->id
-                || ($flag_obj->requestee_id && $flag_obj->requestee_id == $user->id))
-            {
-                push(@editable_flags, $flag_list{$flag_obj->id});
-            }
-        }
 
-        if (scalar @editable_flags) {
-            $attachment->set_flags(\@editable_flags, []);
-            # Flag changes must be committed.
-            $can_edit = 1;
-        }
+    if (scalar @editable_flags) {
+      $attachment->set_flags(\@editable_flags, []);
+
+      # Flag changes must be committed.
+      $can_edit = 1;
     }
+  }
 
-    # Figure out when the changes were made.
-    my $timestamp = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
+  # 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);
+  # 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
-        # reflect this change in the bug object.
-        $bug->{delta_ts} = $timestamp if scalar(keys %$changes);
-    }
+  if ($can_edit) {
+    my $changes = $attachment->update($timestamp);
 
-    # Commit the transaction now that we are finished updating the database.
-    $dbh->bz_commit_transaction();
+    # If there are changes, we updated delta_ts in the DB. We have to
+    # reflect this change in the bug object.
+    $bug->{delta_ts} = $timestamp if scalar(keys %$changes);
+  }
 
-    # Define the variables and functions that will be passed to the UI template.
-    $vars->{'attachment'} = $attachment;
-    $vars->{'bugs'} = [$bug];
-    $vars->{'header_done'} = 1;
-    $vars->{'sent_bugmail'} = 
-        Bugzilla::BugMail::Send($bug->id, { 'changer' => $user });
+  # Commit the transaction now that we are finished updating the database.
+  $dbh->bz_commit_transaction();
 
-    print $cgi->header();
+  # Define the variables and functions that will be passed to the UI template.
+  $vars->{'attachment'}  = $attachment;
+  $vars->{'bugs'}        = [$bug];
+  $vars->{'header_done'} = 1;
+  $vars->{'sent_bugmail'}
+    = Bugzilla::BugMail::Send($bug->id, {'changer' => $user});
 
-    # Generate and return the UI (HTML page) from the appropriate template.
-    $template->process("attachment/updated.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
+  print $cgi->header();
+
+  # Generate and return the UI (HTML page) from the appropriate template.
+  $template->process("attachment/updated.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
 }
 
 # Only administrators can delete attachments.
 sub delete_attachment {
-    my $user = Bugzilla->login(LOGIN_REQUIRED);
-    my $dbh = Bugzilla->dbh;
+  my $user = Bugzilla->login(LOGIN_REQUIRED);
+  my $dbh  = Bugzilla->dbh;
 
-    print $cgi->header();
+  print $cgi->header();
 
-    $user->in_group('admin')
-      || ThrowUserError('auth_failure', {group  => 'admin',
-                                         action => 'delete',
-                                         object => 'attachment'});
-
-    Bugzilla->params->{'allow_attachment_deletion'}
-      || ThrowUserError('attachment_deletion_disabled');
-
-    # Make sure the administrator is allowed to edit this attachment.
-    my $attachment = validateID();
-    Bugzilla::Attachment->_check_bug($attachment->bug);
-
-    $attachment->datasize || ThrowUserError('attachment_removed');
-
-    # We don't want to let a malicious URL accidentally delete an attachment.
-    my $token = trim($cgi->param('token'));
-    if ($token) {
-        my ($creator_id, $date, $event) = Bugzilla::Token::GetTokenData($token);
-        unless ($creator_id
-                  && ($creator_id == $user->id)
-                  && ($event eq 'delete_attachment' . $attachment->id))
-        {
-            # The token is invalid.
-            ThrowUserError('token_does_not_exist');
-        }
+  $user->in_group('admin')
+    || ThrowUserError('auth_failure',
+    {group => 'admin', action => 'delete', object => 'attachment'});
 
-        my $bug = new Bugzilla::Bug($attachment->bug_id);
+  Bugzilla->params->{'allow_attachment_deletion'}
+    || ThrowUserError('attachment_deletion_disabled');
 
-        # The token is valid. Delete the content of the attachment.
-        my $msg;
-        $vars->{'attachment'} = $attachment;
-        $vars->{'reason'} = clean_text($cgi->param('reason') || '');
+  # Make sure the administrator is allowed to edit this attachment.
+  my $attachment = validateID();
+  Bugzilla::Attachment->_check_bug($attachment->bug);
 
-        $template->process("attachment/delete_reason.txt.tmpl", $vars, \$msg)
-          || ThrowTemplateError($template->error());
+  $attachment->datasize || ThrowUserError('attachment_removed');
+
+  # We don't want to let a malicious URL accidentally delete an attachment.
+  my $token = trim($cgi->param('token'));
+  if ($token) {
+    my ($creator_id, $date, $event) = Bugzilla::Token::GetTokenData($token);
+    unless ($creator_id
+      && ($creator_id == $user->id)
+      && ($event eq 'delete_attachment' . $attachment->id))
+    {
+      # The token is invalid.
+      ThrowUserError('token_does_not_exist');
+    }
 
-        # Paste the reason provided by the admin into a comment.
-        $bug->add_comment($msg);
+    my $bug = new Bugzilla::Bug($attachment->bug_id);
 
-        $attachment->remove_from_db();
+    # The token is valid. Delete the content of the attachment.
+    my $msg;
+    $vars->{'attachment'} = $attachment;
+    $vars->{'reason'} = clean_text($cgi->param('reason') || '');
 
-        # Now delete the token.
-        delete_token($token);
+    $template->process("attachment/delete_reason.txt.tmpl", $vars, \$msg)
+      || ThrowTemplateError($template->error());
 
-        # Insert the comment.
-        $bug->update();
+    # Paste the reason provided by the admin into a comment.
+    $bug->add_comment($msg);
 
-        # Required to display the bug the deleted attachment belongs to.
-        $vars->{'bugs'} = [$bug];
-        $vars->{'header_done'} = 1;
+    $attachment->remove_from_db();
 
-        $vars->{'sent_bugmail'} =
-            Bugzilla::BugMail::Send($bug->id, { 'changer' => $user });
+    # Now delete the token.
+    delete_token($token);
 
-        $template->process("attachment/updated.html.tmpl", $vars)
-          || ThrowTemplateError($template->error());
-    }
-    else {
-        # Create a token.
-        $token = issue_session_token('delete_attachment' . $attachment->id);
+    # Insert the comment.
+    $bug->update();
 
-        $vars->{'a'} = $attachment;
-        $vars->{'token'} = $token;
+    # Required to display the bug the deleted attachment belongs to.
+    $vars->{'bugs'}        = [$bug];
+    $vars->{'header_done'} = 1;
 
-        $template->process("attachment/confirm-delete.html.tmpl", $vars)
-          || ThrowTemplateError($template->error());
-    }
+    $vars->{'sent_bugmail'}
+      = Bugzilla::BugMail::Send($bug->id, {'changer' => $user});
+
+    $template->process("attachment/updated.html.tmpl", $vars)
+      || ThrowTemplateError($template->error());
+  }
+  else {
+    # Create a token.
+    $token = issue_session_token('delete_attachment' . $attachment->id);
+
+    $vars->{'a'}     = $attachment;
+    $vars->{'token'} = $token;
+
+    $template->process("attachment/confirm-delete.html.tmpl", $vars)
+      || ThrowTemplateError($template->error());
+  }
 }
diff --git a/buglist.cgi b/buglist.cgi
index daee34c9b..aa0faa426 100755
--- a/buglist.cgi
+++ b/buglist.cgi
@@ -28,10 +28,10 @@ use Bugzilla::Token;
 
 use Date::Parse;
 
-my $cgi = Bugzilla->cgi;
-my $dbh = Bugzilla->dbh;
+my $cgi      = Bugzilla->cgi;
+my $dbh      = Bugzilla->dbh;
 my $template = Bugzilla->template;
-my $vars = {};
+my $vars     = {};
 
 # We have to check the login here to get the correct footer if an error is
 # thrown and to prevent a logged out user to use QuickSearch if 'requirelogin'
@@ -42,26 +42,28 @@ $cgi->redirect_search_url();
 
 my $buffer = $cgi->query_string();
 if (length($buffer) == 0) {
-    ThrowUserError("buglist_parameters_required");
+  ThrowUserError("buglist_parameters_required");
 }
 
 
 # Determine whether this is a quicksearch query.
 my $searchstring = $cgi->param('quicksearch');
 if (defined($searchstring)) {
-    $buffer = quicksearch($searchstring);
-    # Quicksearch may do a redirect, in which case it does not return.
-    # If it does return, it has modified $cgi->params so we can use them here
-    # as if this had been a normal query from the beginning.
+  $buffer = quicksearch($searchstring);
+
+  # Quicksearch may do a redirect, in which case it does not return.
+  # If it does return, it has modified $cgi->params so we can use them here
+  # as if this had been a normal query from the beginning.
 }
 
 # If configured to not allow empty words, reject empty searches from the
-# Find a Specific Bug search form, including words being a single or 
+# Find a Specific Bug search form, including words being a single or
 # several consecutive whitespaces only.
-if (!Bugzilla->params->{'search_allow_no_criteria'}
-    && defined($cgi->param('content')) && $cgi->param('content') =~ /^\s*$/)
+if ( !Bugzilla->params->{'search_allow_no_criteria'}
+  && defined($cgi->param('content'))
+  && $cgi->param('content') =~ /^\s*$/)
 {
-    ThrowUserError("buglist_parameters_required");
+  ThrowUserError("buglist_parameters_required");
 }
 
 ################################################################################
@@ -73,26 +75,31 @@ my $dotweak = $cgi->param('tweak') ? 1 : 0;
 
 # Log the user in
 if ($dotweak) {
-    Bugzilla->login(LOGIN_REQUIRED);
+  Bugzilla->login(LOGIN_REQUIRED);
 }
 
 # Hack to support legacy applications that think the RDF ctype is at format=rdf.
-if (defined $cgi->param('format') && $cgi->param('format') eq "rdf"
-    && !defined $cgi->param('ctype')) {
-    $cgi->param('ctype', "rdf");
-    $cgi->delete('format');
+if ( defined $cgi->param('format')
+  && $cgi->param('format') eq "rdf"
+  && !defined $cgi->param('ctype'))
+{
+  $cgi->param('ctype', "rdf");
+  $cgi->delete('format');
 }
 
 # Treat requests for ctype=rss as requests for ctype=atom
 if (defined $cgi->param('ctype') && $cgi->param('ctype') eq "rss") {
-    $cgi->param('ctype', "atom");
+  $cgi->param('ctype', "atom");
 }
 
 # Determine the format in which the user would like to receive the output.
 # Uses the default format if the user did not specify an output format;
 # otherwise validates the user's choice against the list of available formats.
-my $format = $template->get_format("list/list", scalar $cgi->param('format'),
-                                   scalar $cgi->param('ctype'));
+my $format = $template->get_format(
+  "list/list",
+  scalar $cgi->param('format'),
+  scalar $cgi->param('ctype')
+);
 
 # Use server push to display a "Please wait..." message for the user while
 # executing their query if their browser supports it and they are viewing
@@ -102,14 +109,13 @@ my $format = $template->get_format("list/list", scalar $cgi->param('format'),
 # Server push is compatible with Gecko-based browsers and Opera, but not with
 # MSIE, Lynx or Safari (bug 441496).
 
-my $serverpush =
-  $format->{'extension'} eq "html"
-    && exists $ENV{'HTTP_USER_AGENT'} 
-      && $ENV{'HTTP_USER_AGENT'} =~ /(Mozilla.[3-9]|Opera)/
-        && $ENV{'HTTP_USER_AGENT'} !~ /compatible/i
-          && $ENV{'HTTP_USER_AGENT'} !~ /(?:WebKit|Trident|KHTML)/
-            && !defined($cgi->param('serverpush'))
-              || $cgi->param('serverpush');
+my $serverpush
+  = $format->{'extension'} eq "html"
+  && exists $ENV{'HTTP_USER_AGENT'}
+  && $ENV{'HTTP_USER_AGENT'} =~ /(Mozilla.[3-9]|Opera)/
+  && $ENV{'HTTP_USER_AGENT'} !~ /compatible/i
+  && $ENV{'HTTP_USER_AGENT'} !~ /(?:WebKit|Trident|KHTML)/
+  && !defined($cgi->param('serverpush')) || $cgi->param('serverpush');
 
 my $order = $cgi->param('order') || "";
 
@@ -119,25 +125,26 @@ my $params;
 # If the user is retrieving the last bug list they looked at, hack the buffer
 # storing the query string so that it looks like a query retrieving those bugs.
 if (my $last_list = $cgi->param('regetlastlist')) {
-    my $bug_ids;
-
-    # Logged-out users use the old cookie method for storing the last search.
-    if (!$user->id or $last_list eq 'cookie') {
-        $bug_ids = $cgi->cookie('BUGLIST') or ThrowUserError("missing_cookie");
-        $bug_ids =~ s/[:-]/,/g;
-        $order ||= "reuse last sort";
-    }
-    # But logged in users store the last X searches in the DB so they can
-    # have multiple bug lists available.
-    else {
-        my $last_search = Bugzilla::Search::Recent->check(
-            { id => $last_list });
-        $bug_ids = join(',', @{ $last_search->bug_list });
-        $order ||= $last_search->list_order;
-    }
-    # set up the params for this new query
-    $params = new Bugzilla::CGI({ bug_id => $bug_ids, order => $order });
-    $params->param('list_id', $last_list);
+  my $bug_ids;
+
+  # Logged-out users use the old cookie method for storing the last search.
+  if (!$user->id or $last_list eq 'cookie') {
+    $bug_ids = $cgi->cookie('BUGLIST') or ThrowUserError("missing_cookie");
+    $bug_ids =~ s/[:-]/,/g;
+    $order ||= "reuse last sort";
+  }
+
+  # But logged in users store the last X searches in the DB so they can
+  # have multiple bug lists available.
+  else {
+    my $last_search = Bugzilla::Search::Recent->check({id => $last_list});
+    $bug_ids = join(',', @{$last_search->bug_list});
+    $order ||= $last_search->list_order;
+  }
+
+  # set up the params for this new query
+  $params = new Bugzilla::CGI({bug_id => $bug_ids, order => $order});
+  $params->param('list_id', $last_list);
 }
 
 # Figure out whether or not the user is doing a fulltext search.  If not,
@@ -147,10 +154,10 @@ my $fulltext = 0;
 if ($cgi->param('content')) { $fulltext = 1 }
 my @charts = map(/^field(\d-\d-\d)$/ ? $1 : (), $cgi->param());
 foreach my $chart (@charts) {
-    if ($cgi->param("field$chart") eq 'content' && $cgi->param("value$chart")) {
-        $fulltext = 1;
-        last;
-    }
+  if ($cgi->param("field$chart") eq 'content' && $cgi->param("value$chart")) {
+    $fulltext = 1;
+    last;
+  }
 }
 
 ################################################################################
@@ -158,34 +165,35 @@ foreach my $chart (@charts) {
 ################################################################################
 
 sub DiffDate {
-    my ($datestr) = @_;
-    my $date = str2time($datestr);
-    my $age = time() - $date;
-
-    if( $age < 18*60*60 ) {
-        $date = format_time($datestr, '%H:%M:%S');
-    } elsif( $age < 6*24*60*60 ) {
-        $date = format_time($datestr, '%a %H:%M');
-    } else {
-        $date = format_time($datestr, '%Y-%m-%d');
-    }
-    return $date;
+  my ($datestr) = @_;
+  my $date      = str2time($datestr);
+  my $age       = time() - $date;
+
+  if ($age < 18 * 60 * 60) {
+    $date = format_time($datestr, '%H:%M:%S');
+  }
+  elsif ($age < 6 * 24 * 60 * 60) {
+    $date = format_time($datestr, '%a %H:%M');
+  }
+  else {
+    $date = format_time($datestr, '%Y-%m-%d');
+  }
+  return $date;
 }
 
 sub LookupNamedQuery {
-    my ($name, $sharer_id) = @_;
+  my ($name, $sharer_id) = @_;
 
-    Bugzilla->login(LOGIN_REQUIRED);
+  Bugzilla->login(LOGIN_REQUIRED);
 
-    my $query = Bugzilla::Search::Saved->check(
-        { user => $sharer_id, name => $name, _error => 'missing_query' });
+  my $query = Bugzilla::Search::Saved->check(
+    {user => $sharer_id, name => $name, _error => 'missing_query'});
 
-    $query->url
-       || ThrowUserError("buglist_parameters_required");
+  $query->url || ThrowUserError("buglist_parameters_required");
 
-    # Detaint $sharer_id.
-    $sharer_id = $query->user->id if $sharer_id;
-    return wantarray ? ($query->url, $query->id, $sharer_id) : $query->url;
+  # Detaint $sharer_id.
+  $sharer_id = $query->user->id if $sharer_id;
+  return wantarray ? ($query->url, $query->id, $sharer_id) : $query->url;
 }
 
 # Inserts a Named Query (a "Saved Search") into the database, or
@@ -197,119 +205,121 @@ sub LookupNamedQuery {
 #              will throw a UserError. Leading and trailing whitespace
 #              will be stripped from this value before it is inserted
 #              into the DB.
-# query - The query part of the buglist.cgi URL, unencoded. Must not be 
+# query - The query part of the buglist.cgi URL, unencoded. Must not be
 #         empty, or we will throw a UserError.
-# link_in_footer (optional) - 1 if the Named Query should be 
+# link_in_footer (optional) - 1 if the Named Query should be
 # displayed in the user's footer, 0 otherwise.
 #
 # All parameters are validated before passing them into the database.
 #
-# Returns: A boolean true value if the query existed in the database 
+# Returns: A boolean true value if the query existed in the database
 # before, and we updated it. A boolean false value otherwise.
 sub InsertNamedQuery {
-    my ($query_name, $query, $link_in_footer) = @_;
-    my $dbh = Bugzilla->dbh;
-
-    $query_name = trim($query_name);
-    my ($query_obj) = grep {lc($_->name) eq lc($query_name)} @{Bugzilla->user->queries};
-
-    if ($query_obj) {
-        $query_obj->set_name($query_name);
-        $query_obj->set_url($query);
-        $query_obj->update();
-    } else {
-        Bugzilla::Search::Saved->create({
-            name           => $query_name,
-            query          => $query,
-            link_in_footer => $link_in_footer
-        });
-    }
-
-    return $query_obj ? 1 : 0;
+  my ($query_name, $query, $link_in_footer) = @_;
+  my $dbh = Bugzilla->dbh;
+
+  $query_name = trim($query_name);
+  my ($query_obj)
+    = grep { lc($_->name) eq lc($query_name) } @{Bugzilla->user->queries};
+
+  if ($query_obj) {
+    $query_obj->set_name($query_name);
+    $query_obj->set_url($query);
+    $query_obj->update();
+  }
+  else {
+    Bugzilla::Search::Saved->create({
+      name => $query_name, query => $query, link_in_footer => $link_in_footer
+    });
+  }
+
+  return $query_obj ? 1 : 0;
 }
 
 sub LookupSeries {
-    my ($series_id) = @_;
-    detaint_natural($series_id) || ThrowCodeError("invalid_series_id");
-    
-    my $dbh = Bugzilla->dbh;
-    my $result = $dbh->selectrow_array("SELECT query FROM series " .
-                                       "WHERE series_id = ?"
-                                       , undef, ($series_id));
-    $result
-           || ThrowCodeError("invalid_series_id", {'series_id' => $series_id});
-    return $result;
+  my ($series_id) = @_;
+  detaint_natural($series_id) || ThrowCodeError("invalid_series_id");
+
+  my $dbh = Bugzilla->dbh;
+  my $result
+    = $dbh->selectrow_array("SELECT query FROM series " . "WHERE series_id = ?",
+    undef, ($series_id));
+  $result || ThrowCodeError("invalid_series_id", {'series_id' => $series_id});
+  return $result;
 }
 
 sub GetQuip {
-    my $dbh = Bugzilla->dbh;
-    # COUNT is quick because it is cached for MySQL. We may want to revisit
-    # this when we support other databases.
-    my $count = $dbh->selectrow_array("SELECT COUNT(quip)"
-                                    . " FROM quips WHERE approved = 1");
-    my $random = int(rand($count));
-    my $quip = 
-        $dbh->selectrow_array("SELECT quip FROM quips WHERE approved = 1 " . 
-                              $dbh->sql_limit(1, $random));
-    return $quip;
+  my $dbh = Bugzilla->dbh;
+
+  # COUNT is quick because it is cached for MySQL. We may want to revisit
+  # this when we support other databases.
+  my $count = $dbh->selectrow_array(
+    "SELECT COUNT(quip)" . " FROM quips WHERE approved = 1");
+  my $random = int(rand($count));
+  my $quip   = $dbh->selectrow_array(
+    "SELECT quip FROM quips WHERE approved = 1 " . $dbh->sql_limit(1, $random));
+  return $quip;
 }
 
 # Return groups available for at least one product of the buglist.
 sub GetGroups {
-    my $product_names = shift;
-    my $user = Bugzilla->user;
-    my %legal_groups;
+  my $product_names = shift;
+  my $user          = Bugzilla->user;
+  my %legal_groups;
 
-    foreach my $product_name (@$product_names) {
-        my $product = Bugzilla::Product->new({name => $product_name, cache => 1});
+  foreach my $product_name (@$product_names) {
+    my $product = Bugzilla::Product->new({name => $product_name, cache => 1});
 
-        foreach my $gid (keys %{$product->group_controls}) {
-            # The user can only edit groups they belong to.
-            next unless $user->in_group_id($gid);
+    foreach my $gid (keys %{$product->group_controls}) {
 
-            # The user has no control on groups marked as NA or MANDATORY.
-            my $group = $product->group_controls->{$gid};
-            next if ($group->{membercontrol} == CONTROLMAPMANDATORY
-                     || $group->{membercontrol} == CONTROLMAPNA);
+      # The user can only edit groups they belong to.
+      next unless $user->in_group_id($gid);
 
-            # It's fine to include inactive groups. Those will be marked
-            # as "remove only" when editing several bugs at once.
-            $legal_groups{$gid} ||= $group->{group};
-        }
+      # The user has no control on groups marked as NA or MANDATORY.
+      my $group = $product->group_controls->{$gid};
+      next
+        if ($group->{membercontrol} == CONTROLMAPMANDATORY
+        || $group->{membercontrol} == CONTROLMAPNA);
+
+      # It's fine to include inactive groups. Those will be marked
+      # as "remove only" when editing several bugs at once.
+      $legal_groups{$gid} ||= $group->{group};
     }
-    # Return a list of group objects.
-    return [values %legal_groups];
+  }
+
+  # Return a list of group objects.
+  return [values %legal_groups];
 }
 
 sub _get_common_flag_types {
-    my $component_ids = shift;
-    my $user = Bugzilla->user;
-
-    # Get all the different components in the bug list
-    my $components = Bugzilla::Component->new_from_list($component_ids);
-    my %flag_types;
-    my @flag_types_ids;
-    foreach my $component (@$components) {
-        foreach my $flag_type (@{$component->flag_types->{'bug'}}) {
-            push @flag_types_ids, $flag_type->id;
-            $flag_types{$flag_type->id} = $flag_type;
-        }
-    }
-
-    # We only want flags that appear in all components
-    my %common_flag_types;
-    foreach my $id (keys %flag_types) {
-        my $flag_type_count = scalar grep { $_ == $id } @flag_types_ids;
-        $common_flag_types{$id} = $flag_types{$id}
-            if $flag_type_count == scalar @$components;
+  my $component_ids = shift;
+  my $user          = Bugzilla->user;
+
+  # Get all the different components in the bug list
+  my $components = Bugzilla::Component->new_from_list($component_ids);
+  my %flag_types;
+  my @flag_types_ids;
+  foreach my $component (@$components) {
+    foreach my $flag_type (@{$component->flag_types->{'bug'}}) {
+      push @flag_types_ids, $flag_type->id;
+      $flag_types{$flag_type->id} = $flag_type;
     }
-
-    # We only show flags that a user can request.
-    my @show_flag_types
-        = grep { $user->can_request_flag($_) } values %common_flag_types;
-    my $any_flags_requesteeble = grep { $_->is_requesteeble } @show_flag_types;
-
-    return(\@show_flag_types, $any_flags_requesteeble);
+  }
+
+  # We only want flags that appear in all components
+  my %common_flag_types;
+  foreach my $id (keys %flag_types) {
+    my $flag_type_count = scalar grep { $_ == $id } @flag_types_ids;
+    $common_flag_types{$id} = $flag_types{$id}
+      if $flag_type_count == scalar @$components;
+  }
+
+  # We only show flags that a user can request.
+  my @show_flag_types
+    = grep { $user->can_request_flag($_) } values %common_flag_types;
+  my $any_flags_requesteeble = grep { $_->is_requesteeble } @show_flag_types;
+
+  return (\@show_flag_types, $any_flags_requesteeble);
 }
 
 ################################################################################
@@ -322,13 +332,13 @@ my $sharer_id;
 
 # Backwards-compatibility - the old interface had cmdtype="runnamed" to run
 # a named command, and we can't break this because it's in bookmarks.
-if ($cmdtype eq "runnamed") {  
-    $cmdtype = "dorem";
-    $remaction = "run";
+if ($cmdtype eq "runnamed") {
+  $cmdtype   = "dorem";
+  $remaction = "run";
 }
 
 # Now we're going to be running, so ensure that the params object is set up,
-# using ||= so that we only do so if someone hasn't overridden this 
+# using ||= so that we only do so if someone hasn't overridden this
 # earlier, for example by setting up a named query search.
 
 # This will be modified, so make a copy.
@@ -336,51 +346,52 @@ $params ||= new Bugzilla::CGI($cgi);
 
 # Generate a reasonable filename for the user agent to suggest to the user
 # when the user saves the bug list.  Uses the name of the remembered query
-# if available.  We have to do this now, even though we return HTTP headers 
-# at the end, because the fact that there is a remembered query gets 
+# if available.  We have to do this now, even though we return HTTP headers
+# at the end, because the fact that there is a remembered query gets
 # forgotten in the process of retrieving it.
 my $disp_prefix = "bugs";
 if ($cmdtype eq "dorem" && $remaction =~ /^run/) {
-    $disp_prefix = $cgi->param('namedcmd');
+  $disp_prefix = $cgi->param('namedcmd');
 }
 
 # Take appropriate action based on user's request.
-if ($cmdtype eq "dorem") {  
-    if ($remaction eq "run") {
-        my $query_id;
-        ($buffer, $query_id, $sharer_id) =
-          LookupNamedQuery(scalar $cgi->param("namedcmd"),
-                           scalar $cgi->param('sharer_id'));
-        # If this is the user's own query, remember information about it
-        # so that it can be modified easily.
-        $vars->{'searchname'} = $cgi->param('namedcmd');
-        if (!$cgi->param('sharer_id') ||
-            $cgi->param('sharer_id') == $user->id) {
-            $vars->{'searchtype'} = "saved";
-            $vars->{'search_id'} = $query_id;
-        }
-        $params = new Bugzilla::CGI($buffer);
-        $order = $params->param('order') || $order;
-
-    }
-    elsif ($remaction eq "runseries") {
-        $buffer = LookupSeries(scalar $cgi->param("series_id"));
-        $vars->{'searchname'} = $cgi->param('namedcmd');
-        $vars->{'searchtype'} = "series";
-        $params = new Bugzilla::CGI($buffer);
-        $order = $params->param('order') || $order;
+if ($cmdtype eq "dorem") {
+  if ($remaction eq "run") {
+    my $query_id;
+    ($buffer, $query_id, $sharer_id)
+      = LookupNamedQuery(scalar $cgi->param("namedcmd"),
+      scalar $cgi->param('sharer_id'));
+
+    # If this is the user's own query, remember information about it
+    # so that it can be modified easily.
+    $vars->{'searchname'} = $cgi->param('namedcmd');
+    if (!$cgi->param('sharer_id') || $cgi->param('sharer_id') == $user->id) {
+      $vars->{'searchtype'} = "saved";
+      $vars->{'search_id'}  = $query_id;
     }
-    elsif ($remaction eq "forget") {
-        $user = Bugzilla->login(LOGIN_REQUIRED);
-        # Copy the name into a variable, so that we can trick_taint it for
-        # the DB. We know it's safe, because we're using placeholders in 
-        # the SQL, and the SQL is only a DELETE.
-        my $qname = $cgi->param('namedcmd');
-        trick_taint($qname);
-
-        # Do not forget the saved search if it is being used in a whine
-        my $whines_in_use = 
-            $dbh->selectcol_arrayref('SELECT DISTINCT whine_events.subject
+    $params = new Bugzilla::CGI($buffer);
+    $order = $params->param('order') || $order;
+
+  }
+  elsif ($remaction eq "runseries") {
+    $buffer               = LookupSeries(scalar $cgi->param("series_id"));
+    $vars->{'searchname'} = $cgi->param('namedcmd');
+    $vars->{'searchtype'} = "series";
+    $params               = new Bugzilla::CGI($buffer);
+    $order                = $params->param('order') || $order;
+  }
+  elsif ($remaction eq "forget") {
+    $user = Bugzilla->login(LOGIN_REQUIRED);
+
+    # Copy the name into a variable, so that we can trick_taint it for
+    # the DB. We know it's safe, because we're using placeholders in
+    # the SQL, and the SQL is only a DELETE.
+    my $qname = $cgi->param('namedcmd');
+    trick_taint($qname);
+
+    # Do not forget the saved search if it is being used in a whine
+    my $whines_in_use = $dbh->selectcol_arrayref(
+      'SELECT DISTINCT whine_events.subject
                                                  FROM whine_events
                                            INNER JOIN whine_queries
                                                    ON whine_queries.eventid
@@ -389,92 +400,100 @@ if ($cmdtype eq "dorem") {
                                                       = ?
                                                   AND whine_queries.query_name
                                                       = ?
-                                      ', undef, $user->id, $qname);
-        if (scalar(@$whines_in_use)) {
-            ThrowUserError('saved_search_used_by_whines', 
-                           { subjects    => join(',', @$whines_in_use),
-                             search_name => $qname                      }
-            );
-        }
-
-        # If we are here, then we can safely remove the saved search
-        my $query_id;
-        ($buffer, $query_id) = LookupNamedQuery(scalar $cgi->param("namedcmd"),
-                                                $user->id);
-        if ($query_id) {
-            # Make sure the user really wants to delete their saved search.
-            my $token = $cgi->param('token');
-            check_hash_token($token, [$query_id, $qname]);
-
-            $dbh->do('DELETE FROM namedqueries
-                            WHERE id = ?',
-                     undef, $query_id);
-            $dbh->do('DELETE FROM namedqueries_link_in_footer
-                            WHERE namedquery_id = ?',
-                     undef, $query_id);
-            $dbh->do('DELETE FROM namedquery_group_map
-                            WHERE namedquery_id = ?',
-                     undef, $query_id);
-            Bugzilla->memcached->clear({ table => 'namedqueries', id => $query_id });
-        }
+                                      ', undef, $user->id, $qname
+    );
+    if (scalar(@$whines_in_use)) {
+      ThrowUserError('saved_search_used_by_whines',
+        {subjects => join(',', @$whines_in_use), search_name => $qname});
+    }
 
-        # Now reset the cached queries
-        $user->flush_queries_cache();
-
-        print $cgi->header();
-        # Generate and return the UI (HTML page) from the appropriate template.
-        $vars->{'message'} = "buglist_query_gone";
-        $vars->{'namedcmd'} = $qname;
-        $vars->{'url'} = "buglist.cgi?newquery=" . url_quote($buffer)
-                         . "&cmdtype=doit&remtype=asnamed&newqueryname=" . url_quote($qname)
-                         . "&token=" . url_quote(issue_hash_token(['savedsearch']));
-        $template->process("global/message.html.tmpl", $vars)
-          || ThrowTemplateError($template->error());
-        exit;
+    # If we are here, then we can safely remove the saved search
+    my $query_id;
+    ($buffer, $query_id)
+      = LookupNamedQuery(scalar $cgi->param("namedcmd"), $user->id);
+    if ($query_id) {
+
+      # Make sure the user really wants to delete their saved search.
+      my $token = $cgi->param('token');
+      check_hash_token($token, [$query_id, $qname]);
+
+      $dbh->do(
+        'DELETE FROM namedqueries
+                            WHERE id = ?', undef, $query_id
+      );
+      $dbh->do(
+        'DELETE FROM namedqueries_link_in_footer
+                            WHERE namedquery_id = ?', undef, $query_id
+      );
+      $dbh->do(
+        'DELETE FROM namedquery_group_map
+                            WHERE namedquery_id = ?', undef, $query_id
+      );
+      Bugzilla->memcached->clear({table => 'namedqueries', id => $query_id});
     }
+
+    # Now reset the cached queries
+    $user->flush_queries_cache();
+
+    print $cgi->header();
+
+    # Generate and return the UI (HTML page) from the appropriate template.
+    $vars->{'message'}  = "buglist_query_gone";
+    $vars->{'namedcmd'} = $qname;
+    $vars->{'url'}
+      = "buglist.cgi?newquery="
+      . url_quote($buffer)
+      . "&cmdtype=doit&remtype=asnamed&newqueryname="
+      . url_quote($qname)
+      . "&token="
+      . url_quote(issue_hash_token(['savedsearch']));
+    $template->process("global/message.html.tmpl", $vars)
+      || ThrowTemplateError($template->error());
+    exit;
+  }
 }
 elsif (($cmdtype eq "doit") && defined $cgi->param('remtype')) {
-    if ($cgi->param('remtype') eq "asdefault") {
-        $user = Bugzilla->login(LOGIN_REQUIRED);
-        my $token = $cgi->param('token');
-        check_hash_token($token, ['searchknob']);
-        $buffer = $params->canonicalise_query('cmdtype', 'remtype',
-                                              'query_based_on', 'token');
-        InsertNamedQuery(DEFAULT_QUERY_NAME, $buffer);
-        $vars->{'message'} = "buglist_new_default_query";
+  if ($cgi->param('remtype') eq "asdefault") {
+    $user = Bugzilla->login(LOGIN_REQUIRED);
+    my $token = $cgi->param('token');
+    check_hash_token($token, ['searchknob']);
+    $buffer = $params->canonicalise_query('cmdtype', 'remtype', 'query_based_on',
+      'token');
+    InsertNamedQuery(DEFAULT_QUERY_NAME, $buffer);
+    $vars->{'message'} = "buglist_new_default_query";
+  }
+  elsif ($cgi->param('remtype') eq "asnamed") {
+    $user = Bugzilla->login(LOGIN_REQUIRED);
+    my $query_name = $cgi->param('newqueryname');
+    my $new_query  = $cgi->param('newquery');
+    my $token      = $cgi->param('token');
+    check_hash_token($token, ['savedsearch']);
+    my $existed_before = InsertNamedQuery($query_name, $new_query, 1);
+    if ($existed_before) {
+      $vars->{'message'} = "buglist_updated_named_query";
     }
-    elsif ($cgi->param('remtype') eq "asnamed") {
-        $user = Bugzilla->login(LOGIN_REQUIRED);
-        my $query_name = $cgi->param('newqueryname');
-        my $new_query = $cgi->param('newquery');
-        my $token = $cgi->param('token');
-        check_hash_token($token, ['savedsearch']);
-        my $existed_before = InsertNamedQuery($query_name, $new_query, 1);
-        if ($existed_before) {
-            $vars->{'message'} = "buglist_updated_named_query";
-        }
-        else {
-            $vars->{'message'} = "buglist_new_named_query";
-        }
-        $vars->{'queryname'} = $query_name;
+    else {
+      $vars->{'message'} = "buglist_new_named_query";
+    }
+    $vars->{'queryname'} = $query_name;
 
-        # Make sure to invalidate any cached query data, so that the footer is
-        # correctly displayed
-        $user->flush_queries_cache();
+    # Make sure to invalidate any cached query data, so that the footer is
+    # correctly displayed
+    $user->flush_queries_cache();
 
-        print $cgi->header();
-        $template->process("global/message.html.tmpl", $vars)
-          || ThrowTemplateError($template->error());
-        exit;
-    }
+    print $cgi->header();
+    $template->process("global/message.html.tmpl", $vars)
+      || ThrowTemplateError($template->error());
+    exit;
+  }
 }
 
 # backward compatibility hack: if the saved query doesn't say which
 # form was used to create it, assume it was on the advanced query
 # form - see bug 252295
 if (!$params->param('query_format')) {
-    $params->param('query_format', 'advanced');
-    $buffer = $params->query_string;
+  $params->param('query_format', 'advanced');
+  $buffer = $params->query_string;
 }
 
 ################################################################################
@@ -487,40 +506,42 @@ my $columns = Bugzilla::Search::COLUMNS;
 # Display Column Determination
 ################################################################################
 
-# Determine the columns that will be displayed in the bug list via the 
+# Determine the columns that will be displayed in the bug list via the
 # columnlist CGI parameter, the user's preferences, or the default.
 my @displaycolumns = ();
 if (defined $params->param('columnlist')) {
-    if ($params->param('columnlist') eq "all") {
-        # If the value of the CGI parameter is "all", display all columns,
-        # but remove the redundant "short_desc" column.
-        @displaycolumns = grep($_ ne 'short_desc', keys(%$columns));
-    }
-    else {
-        @displaycolumns = split(/[ ,]+/, $params->param('columnlist'));
-    }
+  if ($params->param('columnlist') eq "all") {
+
+    # If the value of the CGI parameter is "all", display all columns,
+    # but remove the redundant "short_desc" column.
+    @displaycolumns = grep($_ ne 'short_desc', keys(%$columns));
+  }
+  else {
+    @displaycolumns = split(/[ ,]+/, $params->param('columnlist'));
+  }
 }
 elsif (defined $cgi->cookie('COLUMNLIST')) {
-    # 2002-10-31 Rename column names (see bug 176461)
-    my $columnlist = $cgi->cookie('COLUMNLIST');
-    $columnlist =~ s/\bowner\b/assigned_to/;
-    $columnlist =~ s/\bowner_realname\b/assigned_to_realname/;
-    $columnlist =~ s/\bplatform\b/rep_platform/;
-    $columnlist =~ s/\bseverity\b/bug_severity/;
-    $columnlist =~ s/\bstatus\b/bug_status/;
-    $columnlist =~ s/\bsummaryfull\b/short_desc/;
-    $columnlist =~ s/\bsummary\b/short_short_desc/;
-
-    # Use the columns listed in the user's preferences.
-    @displaycolumns = split(/ /, $columnlist);
+
+  # 2002-10-31 Rename column names (see bug 176461)
+  my $columnlist = $cgi->cookie('COLUMNLIST');
+  $columnlist =~ s/\bowner\b/assigned_to/;
+  $columnlist =~ s/\bowner_realname\b/assigned_to_realname/;
+  $columnlist =~ s/\bplatform\b/rep_platform/;
+  $columnlist =~ s/\bseverity\b/bug_severity/;
+  $columnlist =~ s/\bstatus\b/bug_status/;
+  $columnlist =~ s/\bsummaryfull\b/short_desc/;
+  $columnlist =~ s/\bsummary\b/short_short_desc/;
+
+  # Use the columns listed in the user's preferences.
+  @displaycolumns = split(/ /, $columnlist);
 }
 else {
-    # Use the default list of columns.
-    @displaycolumns = DEFAULT_COLUMN_LIST;
+  # Use the default list of columns.
+  @displaycolumns = DEFAULT_COLUMN_LIST;
 }
 
-# Weed out columns that don't actually exist to prevent the user 
-# from hacking their column list cookie to grab data to which they 
+# Weed out columns that don't actually exist to prevent the user
+# from hacking their column list cookie to grab data to which they
 # should not have access.  Detaint the data along the way.
 @displaycolumns = grep($columns->{$_} && trick_taint($_), @displaycolumns);
 
@@ -531,14 +552,14 @@ else {
 # Remove the timetracking columns if they are not a part of the group
 # (happens if a user had access to time tracking and it was revoked/disabled)
 if (!$user->is_timetracker) {
-   foreach my $tt_field (TIMETRACKING_FIELDS) {
-       @displaycolumns = grep($_ ne $tt_field, @displaycolumns);
-   }
+  foreach my $tt_field (TIMETRACKING_FIELDS) {
+    @displaycolumns = grep($_ ne $tt_field, @displaycolumns);
+  }
 }
 
 # Remove the relevance column if the user is not doing a fulltext search.
 if (grep('relevance', @displaycolumns) && !$fulltext) {
-    @displaycolumns = grep($_ ne 'relevance', @displaycolumns);
+  @displaycolumns = grep($_ ne 'relevance', @displaycolumns);
 }
 
 ################################################################################
@@ -550,13 +571,14 @@ if (grep('relevance', @displaycolumns) && !$fulltext) {
 # The bug ID is always selected because bug IDs are always displayed.
 # Severity, priority, resolution and status are required for buglist
 # CSS classes.
-my @selectcolumns = ("bug_id", "bug_severity", "priority", "bug_status",
-                     "resolution", "product");
+my @selectcolumns
+  = ("bug_id", "bug_severity", "priority", "bug_status", "resolution",
+  "product");
 
 # remaining and actual_time are required for percentage_complete calculation:
 if (grep { $_ eq "percentage_complete" } @displaycolumns) {
-    push (@selectcolumns, "remaining_time");
-    push (@selectcolumns, "actual_time");
+  push(@selectcolumns, "remaining_time");
+  push(@selectcolumns, "actual_time");
 }
 
 # Make sure that the login_name version of a field is always also
@@ -564,58 +586,51 @@ if (grep { $_ eq "percentage_complete" } @displaycolumns) {
 # display the login name when the realname is empty.
 my @realname_fields = grep(/_realname$/, @displaycolumns);
 foreach my $item (@realname_fields) {
-    my $login_field = $item;
-    $login_field =~ s/_realname$//;
-    if (!grep($_ eq $login_field, @selectcolumns)) {
-        push(@selectcolumns, $login_field);
-    }
+  my $login_field = $item;
+  $login_field =~ s/_realname$//;
+  if (!grep($_ eq $login_field, @selectcolumns)) {
+    push(@selectcolumns, $login_field);
+  }
 }
 
 # Display columns are selected because otherwise we could not display them.
 foreach my $col (@displaycolumns) {
-    push (@selectcolumns, $col) if !grep($_ eq $col, @selectcolumns);
+  push(@selectcolumns, $col) if !grep($_ eq $col, @selectcolumns);
 }
 
-# If the user is editing multiple bugs, we also make sure to select the 
+# If the user is editing multiple bugs, we also make sure to select the
 # status, because the values of that field determines what options the user
 # has for modifying the bugs.
 if ($dotweak) {
-    push(@selectcolumns, "bug_status") if !grep($_ eq 'bug_status', @selectcolumns);
-    push(@selectcolumns, "bugs.component_id");
+  push(@selectcolumns, "bug_status") if !grep($_ eq 'bug_status', @selectcolumns);
+  push(@selectcolumns, "bugs.component_id");
 }
 
 if ($format->{'extension'} eq 'ics') {
-    push(@selectcolumns, "opendate") if !grep($_ eq 'opendate', @selectcolumns);
-    if (Bugzilla->params->{'timetrackinggroup'}) {
-        push(@selectcolumns, "deadline") if !grep($_ eq 'deadline', @selectcolumns);
-    }
+  push(@selectcolumns, "opendate") if !grep($_ eq 'opendate', @selectcolumns);
+  if (Bugzilla->params->{'timetrackinggroup'}) {
+    push(@selectcolumns, "deadline") if !grep($_ eq 'deadline', @selectcolumns);
+  }
 }
 
 if ($format->{'extension'} eq 'atom') {
-    # The title of the Atom feed will be the same one as for the bug list.
-    $vars->{'title'} = $cgi->param('title');
-
-    # This is the list of fields that are needed by the Atom filter.
-    my @required_atom_columns = (
-      'short_desc',
-      'opendate',
-      'changeddate',
-      'reporter',
-      'reporter_realname',
-      'priority',
-      'bug_severity',
-      'assigned_to',
-      'assigned_to_realname',
-      'bug_status',
-      'product',
-      'component',
-      'resolution'
-    );
-    push(@required_atom_columns, 'target_milestone') if Bugzilla->params->{'usetargetmilestone'};
 
-    foreach my $required (@required_atom_columns) {
-        push(@selectcolumns, $required) if !grep($_ eq $required,@selectcolumns);
-    }
+  # The title of the Atom feed will be the same one as for the bug list.
+  $vars->{'title'} = $cgi->param('title');
+
+  # This is the list of fields that are needed by the Atom filter.
+  my @required_atom_columns = (
+    'short_desc',           'opendate',   'changeddate',  'reporter',
+    'reporter_realname',    'priority',   'bug_severity', 'assigned_to',
+    'assigned_to_realname', 'bug_status', 'product',      'component',
+    'resolution'
+  );
+  push(@required_atom_columns, 'target_milestone')
+    if Bugzilla->params->{'usetargetmilestone'};
+
+  foreach my $required (@required_atom_columns) {
+    push(@selectcolumns, $required) if !grep($_ eq $required, @selectcolumns);
+  }
 }
 
 ################################################################################
@@ -627,76 +642,79 @@ if ($format->{'extension'} eq 'atom') {
 # First check if we'll want to reuse the last sorting order; that happens if
 # the order is not defined or its value is "reuse last sort"
 if (!$order || $order =~ /^reuse/i) {
-    if ($cgi->cookie('LASTORDER')) {
-        $order = $cgi->cookie('LASTORDER');
-       
-        # Cookies from early versions of Specific Search included this text,
-        # which is now invalid.
-        $order =~ s/ LIMIT 200//;
-    }
-    else {
-        $order = '';  # Remove possible "reuse" identifier as unnecessary
-    }
+  if ($cgi->cookie('LASTORDER')) {
+    $order = $cgi->cookie('LASTORDER');
+
+    # Cookies from early versions of Specific Search included this text,
+    # which is now invalid.
+    $order =~ s/ LIMIT 200//;
+  }
+  else {
+    $order = '';    # Remove possible "reuse" identifier as unnecessary
+  }
 }
 
 my @order_columns;
 if ($order) {
-    # Convert the value of the "order" form field into a list of columns
-    # by which to sort the results.
-    ORDER: for ($order) {
-        /^Bug Number$/ && do {
-            @order_columns = ("bug_id");
-            last ORDER;
-        };
-        /^Importance$/ && do {
-            @order_columns = ("priority", "bug_severity");
-            last ORDER;
-        };
-        /^Assignee$/ && do {
-            @order_columns = ("assigned_to", "bug_status", "priority",
-                              "bug_id");
-            last ORDER;
-        };
-        /^Last Changed$/ && do {
-            @order_columns = ("changeddate", "bug_status", "priority",
-                              "assigned_to", "bug_id");
-            last ORDER;
-        };
-        do {
-            # A custom list of columns. Bugzilla::Search will validate items.
-            @order_columns = split(/\s*,\s*/, $order);
-        };
-    }
+
+  # Convert the value of the "order" form field into a list of columns
+  # by which to sort the results.
+ORDER: for ($order) {
+    /^Bug Number$/ && do {
+      @order_columns = ("bug_id");
+      last ORDER;
+    };
+    /^Importance$/ && do {
+      @order_columns = ("priority", "bug_severity");
+      last ORDER;
+    };
+    /^Assignee$/ && do {
+      @order_columns = ("assigned_to", "bug_status", "priority", "bug_id");
+      last ORDER;
+    };
+    /^Last Changed$/ && do {
+      @order_columns
+        = ("changeddate", "bug_status", "priority", "assigned_to", "bug_id");
+      last ORDER;
+    };
+    do {
+      # A custom list of columns. Bugzilla::Search will validate items.
+      @order_columns = split(/\s*,\s*/, $order);
+    };
+  }
 }
 
 if (!scalar @order_columns) {
-    # DEFAULT
-    @order_columns = ("bug_status", "priority", "assigned_to", "bug_id");
+
+  # DEFAULT
+  @order_columns = ("bug_status", "priority", "assigned_to", "bug_id");
 }
 
 # In the HTML interface, by default, we limit the returned results,
 # which speeds up quite a few searches where people are really only looking
 # for the top results.
 if ($format->{'extension'} eq 'html' && !defined $params->param('limit')) {
-    $params->param('limit', Bugzilla->params->{'default_search_limit'});
-    $vars->{'default_limited'} = 1;
+  $params->param('limit', Bugzilla->params->{'default_search_limit'});
+  $vars->{'default_limited'} = 1;
 }
 
 # Generate the basic SQL query that will be used to generate the bug list.
-my $search = new Bugzilla::Search('fields' => \@selectcolumns, 
-                                  'params' => scalar $params->Vars,
-                                  'order'  => \@order_columns,
-                                  'sharer' => $sharer_id);
+my $search = new Bugzilla::Search(
+  'fields' => \@selectcolumns,
+  'params' => scalar $params->Vars,
+  'order'  => \@order_columns,
+  'sharer' => $sharer_id
+);
 
 $order = join(',', $search->order);
 
 if (scalar @{$search->invalid_order_columns}) {
-    $vars->{'message'} = 'invalid_column_name';
-    $vars->{'invalid_fragments'} = $search->invalid_order_columns;
+  $vars->{'message'}           = 'invalid_column_name';
+  $vars->{'invalid_fragments'} = $search->invalid_order_columns;
 }
 
-if ($fulltext and grep { /^relevance/ } $search->order) {
-    $vars->{'message'} = 'buglist_sorted_by_relevance'
+if ($fulltext and grep {/^relevance/} $search->order) {
+  $vars->{'message'} = 'buglist_sorted_by_relevance';
 }
 
 # We don't want saved searches and other buglist things to save
@@ -710,22 +728,22 @@ $params->delete('limit') if $vars->{'default_limited'};
 # Time to use server push to display an interim message to the user until
 # the query completes and we can display the bug list.
 if ($serverpush) {
-    print $cgi->multipart_init();
-    print $cgi->multipart_start(-type => 'text/html');
-
-    # Generate and return the UI (HTML page) from the appropriate template.
-    $template->process("list/server-push.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
-
-    # Under mod_perl, flush stdout so that the page actually shows up.
-    if ($ENV{MOD_PERL}) {
-        require Apache2::RequestUtil;
-        Apache2::RequestUtil->request->rflush();
-    }
-
-    # Don't do multipart_end() until we're ready to display the replacement
-    # page, otherwise any errors that happen before then (like SQL errors)
-    # will result in a blank page being shown to the user instead of the error.
+  print $cgi->multipart_init();
+  print $cgi->multipart_start(-type => 'text/html');
+
+  # Generate and return the UI (HTML page) from the appropriate template.
+  $template->process("list/server-push.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+
+  # Under mod_perl, flush stdout so that the page actually shows up.
+  if ($ENV{MOD_PERL}) {
+    require Apache2::RequestUtil;
+    Apache2::RequestUtil->request->rflush();
+  }
+
+  # Don't do multipart_end() until we're ready to display the replacement
+  # page, otherwise any errors that happen before then (like SQL errors)
+  # will result in a blank page being shown to the user instead of the error.
 }
 
 # Connect to the shadow database if this installation is using one to improve
@@ -742,24 +760,25 @@ $::SIG{PIPE} = 'DEFAULT';
 my ($data, $extra_data) = $search->data;
 $vars->{'search_description'} = $search->search_description;
 
-if ($cgi->param('debug')
-    && Bugzilla->params->{debug_group}
-    && $user->in_group(Bugzilla->params->{debug_group})
-) {
-    $vars->{'debug'} = 1;
-    $vars->{'queries'} = $extra_data;
-    my $query_time = 0;
-    $query_time += $_->{'time'} foreach @$extra_data;
-    $vars->{'query_time'} = $query_time;
-    # Explains are limited to admins because you could use them to figure
-    # out how many hidden bugs are in a particular product (by doing
-    # searches and looking at the number of rows the explain says it's
-    # examining).
-    if ($user->in_group('admin')) {
-        foreach my $query (@$extra_data) {
-            $query->{explain} = $dbh->bz_explain($query->{sql});
-        }
+if ( $cgi->param('debug')
+  && Bugzilla->params->{debug_group}
+  && $user->in_group(Bugzilla->params->{debug_group}))
+{
+  $vars->{'debug'}   = 1;
+  $vars->{'queries'} = $extra_data;
+  my $query_time = 0;
+  $query_time += $_->{'time'} foreach @$extra_data;
+  $vars->{'query_time'} = $query_time;
+
+  # Explains are limited to admins because you could use them to figure
+  # out how many hidden bugs are in a particular product (by doing
+  # searches and looking at the number of rows the explain says it's
+  # examining).
+  if ($user->in_group('admin')) {
+    foreach my $query (@$extra_data) {
+      $query->{explain} = $dbh->bz_explain($query->{sql});
     }
+  }
 }
 
 ################################################################################
@@ -771,72 +790,74 @@ if ($cgi->param('debug')
 
 # If we're doing time tracking, then keep totals for all bugs.
 my $percentage_complete = grep($_ eq 'percentage_complete', @displaycolumns);
-my $estimated_time      = grep($_ eq 'estimated_time', @displaycolumns);
-my $remaining_time      = grep($_ eq 'remaining_time', @displaycolumns)
-                            || $percentage_complete;
-my $actual_time         = grep($_ eq 'actual_time', @displaycolumns)
-                            || $percentage_complete;
-
-my $time_info = { 'estimated_time' => 0,
-                  'remaining_time' => 0,
-                  'actual_time' => 0,
-                  'percentage_complete' => 0,
-                  'time_present' => ($estimated_time || $remaining_time ||
-                                     $actual_time || $percentage_complete),
-                };
-
-my $bugowners = {};
-my $bugproducts = {};
+my $estimated_time      = grep($_ eq 'estimated_time',      @displaycolumns);
+my $remaining_time
+  = grep($_ eq 'remaining_time', @displaycolumns) || $percentage_complete;
+my $actual_time
+  = grep($_ eq 'actual_time', @displaycolumns) || $percentage_complete;
+
+my $time_info = {
+  'estimated_time'      => 0,
+  'remaining_time'      => 0,
+  'actual_time'         => 0,
+  'percentage_complete' => 0,
+  'time_present' =>
+    ($estimated_time || $remaining_time || $actual_time || $percentage_complete),
+};
+
+my $bugowners       = {};
+my $bugproducts     = {};
 my $bugcomponentids = {};
-my $bugcomponents = {};
-my $bugstatuses = {};
+my $bugcomponents   = {};
+my $bugstatuses     = {};
 my @bugidlist;
 
-my @bugs; # the list of records
+my @bugs;    # the list of records
 
 foreach my $row (@$data) {
-    my $bug = {}; # a record
-
-    # Slurp the row of data into the record.
-    # The second from last column in the record is the number of groups
-    # to which the bug is restricted.
-    foreach my $column (@selectcolumns) {
-        $bug->{$column} = shift @$row;
-    }
-
-    # Process certain values further (i.e. date format conversion).
-    if ($bug->{'changeddate'}) {
-        $bug->{'changeddate'} =~ 
-            s/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$/$1-$2-$3 $4:$5:$6/;
-
-        $bug->{'changedtime'} = $bug->{'changeddate'}; # for iCalendar and Atom
-        $bug->{'changeddate'} = DiffDate($bug->{'changeddate'});
-    }
-
-    if ($bug->{'opendate'}) {
-        $bug->{'opentime'} = $bug->{'opendate'}; # for iCalendar
-        $bug->{'opendate'} = DiffDate($bug->{'opendate'});
-    }
-
-    # Record the assignee, product, and status in the big hashes of those things.
-    $bugowners->{$bug->{'assigned_to'}} = 1 if $bug->{'assigned_to'};
-    $bugproducts->{$bug->{'product'}} = 1 if $bug->{'product'};
-    $bugcomponentids->{$bug->{'bugs.component_id'}} = 1 if $bug->{'bugs.component_id'};
-    $bugcomponents->{$bug->{'component'}} = 1 if $bug->{'component'};
-    $bugstatuses->{$bug->{'bug_status'}} = 1 if $bug->{'bug_status'};
-
-    $bug->{'secure_mode'} = undef;
-
-    # Add the record to the list.
-    push(@bugs, $bug);
-
-    # Add id to list for checking for bug privacy later
-    push(@bugidlist, $bug->{'bug_id'});
-
-    # Compute time tracking info.
-    $time_info->{'estimated_time'} += $bug->{'estimated_time'} if ($estimated_time);
-    $time_info->{'remaining_time'} += $bug->{'remaining_time'} if ($remaining_time);
-    $time_info->{'actual_time'}    += $bug->{'actual_time'}    if ($actual_time);
+  my $bug = {};    # a record
+
+  # Slurp the row of data into the record.
+  # The second from last column in the record is the number of groups
+  # to which the bug is restricted.
+  foreach my $column (@selectcolumns) {
+    $bug->{$column} = shift @$row;
+  }
+
+  # Process certain values further (i.e. date format conversion).
+  if ($bug->{'changeddate'}) {
+    $bug->{'changeddate'}
+      =~ s/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$/$1-$2-$3 $4:$5:$6/;
+
+    $bug->{'changedtime'} = $bug->{'changeddate'};          # for iCalendar and Atom
+    $bug->{'changeddate'} = DiffDate($bug->{'changeddate'});
+  }
+
+  if ($bug->{'opendate'}) {
+    $bug->{'opentime'} = $bug->{'opendate'};                # for iCalendar
+    $bug->{'opendate'} = DiffDate($bug->{'opendate'});
+  }
+
+  # Record the assignee, product, and status in the big hashes of those things.
+  $bugowners->{$bug->{'assigned_to'}} = 1 if $bug->{'assigned_to'};
+  $bugproducts->{$bug->{'product'}}   = 1 if $bug->{'product'};
+  $bugcomponentids->{$bug->{'bugs.component_id'}} = 1
+    if $bug->{'bugs.component_id'};
+  $bugcomponents->{$bug->{'component'}} = 1 if $bug->{'component'};
+  $bugstatuses->{$bug->{'bug_status'}}  = 1 if $bug->{'bug_status'};
+
+  $bug->{'secure_mode'} = undef;
+
+  # Add the record to the list.
+  push(@bugs, $bug);
+
+  # Add id to list for checking for bug privacy later
+  push(@bugidlist, $bug->{'bug_id'});
+
+  # Compute time tracking info.
+  $time_info->{'estimated_time'} += $bug->{'estimated_time'} if ($estimated_time);
+  $time_info->{'remaining_time'} += $bug->{'remaining_time'} if ($remaining_time);
+  $time_info->{'actual_time'}    += $bug->{'actual_time'}    if ($actual_time);
 }
 
 # Check for bug privacy and set $bug->{'secure_mode'} to 'implied' or 'manual'
@@ -844,39 +865,41 @@ foreach my $row (@$data) {
 # or because of human choice
 my %min_membercontrol;
 if (@bugidlist) {
-    my $sth = $dbh->prepare(
-        "SELECT DISTINCT bugs.bug_id, MIN(group_control_map.membercontrol) " .
-          "FROM bugs " .
-    "INNER JOIN bug_group_map " .
-            "ON bugs.bug_id = bug_group_map.bug_id " .
-     "LEFT JOIN group_control_map " .
-            "ON group_control_map.product_id = bugs.product_id " .
-           "AND group_control_map.group_id = bug_group_map.group_id " .
-         "WHERE " . $dbh->sql_in('bugs.bug_id', \@bugidlist) . 
-            $dbh->sql_group_by('bugs.bug_id'));
-    $sth->execute();
-    while (my ($bug_id, $min_membercontrol) = $sth->fetchrow_array()) {
-        $min_membercontrol{$bug_id} = $min_membercontrol || CONTROLMAPNA;
+  my $sth
+    = $dbh->prepare(
+        "SELECT DISTINCT bugs.bug_id, MIN(group_control_map.membercontrol) "
+      . "FROM bugs "
+      . "INNER JOIN bug_group_map "
+      . "ON bugs.bug_id = bug_group_map.bug_id "
+      . "LEFT JOIN group_control_map "
+      . "ON group_control_map.product_id = bugs.product_id "
+      . "AND group_control_map.group_id = bug_group_map.group_id "
+      . "WHERE "
+      . $dbh->sql_in('bugs.bug_id', \@bugidlist)
+      . $dbh->sql_group_by('bugs.bug_id'));
+  $sth->execute();
+  while (my ($bug_id, $min_membercontrol) = $sth->fetchrow_array()) {
+    $min_membercontrol{$bug_id} = $min_membercontrol || CONTROLMAPNA;
+  }
+  foreach my $bug (@bugs) {
+    next unless defined($min_membercontrol{$bug->{'bug_id'}});
+    if ($min_membercontrol{$bug->{'bug_id'}} == CONTROLMAPMANDATORY) {
+      $bug->{'secure_mode'} = 'implied';
     }
-    foreach my $bug (@bugs) {
-        next unless defined($min_membercontrol{$bug->{'bug_id'}});
-        if ($min_membercontrol{$bug->{'bug_id'}} == CONTROLMAPMANDATORY) {
-            $bug->{'secure_mode'} = 'implied';
-        }
-        else {
-            $bug->{'secure_mode'} = 'manual';
-        }
+    else {
+      $bug->{'secure_mode'} = 'manual';
     }
+  }
 }
 
 # Compute percentage complete without rounding.
-my $sum = $time_info->{'actual_time'}+$time_info->{'remaining_time'};
+my $sum = $time_info->{'actual_time'} + $time_info->{'remaining_time'};
 if ($sum > 0) {
-    $time_info->{'percentage_complete'} = 100*$time_info->{'actual_time'}/$sum;
+  $time_info->{'percentage_complete'} = 100 * $time_info->{'actual_time'} / $sum;
+}
+else {    # remaining_time <= 0
+  $time_info->{'percentage_complete'} = 0;
 }
-else { # remaining_time <= 0 
-    $time_info->{'percentage_complete'} = 0
-}                             
 
 ################################################################################
 # Template Variable Definition
@@ -884,45 +907,45 @@ else { # remaining_time <= 0
 
 # Define the variables and functions that will be passed to the UI template.
 
-$vars->{'bugs'} = \@bugs;
-$vars->{'buglist'} = \@bugidlist;
-$vars->{'columns'} = $columns;
+$vars->{'bugs'}           = \@bugs;
+$vars->{'buglist'}        = \@bugidlist;
+$vars->{'columns'}        = $columns;
 $vars->{'displaycolumns'} = \@displaycolumns;
 
 $vars->{'openstates'} = [BUG_STATE_OPEN];
-$vars->{'closedstates'} = [map {$_->name} closed_bug_statuses()];
+$vars->{'closedstates'} = [map { $_->name } closed_bug_statuses()];
 
 # The iCal file needs priorities ordered from 1 to 9 (highest to lowest)
 # If there are more than 9 values, just make all the lower ones 9
 if ($format->{'extension'} eq 'ics') {
-    my $n = 1;
-    $vars->{'ics_priorities'} = {};
-    my $priorities = get_legal_field_values('priority');
-    foreach my $p (@$priorities) {
-        $vars->{'ics_priorities'}->{$p} = ($n > 9) ? 9 : $n++;
-    }
+  my $n = 1;
+  $vars->{'ics_priorities'} = {};
+  my $priorities = get_legal_field_values('priority');
+  foreach my $p (@$priorities) {
+    $vars->{'ics_priorities'}->{$p} = ($n > 9) ? 9 : $n++;
+  }
 }
 
-$vars->{'order'} = $order;
+$vars->{'order'}       = $order;
 $vars->{'caneditbugs'} = 1;
-$vars->{'time_info'} = $time_info;
+$vars->{'time_info'}   = $time_info;
 
 if (!$user->in_group('editbugs')) {
-    foreach my $product (keys %$bugproducts) {
-        my $prod = Bugzilla::Product->new({name => $product, cache => 1});
-        if (!$user->in_group('editbugs', $prod->id)) {
-            $vars->{'caneditbugs'} = 0;
-            last;
-        }
+  foreach my $product (keys %$bugproducts) {
+    my $prod = Bugzilla::Product->new({name => $product, cache => 1});
+    if (!$user->in_group('editbugs', $prod->id)) {
+      $vars->{'caneditbugs'} = 0;
+      last;
     }
+  }
 }
 
 my @bugowners = keys %$bugowners;
 if (scalar(@bugowners) > 1 && $user->in_group('editbugs')) {
-    my $suffix = Bugzilla->params->{'emailsuffix'};
-    map(s/$/$suffix/, @bugowners) if $suffix;
-    my $bugowners = join(",", @bugowners);
-    $vars->{'bugowners'} = $bugowners;
+  my $suffix = Bugzilla->params->{'emailsuffix'};
+  map(s/$/$suffix/, @bugowners) if $suffix;
+  my $bugowners = join(",", @bugowners);
+  $vars->{'bugowners'} = $bugowners;
 }
 
 # Whether or not to split the column titles across two rows to make
@@ -930,7 +953,7 @@ if (scalar(@bugowners) > 1 && $user->in_group('editbugs')) {
 $vars->{'splitheader'} = $cgi->cookie('SPLITHEADER') ? 1 : 0;
 
 if ($user->settings->{'display_quips'}->{'value'} eq 'on') {
-    $vars->{'quip'} = GetQuip();
+  $vars->{'quip'} = GetQuip();
 }
 
 $vars->{'currenttime'} = localtime(time());
@@ -940,18 +963,20 @@ $vars->{'currenttime'} = localtime(time());
 my @products = keys %$bugproducts;
 my $one_product;
 if (scalar(@products) == 1) {
-    $one_product = Bugzilla::Product->new({ name => $products[0], cache => 1 });
+  $one_product = Bugzilla::Product->new({name => $products[0], cache => 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 = Bugzilla::Product->new({ name => $product_input[0], cache => 1 });
-    }
+  if (scalar(@product_input) == 1 and $product_input[0] ne '') {
+    $one_product = Bugzilla::Product->new({name => $product_input[0], cache => 1});
+  }
 }
-# We only want the template to use it if the user can actually 
+
+# We only want the template to use it if the user can actually
 # enter bugs against it.
 if ($one_product && $user->can_enter_product($one_product)) {
-    $vars->{'one_product'} = $one_product;
+  $vars->{'one_product'} = $one_product;
 }
 
 # See if there's only one component in all the results (or only one component
@@ -959,50 +984,50 @@ if ($one_product && $user->can_enter_product($one_product)) {
 my @components = keys %$bugcomponents;
 my $one_component;
 if (scalar(@components) == 1) {
-    $vars->{one_component} = $components[0];
+  $vars->{one_component} = $components[0];
 }
+
 # This is used in the "Zarroo Boogs" case.
 elsif (my @component_input = $cgi->param('component')) {
-    if (scalar(@component_input) == 1 and $component_input[0] ne '') {
-        $vars->{one_component}= $cgi->param('component');
-    }
+  if (scalar(@component_input) == 1 and $component_input[0] ne '') {
+    $vars->{one_component} = $cgi->param('component');
+  }
 }
 
 # The following variables are used when the user is making changes to multiple bugs.
 if ($dotweak && scalar @bugs) {
-    if (!$vars->{'caneditbugs'}) {
-        ThrowUserError('auth_failure', {group  => 'editbugs',
-                                        action => 'modify',
-                                        object => 'multiple_bugs'});
-    }
-    $vars->{'dotweak'} = 1;
-  
-    # issue_session_token needs to write to the master DB.
-    Bugzilla->switch_to_main_db();
-    $vars->{'token'} = issue_session_token('buglist_mass_change');
-    Bugzilla->switch_to_shadow_db();
-
-    $vars->{'products'} = $user->get_enterable_products;
-    $vars->{'platforms'} = get_legal_field_values('rep_platform');
-    $vars->{'op_sys'} = get_legal_field_values('op_sys');
-    $vars->{'priorities'} = get_legal_field_values('priority');
-    $vars->{'severities'} = get_legal_field_values('bug_severity');
-    $vars->{'resolutions'} = get_legal_field_values('resolution');
-
-    ($vars->{'flag_types'}, $vars->{any_flags_requesteeble})
-        = _get_common_flag_types([keys %$bugcomponentids]);
-
-    # Convert bug statuses to their ID.
-    my @bug_statuses = map {$dbh->quote($_)} keys %$bugstatuses;
-    my $bug_status_ids =
-      $dbh->selectcol_arrayref('SELECT id FROM bug_status
-                               WHERE ' . $dbh->sql_in('value', \@bug_statuses));
-
-    # This query collects new statuses which are common to all current bug statuses.
-    # It also accepts transitions where the bug status doesn't change.
-    $bug_status_ids =
-      $dbh->selectcol_arrayref(
-            'SELECT DISTINCT sw1.new_status
+  if (!$vars->{'caneditbugs'}) {
+    ThrowUserError('auth_failure',
+      {group => 'editbugs', action => 'modify', object => 'multiple_bugs'});
+  }
+  $vars->{'dotweak'} = 1;
+
+  # issue_session_token needs to write to the master DB.
+  Bugzilla->switch_to_main_db();
+  $vars->{'token'} = issue_session_token('buglist_mass_change');
+  Bugzilla->switch_to_shadow_db();
+
+  $vars->{'products'}    = $user->get_enterable_products;
+  $vars->{'platforms'}   = get_legal_field_values('rep_platform');
+  $vars->{'op_sys'}      = get_legal_field_values('op_sys');
+  $vars->{'priorities'}  = get_legal_field_values('priority');
+  $vars->{'severities'}  = get_legal_field_values('bug_severity');
+  $vars->{'resolutions'} = get_legal_field_values('resolution');
+
+  ($vars->{'flag_types'}, $vars->{any_flags_requesteeble})
+    = _get_common_flag_types([keys %$bugcomponentids]);
+
+  # Convert bug statuses to their ID.
+  my @bug_statuses = map { $dbh->quote($_) } keys %$bugstatuses;
+  my $bug_status_ids = $dbh->selectcol_arrayref(
+    'SELECT id FROM bug_status
+                               WHERE ' . $dbh->sql_in('value', \@bug_statuses)
+  );
+
+  # This query collects new statuses which are common to all current bug statuses.
+  # It also accepts transitions where the bug status doesn't change.
+  $bug_status_ids = $dbh->selectcol_arrayref(
+    'SELECT DISTINCT sw1.new_status
                FROM status_workflow sw1
          INNER JOIN bug_status
                  ON bug_status.id = sw1.new_status
@@ -1011,74 +1036,78 @@ if ($dotweak && scalar @bugs) {
                    (SELECT * FROM status_workflow sw2
                      WHERE sw2.old_status != sw1.new_status 
                            AND '
-                         . $dbh->sql_in('sw2.old_status', $bug_status_ids)
-                         . ' AND NOT EXISTS 
+      . $dbh->sql_in('sw2.old_status', $bug_status_ids) . ' AND NOT EXISTS 
                            (SELECT * FROM status_workflow sw3
                              WHERE sw3.new_status = sw1.new_status
-                                   AND sw3.old_status = sw2.old_status))');
-
-    $vars->{'current_bug_statuses'} = [keys %$bugstatuses];
-    $vars->{'new_bug_statuses'} = Bugzilla::Status->new_from_list($bug_status_ids);
-
-    # The groups the user belongs to and which are editable for the given buglist.
-    $vars->{'groups'} = GetGroups(\@products);
-
-    # If all bugs being changed are in the same product, the user can change
-    # their version and component, so generate a list of products, a list of
-    # versions for the product (if there is only one product on the list of
-    # products), and a list of components for the product.
-    if ($one_product) {
-        $vars->{'versions'} = [map($_->name, grep($_->is_active, @{ $one_product->versions }))];
-        $vars->{'components'} = [map($_->name, grep($_->is_active, @{ $one_product->components }))];
-        if (Bugzilla->params->{'usetargetmilestone'}) {
-            $vars->{'milestones'} = [map($_->name, grep($_->is_active,
-                                               @{ $one_product->milestones }))];
-        }
+                                   AND sw3.old_status = sw2.old_status))'
+  );
+
+  $vars->{'current_bug_statuses'} = [keys %$bugstatuses];
+  $vars->{'new_bug_statuses'} = Bugzilla::Status->new_from_list($bug_status_ids);
+
+  # The groups the user belongs to and which are editable for the given buglist.
+  $vars->{'groups'} = GetGroups(\@products);
+
+  # If all bugs being changed are in the same product, the user can change
+  # their version and component, so generate a list of products, a list of
+  # versions for the product (if there is only one product on the list of
+  # products), and a list of components for the product.
+  if ($one_product) {
+    $vars->{'versions'}
+      = [map($_->name, grep($_->is_active, @{$one_product->versions}))];
+    $vars->{'components'}
+      = [map($_->name, grep($_->is_active, @{$one_product->components}))];
+    if (Bugzilla->params->{'usetargetmilestone'}) {
+      $vars->{'milestones'}
+        = [map($_->name, grep($_->is_active, @{$one_product->milestones}))];
+    }
+  }
+  else {
+    # We will only show the values at are active in all products.
+    my %values = ();
+    my @fields = ('components', 'versions');
+    if (Bugzilla->params->{'usetargetmilestone'}) {
+      push @fields, 'milestones';
     }
-    else {
-        # We will only show the values at are active in all products.
-        my %values = ();
-        my @fields = ('components', 'versions');
-        if (Bugzilla->params->{'usetargetmilestone'}) {
-            push @fields, 'milestones';
-        }
 
-        # Go through each product and count the number of times each field
-        # is used
-        foreach my $product_name (@products) {
-            my $product = Bugzilla::Product->new({name => $product_name, cache => 1});
-            foreach my $field (@fields) {
-                my $list = $product->$field;
-                foreach my $item (@$list) {
-                    ++$values{$field}{$item->name} if $item->is_active;
-                }
-            }
+    # Go through each product and count the number of times each field
+    # is used
+    foreach my $product_name (@products) {
+      my $product = Bugzilla::Product->new({name => $product_name, cache => 1});
+      foreach my $field (@fields) {
+        my $list = $product->$field;
+        foreach my $item (@$list) {
+          ++$values{$field}{$item->name} if $item->is_active;
         }
+      }
+    }
 
-        # Now we get the list of each field and see which values have
-        # $product_count (i.e. appears in every product)
-        my $product_count = scalar(@products);
-        foreach my $field (@fields) {
-            my @values = grep { $values{$field}{$_} == $product_count } keys %{$values{$field}};
-            if (scalar @values) {
-                @{$vars->{$field}} = $field eq 'version'
-                    ? sort { vers_cmp(lc($a), lc($b)) } @values
-                    : sort { lc($a) cmp lc($b) } @values
-            }
-
-            # Do we need to show a warning about limited visiblity?
-            if (@values != scalar keys %{$values{$field}}) {
-                $vars->{excluded_values} = 1;
-            }
-        }
+    # Now we get the list of each field and see which values have
+    # $product_count (i.e. appears in every product)
+    my $product_count = scalar(@products);
+    foreach my $field (@fields) {
+      my @values
+        = grep { $values{$field}{$_} == $product_count } keys %{$values{$field}};
+      if (scalar @values) {
+        @{$vars->{$field}}
+          = $field eq 'version'
+          ? sort { vers_cmp(lc($a), lc($b)) } @values
+          : sort { lc($a) cmp lc($b) } @values;
+      }
+
+      # Do we need to show a warning about limited visiblity?
+      if (@values != scalar keys %{$values{$field}}) {
+        $vars->{excluded_values} = 1;
+      }
     }
+  }
 }
 
 # If we're editing a stored query, use the existing query name as default for
 # the "Remember search as" field.
 $vars->{'defaultsavename'} = $cgi->param('query_based_on');
 
-# If we did a quick search then redisplay the previously entered search 
+# If we did a quick search then redisplay the previously entered search
 # string in the text field.
 $vars->{'quicksearch'} = $searchstring;
 
@@ -1092,32 +1121,33 @@ my $contenttype;
 my $disposition = "inline";
 
 if ($format->{'extension'} eq "html") {
-    my $list_id = $cgi->param('list_id') || $cgi->param('regetlastlist');
-    my $search = $user->save_last_search(
-        { bugs => \@bugidlist, order => $order, vars => $vars, list_id => $list_id });
-    $cgi->param('list_id', $search->id) if $search;
-    $contenttype = "text/html";
+  my $list_id = $cgi->param('list_id') || $cgi->param('regetlastlist');
+  my $search = $user->save_last_search(
+    {bugs => \@bugidlist, order => $order, vars => $vars, list_id => $list_id});
+  $cgi->param('list_id', $search->id) if $search;
+  $contenttype = "text/html";
 }
 else {
-    $contenttype = $format->{'ctype'};
+  $contenttype = $format->{'ctype'};
 }
 
 # Set 'urlquerypart' once the buglist ID is known.
-$vars->{'urlquerypart'} = $params->canonicalise_query('order', 'cmdtype',
-                                                      'query_based_on',
-                                                      'token');
+$vars->{'urlquerypart'}
+  = $params->canonicalise_query('order', 'cmdtype', 'query_based_on', 'token');
 
 if ($format->{'extension'} eq "csv") {
-    # We set CSV files to be downloaded, as they are designed for importing
-    # into other programs.
-    $disposition = "attachment";
 
-    # If the user clicked the CSV link in the search results,
-    # They should get the Field Description, not the column name in the db
-    $vars->{'human'} = $cgi->param('human');
+  # We set CSV files to be downloaded, as they are designed for importing
+  # into other programs.
+  $disposition = "attachment";
+
+  # If the user clicked the CSV link in the search results,
+  # They should get the Field Description, not the column name in the db
+  $vars->{'human'} = $cgi->param('human');
 }
 
-$cgi->close_standby_message($contenttype, $disposition, $disp_prefix, $format->{'extension'});
+$cgi->close_standby_message($contenttype, $disposition, $disp_prefix,
+  $format->{'extension'});
 
 ################################################################################
 # Content Generation
diff --git a/chart.cgi b/chart.cgi
index c1bafa117..a8c609fce 100755
--- a/chart.cgi
+++ b/chart.cgi
@@ -46,26 +46,27 @@ use Bugzilla::Token;
 # when preparing Bugzilla for mod_perl, this script used these
 # variables in so many subroutines that it was easier to just
 # make them globals.
-local our $cgi = Bugzilla->cgi;
+local our $cgi      = Bugzilla->cgi;
 local our $template = Bugzilla->template;
-local our $vars = {};
+local our $vars     = {};
 my $dbh = Bugzilla->dbh;
 
 my $user = Bugzilla->login(LOGIN_REQUIRED);
 
 if (!Bugzilla->feature('new_charts')) {
-    ThrowUserError('feature_disabled', { feature => 'new_charts' });
+  ThrowUserError('feature_disabled', {feature => 'new_charts'});
 }
 
 # Go back to query.cgi if we are adding a boolean chart parameter.
 if (grep(/^cmd-/, $cgi->param())) {
-    my $params = $cgi->canonicalise_query("format", "ctype", "action");
-    print $cgi->redirect("query.cgi?format=" . $cgi->param('query_format') .
-                                               ($params ? "&$params" : ""));
-    exit;
+  my $params = $cgi->canonicalise_query("format", "ctype", "action");
+  print $cgi->redirect("query.cgi?format="
+      . $cgi->param('query_format')
+      . ($params ? "&$params" : ""));
+  exit;
 }
 
-my $action = $cgi->param('action');
+my $action    = $cgi->param('action');
 my $series_id = $cgi->param('series_id');
 $vars->{'doc_section'} = 'using/reports-and-charts.html#charts';
 
@@ -75,283 +76,296 @@ $vars->{'doc_section'} = 'using/reports-and-charts.html#charts';
 # series_id they apply to (e.g. subscribe, unsubscribe).
 my @actions = grep(/^action-/, $cgi->param());
 if ($actions[0] && $actions[0] =~ /^action-([^\d]+)(\d*)$/) {
-    $action = $1;
-    $series_id = $2 if $2;
+  $action = $1;
+  $series_id = $2 if $2;
 }
 
 $action ||= "assemble";
 
 # Go to buglist.cgi if we are doing a search.
 if ($action eq "search") {
-    my $params = $cgi->canonicalise_query("format", "ctype", "action");
-    print $cgi->redirect("buglist.cgi" . ($params ? "?$params" : ""));
-    exit;
+  my $params = $cgi->canonicalise_query("format", "ctype", "action");
+  print $cgi->redirect("buglist.cgi" . ($params ? "?$params" : ""));
+  exit;
 }
 
-$user->in_group(Bugzilla->params->{"chartgroup"})
-  || ThrowUserError("auth_failure", {group  => Bugzilla->params->{"chartgroup"},
-                                     action => "use",
-                                     object => "charts"});
+$user->in_group(Bugzilla->params->{"chartgroup"}) || ThrowUserError(
+  "auth_failure",
+  {
+    group  => Bugzilla->params->{"chartgroup"},
+    action => "use",
+    object => "charts"
+  }
+);
 
 # Only admins may create public queries
 $user->in_group('admin') || $cgi->delete('public');
 
 # 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
-    # that the changes they make will be reflected in it.
-    if ($action =~ /^subscribe|unsubscribe$/) {
-        detaint_natural($series_id) || ThrowCodeError("invalid_series_id");
-        my $series = new Bugzilla::Series($series_id);
-        $series->$action($user->id);
-    }
-
-    my $chart = new Bugzilla::Chart($cgi);
 
-    if ($action =~ /^remove|sum$/) {
-        $chart->$action(getSelectedLines());
-    }
-    elsif ($action eq "add") {
-        my @series_ids = getAndValidateSeriesIDs();
-        $chart->add(@series_ids);
-    }
-
-    view($chart);
+  # These two need to be done before the creation of the Chart object, so
+  # that the changes they make will be reflected in it.
+  if ($action =~ /^subscribe|unsubscribe$/) {
+    detaint_natural($series_id) || ThrowCodeError("invalid_series_id");
+    my $series = new Bugzilla::Series($series_id);
+    $series->$action($user->id);
+  }
+
+  my $chart = new Bugzilla::Chart($cgi);
+
+  if ($action =~ /^remove|sum$/) {
+    $chart->$action(getSelectedLines());
+  }
+  elsif ($action eq "add") {
+    my @series_ids = getAndValidateSeriesIDs();
+    $chart->add(@series_ids);
+  }
+
+  view($chart);
 }
 elsif ($action eq "plot") {
-    plot();
+  plot();
 }
 elsif ($action eq "wrap") {
-    # For CSV "wrap", we go straight to "plot".
-    if ($cgi->param('ctype') && $cgi->param('ctype') eq "csv") {
-        plot();
-    }
-    else {
-        wrap();
-    }
+
+  # For CSV "wrap", we go straight to "plot".
+  if ($cgi->param('ctype') && $cgi->param('ctype') eq "csv") {
+    plot();
+  }
+  else {
+    wrap();
+  }
 }
 elsif ($action eq "create") {
-    assertCanCreate($cgi);
-    my $token = $cgi->param('token');
-    check_hash_token($token, ['create-series']);
-    
-    my $series = new Bugzilla::Series($cgi);
+  assertCanCreate($cgi);
+  my $token = $cgi->param('token');
+  check_hash_token($token, ['create-series']);
 
-    ThrowUserError("series_already_exists", {'series' => $series})
-      if $series->existsInDatabase;
+  my $series = new Bugzilla::Series($cgi);
 
-    $series->writeToDatabase();
-    $vars->{'message'} = "series_created";
-    $vars->{'series'} = $series;
+  ThrowUserError("series_already_exists", {'series' => $series})
+    if $series->existsInDatabase;
 
-    my $chart = new Bugzilla::Chart($cgi);
-    view($chart);
+  $series->writeToDatabase();
+  $vars->{'message'} = "series_created";
+  $vars->{'series'}  = $series;
+
+  my $chart = new Bugzilla::Chart($cgi);
+  view($chart);
 }
 elsif ($action eq "edit") {
-    my $series = assertCanEdit($series_id);
-    edit($series);
+  my $series = assertCanEdit($series_id);
+  edit($series);
 }
 elsif ($action eq "alter") {
-    my $series = assertCanEdit($series_id);
-    my $token = $cgi->param('token');
-    check_hash_token($token, [$series->id, $series->name]);
-    # XXX - This should be replaced by $series->set_foo() methods.
-    $series = new Bugzilla::Series($cgi);
-
-    # We need to check if there is _another_ series in the database with
-    # our (potentially new) name. So we call existsInDatabase() to see if
-    # the return value is us or some other series we need to avoid stomping
-    # on.
-    my $id_of_series_in_db = $series->existsInDatabase();
-    if (defined($id_of_series_in_db) && 
-        $id_of_series_in_db != $series->{'series_id'}) 
-    {
-        ThrowUserError("series_already_exists", {'series' => $series});
-    }
-    
-    $series->writeToDatabase();
-    $vars->{'changes_saved'} = 1;
-    
-    edit($series);
+  my $series = assertCanEdit($series_id);
+  my $token  = $cgi->param('token');
+  check_hash_token($token, [$series->id, $series->name]);
+
+  # XXX - This should be replaced by $series->set_foo() methods.
+  $series = new Bugzilla::Series($cgi);
+
+  # We need to check if there is _another_ series in the database with
+  # our (potentially new) name. So we call existsInDatabase() to see if
+  # the return value is us or some other series we need to avoid stomping
+  # on.
+  my $id_of_series_in_db = $series->existsInDatabase();
+  if (defined($id_of_series_in_db)
+    && $id_of_series_in_db != $series->{'series_id'})
+  {
+    ThrowUserError("series_already_exists", {'series' => $series});
+  }
+
+  $series->writeToDatabase();
+  $vars->{'changes_saved'} = 1;
+
+  edit($series);
 }
 elsif ($action eq "confirm-delete") {
-    $vars->{'series'} = assertCanEdit($series_id);
+  $vars->{'series'} = assertCanEdit($series_id);
 
-    print $cgi->header();
-    $template->process("reports/delete-series.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
+  print $cgi->header();
+  $template->process("reports/delete-series.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
 }
 elsif ($action eq "delete") {
-    my $series = assertCanEdit($series_id);
-    my $token = $cgi->param('token');
-    check_hash_token($token, [$series->id, $series->name]);
-
-    $dbh->bz_start_transaction();
-
-    $series->remove_from_db();
-    # Remove (sub)categories which no longer have any series.
-    foreach my $cat (qw(category subcategory)) {
-        my $is_used = $dbh->selectrow_array("SELECT COUNT(*) FROM series WHERE $cat = ?",
-                                             undef, $series->{"${cat}_id"});
-        if (!$is_used) {
-            $dbh->do('DELETE FROM series_categories WHERE id = ?',
-                      undef, $series->{"${cat}_id"});
-        }
+  my $series = assertCanEdit($series_id);
+  my $token  = $cgi->param('token');
+  check_hash_token($token, [$series->id, $series->name]);
+
+  $dbh->bz_start_transaction();
+
+  $series->remove_from_db();
+
+  # Remove (sub)categories which no longer have any series.
+  foreach my $cat (qw(category subcategory)) {
+    my $is_used
+      = $dbh->selectrow_array("SELECT COUNT(*) FROM series WHERE $cat = ?",
+      undef, $series->{"${cat}_id"});
+    if (!$is_used) {
+      $dbh->do('DELETE FROM series_categories WHERE id = ?',
+        undef, $series->{"${cat}_id"});
     }
-    $dbh->bz_commit_transaction();
+  }
+  $dbh->bz_commit_transaction();
 
-    $vars->{'message'} = "series_deleted";
-    $vars->{'series'} = $series;
-    view();
+  $vars->{'message'} = "series_deleted";
+  $vars->{'series'}  = $series;
+  view();
 }
 elsif ($action eq "convert_search") {
-    my $saved_search = $cgi->param('series_from_search') || '';
-    my ($query) = grep { $_->name eq $saved_search } @{ $user->queries };
-    my $url = '';
-    if ($query) {
-        my $params = new Bugzilla::CGI($query->edit_link);
-        # These two parameters conflict with the one below.
-        $url = $params->canonicalise_query('format', 'query_format');
-        $url = '&' . html_quote($url);
-    }
-    print $cgi->redirect(-location => correct_urlbase() . "query.cgi?format=create-series$url");
+  my $saved_search = $cgi->param('series_from_search') || '';
+  my ($query) = grep { $_->name eq $saved_search } @{$user->queries};
+  my $url = '';
+  if ($query) {
+    my $params = new Bugzilla::CGI($query->edit_link);
+
+    # These two parameters conflict with the one below.
+    $url = $params->canonicalise_query('format', 'query_format');
+    $url = '&' . html_quote($url);
+  }
+  print $cgi->redirect(
+    -location => correct_urlbase() . "query.cgi?format=create-series$url");
 }
 else {
-    ThrowUserError('unknown_action', {action => $action});
+  ThrowUserError('unknown_action', {action => $action});
 }
 
 exit;
 
 # Find any selected series and return either the first or all of them.
 sub getAndValidateSeriesIDs {
-    my @series_ids = grep(/^\d+$/, $cgi->param("name"));
+  my @series_ids = grep(/^\d+$/, $cgi->param("name"));
 
-    return wantarray ? @series_ids : $series_ids[0];
+  return wantarray ? @series_ids : $series_ids[0];
 }
 
 # Return a list of IDs of all the lines selected in the UI.
 sub getSelectedLines {
-    my @ids = map { /^select(\d+)$/ ? $1 : () } $cgi->param();
+  my @ids = map { /^select(\d+)$/ ? $1 : () } $cgi->param();
 
-    return @ids;
+  return @ids;
 }
 
-# Check if the user is the owner of series_id or is an admin. 
+# Check if the user is the owner of series_id or is an admin.
 sub assertCanEdit {
-    my $series_id = shift;
-    my $user = Bugzilla->user;
+  my $series_id = shift;
+  my $user      = Bugzilla->user;
 
-    my $series = new Bugzilla::Series($series_id)
-      || ThrowCodeError('invalid_series_id');
+  my $series
+    = new Bugzilla::Series($series_id) || ThrowCodeError('invalid_series_id');
 
-    if (!$user->in_group('admin') && $series->{creator_id} != $user->id) {
-        ThrowUserError('illegal_series_edit');
-    }
+  if (!$user->in_group('admin') && $series->{creator_id} != $user->id) {
+    ThrowUserError('illegal_series_edit');
+  }
 
-    return $series;
+  return $series;
 }
 
 # Check if the user is permitted to create this series with these parameters.
 sub assertCanCreate {
-    my ($cgi) = shift;
-    my $user = Bugzilla->user;
+  my ($cgi) = shift;
+  my $user = Bugzilla->user;
 
-    $user->in_group("editbugs") || ThrowUserError("illegal_series_creation");
+  $user->in_group("editbugs") || ThrowUserError("illegal_series_creation");
 
-    # Check permission for frequency
-    my $min_freq = 7;
-    if ($cgi->param('frequency') < $min_freq && !$user->in_group("admin")) {
-        ThrowUserError("illegal_frequency", { 'minimum' => $min_freq });
-    }
+  # Check permission for frequency
+  my $min_freq = 7;
+  if ($cgi->param('frequency') < $min_freq && !$user->in_group("admin")) {
+    ThrowUserError("illegal_frequency", {'minimum' => $min_freq});
+  }
 }
 
 sub validateWidthAndHeight {
-    $vars->{'width'} = $cgi->param('width');
-    $vars->{'height'} = $cgi->param('height');
-
-    if (defined($vars->{'width'})) {
-       (detaint_natural($vars->{'width'}) && $vars->{'width'} > 0)
-         || ThrowUserError("invalid_dimensions");
-    }
-
-    if (defined($vars->{'height'})) {
-       (detaint_natural($vars->{'height'}) && $vars->{'height'} > 0)
-         || ThrowUserError("invalid_dimensions");
-    }
-
-    # The equivalent of 2000 square seems like a very reasonable maximum size.
-    # This is merely meant to prevent accidental or deliberate DOS, and should
-    # have no effect in practice.
-    if ($vars->{'width'} && $vars->{'height'}) {
-       (($vars->{'width'} * $vars->{'height'}) <= 4000000)
-         || ThrowUserError("chart_too_large");
-    }
+  $vars->{'width'}  = $cgi->param('width');
+  $vars->{'height'} = $cgi->param('height');
+
+  if (defined($vars->{'width'})) {
+    (detaint_natural($vars->{'width'}) && $vars->{'width'} > 0)
+      || ThrowUserError("invalid_dimensions");
+  }
+
+  if (defined($vars->{'height'})) {
+    (detaint_natural($vars->{'height'}) && $vars->{'height'} > 0)
+      || ThrowUserError("invalid_dimensions");
+  }
+
+  # The equivalent of 2000 square seems like a very reasonable maximum size.
+  # This is merely meant to prevent accidental or deliberate DOS, and should
+  # have no effect in practice.
+  if ($vars->{'width'} && $vars->{'height'}) {
+    (($vars->{'width'} * $vars->{'height'}) <= 4000000)
+      || ThrowUserError("chart_too_large");
+  }
 }
 
 sub edit {
-    my $series = shift;
+  my $series = shift;
 
-    $vars->{'category'} = Bugzilla::Chart::getVisibleSeries();
-    $vars->{'default'} = $series;
-    $vars->{'message'} = 'series_updated' if $vars->{'changes_saved'};
+  $vars->{'category'} = Bugzilla::Chart::getVisibleSeries();
+  $vars->{'default'}  = $series;
+  $vars->{'message'}  = 'series_updated' if $vars->{'changes_saved'};
 
-    print $cgi->header();
-    $template->process("reports/edit-series.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
+  print $cgi->header();
+  $template->process("reports/edit-series.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
 }
 
 sub plot {
-    validateWidthAndHeight();
-    $vars->{'chart'} = new Bugzilla::Chart($cgi);
+  validateWidthAndHeight();
+  $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');
+  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\//);
+  $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
-    $vars->{'chart'}->dump() if $cgi->param('debug');
+  # Debugging PNGs is a pain; we need to be able to see the error messages
+  $vars->{'chart'}->dump() if $cgi->param('debug');
 
-    $template->process($format->{'template'}, $vars)
-      || ThrowTemplateError($template->error());
+  $template->process($format->{'template'}, $vars)
+    || ThrowTemplateError($template->error());
 }
 
 sub wrap {
-    validateWidthAndHeight();
-    
-    # We create a Chart object so we can validate the parameters
-    my $chart = new Bugzilla::Chart($cgi);
-    
-    $vars->{'time'} = localtime(time());
-
-    $vars->{'imagebase'} = $cgi->canonicalise_query(
-                "action", "action-wrap", "ctype", "format", "width", "height");
-
-    print $cgi->header();
-    $template->process("reports/chart.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
+  validateWidthAndHeight();
+
+  # We create a Chart object so we can validate the parameters
+  my $chart = new Bugzilla::Chart($cgi);
+
+  $vars->{'time'} = localtime(time());
+
+  $vars->{'imagebase'}
+    = $cgi->canonicalise_query("action", "action-wrap", "ctype", "format",
+    "width", "height");
+
+  print $cgi->header();
+  $template->process("reports/chart.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
 }
 
 sub view {
-    my $chart = shift;
+  my $chart = shift;
 
-    # Set defaults
-    foreach my $field ('category', 'subcategory', 'name', 'ctype') {
-        $vars->{'default'}{$field} = $cgi->param($field) || 0;
-    }
+  # Set defaults
+  foreach my $field ('category', 'subcategory', 'name', 'ctype') {
+    $vars->{'default'}{$field} = $cgi->param($field) || 0;
+  }
 
-    # Pass the state object to the display UI.
-    $vars->{'chart'} = $chart;
-    $vars->{'category'} = Bugzilla::Chart::getVisibleSeries();
+  # Pass the state object to the display UI.
+  $vars->{'chart'}    = $chart;
+  $vars->{'category'} = Bugzilla::Chart::getVisibleSeries();
 
-    print $cgi->header();
+  print $cgi->header();
 
-    # 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 we have having problems with bad data, we can set debug=1 to dump
+  # the data structure.
+  $chart->dump() if $cgi->param('debug');
 
-    $template->process("reports/create-chart.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
+  $template->process("reports/create-chart.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
 }
diff --git a/checksetup.pl b/checksetup.pl
index 5dda0df6f..76564e2f0 100755
--- a/checksetup.pl
+++ b/checksetup.pl
@@ -26,8 +26,8 @@ use Safe;
 
 use Bugzilla::Constants;
 use Bugzilla::Install::Requirements;
-use Bugzilla::Install::Util qw(install_string get_version_and_os 
-                               init_console success);
+use Bugzilla::Install::Util qw(install_string get_version_and_os
+  init_console success);
 
 ######################################################################
 # Live Code
@@ -42,24 +42,25 @@ init_console();
 
 my %switch;
 GetOptions(\%switch, 'help|h|?', 'check-modules', 'no-templates|t',
-                     'verbose|v|no-silent', 'make-admin=s', 
-                     'reset-password=s', 'version|V');
+  'verbose|v|no-silent', 'make-admin=s', 'reset-password=s', 'version|V');
 
 # Print the help message if that switch was selected.
 pod2usage({-verbose => 1, -exitval => 1}) if $switch{'help'};
 
-# Read in the "answers" file if it exists, for running in 
+# Read in the "answers" file if it exists, for running in
 # non-interactive mode.
 my $answers_file = $ARGV[0];
 my $silent = $answers_file && !$switch{'verbose'};
 
 print(install_string('header', get_version_and_os()) . "\n") unless $silent;
 exit 0 if $switch{'version'};
+
 # Check required --MODULES--
 my $module_results = check_requirements(!$silent);
-Bugzilla::Install::Requirements::print_module_instructions(
-    $module_results, !$silent);
+Bugzilla::Install::Requirements::print_module_instructions($module_results,
+  !$silent);
 exit 1 if !$module_results->{pass};
+
 # Break out if checking the modules is all we have been asked to do.
 exit 0 if $switch{'check-modules'};
 
@@ -86,7 +87,7 @@ import Bugzilla::Install::Localconfig qw(update_localconfig);
 
 require Bugzilla::Install::Filesystem;
 import Bugzilla::Install::Filesystem qw(update_filesystem create_htaccess
-                                        fix_all_file_permissions);
+  fix_all_file_permissions);
 require Bugzilla::Install::DB;
 require Bugzilla::DB;
 require Bugzilla::Template;
@@ -100,8 +101,8 @@ Bugzilla->installation_answers($answers_file);
 # Check and update --LOCAL-- configuration
 ###########################################################################
 
-print "Reading " .  bz_locations()->{'localconfig'} . "...\n" unless $silent;
-update_localconfig({ output => !$silent });
+print "Reading " . bz_locations()->{'localconfig'} . "...\n" unless $silent;
+update_localconfig({output => !$silent});
 my $lc_hash = Bugzilla->localconfig;
 
 ###########################################################################
@@ -117,8 +118,10 @@ Bugzilla::DB::bz_create_database() if $lc_hash->{'db_check'};
 
 # now get a handle to the database:
 my $dbh = Bugzilla->dbh;
+
 # Create the tables, and do any database-specific schema changes.
 $dbh->bz_setup_database();
+
 # Populate the tables that hold the values for the