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 "
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/^/;
+ $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/^/;
- $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/\\x3c/g;
- $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/\\x3c/g;
+ $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;
- $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;
+ $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#($safe)>#$chr/$1$chr#go;
- # Now filter < and >.
- $text =~ s#<#<#g;
- $text =~ s#>#>#g;
- # Restore safe elements.
- $text =~ s#$chr/($safe)$chr#$1>#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#($safe)>#$chr/$1$chr#go;
+
+ # Now filter < and >.
+ $text =~ s#<#<#g;
+ $text =~ s#>#>#g;
+
+ # Restore safe elements.
+ $text =~ s#$chr/($safe)$chr#$1>#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;",ord($1))/eg;
- return $toencode;
+ my ($toencode) = (@_);
+ $toencode =~ s#[ /]#_#g;
+ $toencode =~ s/([^a-zA-Z0-9_\-.])/uc sprintf("%x;",ord($1))/eg;
+ return $toencode;
}
sub xml_quote {
- my ($var) = (@_);
- $var =~ s/\&/\&/g;
- $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;
+ $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